<?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>Mon, 24 Aug 2026 20:25:08 +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>gRPC 전환 : 죽은 연결 지속 문제와 공통 설정 표준화</title>
      <link>https://ecsimsw.tistory.com/entry/gRPC-balancing</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;b&gt;REST에서 gRPC로 전환&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;팀의 공통 학습을 위해 각자가 자신 있는 서비스 하나씩을 맡아 작업했고, 나는 역시 OpenAPI 서비스였다.&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;사실 gRPC에 익숙하지 않았던지라, 개인적인 공부가 필요했다.&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;gRPC는 연결을 유지하며 그 안에서 요청을 처리한다.&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;내가 담당한 서비스 외에도, 다른 팀원들의 담당 서비스의 코드, 전략도 확인했다.&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;b&gt;서비스마다 분산되어 있었던 gRPC 설정의 표준화 라이브러리 개발&lt;/b&gt;을 맡게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 글에선 쿠버네티스 + gRPC + SpringBoot 환경에서 놓치기 쉬운, &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;- 부하 쏠림 문제&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;라운드&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;미감지&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;종료&amp;nbsp;시&amp;nbsp;&amp;nbsp;죽은&amp;nbsp;연결&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;- 검증&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;쿠버네티스 환경에서 서비스 간 통신은, 보통 Service로 요청하여 Pod 분산 요청하는 꼴을 기본으로 생각할 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이때 Kube-Proxy는 L4 LB라서 연결을 맺는 순간 요청 정보는 모른다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;한번 연결을 맺어 전달을 시작했다면, 'A 요청은 1번 Pod로, B 요청은 2번 Pod로' 와 같은 분산은 불가능하다.&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;REST 때처럼 ClusterIp 타입의 Service로 gRPC 서버를 찾아선, &lt;b&gt;한 Pod로만 요청이 전달된다는 뜻&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;그렇다고 내부 통신을 위한 L7 LB를 따로 두거나 전면 프록시를 타게 하기에는 지연&amp;middot;비용에, 과하다고 판단했다.&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;b&gt;Headless Service&lt;/b&gt;는 ClusterIp 를 None으로, 가상 주소를 안 갖는 Service 형태를 말한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이를 사용하면 DNS처럼 Pod IP를 전부 반환받을 수 있게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;gRPC 클라이언트는 기존 L4 LB의 Service에서 벗어나, 직접 모든 gRPC 서버 Pod 들의 접근 경로를 알게 된다.&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;gRPC 클라이언트의 &lt;b&gt;라운드 로빈&lt;/b&gt;은, 유지 중인 연결에 순차적으로 요청을 전달하는 설정이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클라이언트 단에서 직접 요청을 연결에 순차적으로 전달하기 때문에, L4 LB 처럼 한 연결에만 요청이 전달되지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결국 요청 정보를 구분할 수 없는 L4 LB Service를 피해, &lt;b&gt;클라이언트가 직접 돌아가며 요청하는 것&lt;/b&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;2680&quot; data-origin-height=&quot;962&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNjNAj/dJMcad3Z9EE/Q73aswpgw94BYylcOixHMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNjNAj/dJMcad3Z9EE/Q73aswpgw94BYylcOixHMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNjNAj/dJMcad3Z9EE/Q73aswpgw94BYylcOixHMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNjNAj%2FdJMcad3Z9EE%2FQ73aswpgw94BYylcOixHMk%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;2680&quot; height=&quot;962&quot; data-origin-width=&quot;2680&quot; data-origin-height=&quot;962&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;그럼 gRPC 클라이언트는 매번 Headless service에 Pod 목록을 질의할까?&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;b&gt;연결 정보를 재확인하는 것은, 연결이 끊겼을 때뿐이다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요청이 많아져&amp;nbsp;gRPC 서버가 스케일 아웃된 경우를 생각해 보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아직 연결이 끊기지 않았기에 gRPC 클라이언트는 서버가 확장되었는지 여부를 알 수 없다.&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;b&gt;MAX_CONNECTION_AGE&amp;nbsp;&lt;/b&gt;는 연결 최대 유지 시간을 정하는 gRPC 서버의 설정이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버가 주기적으로 연결을 종료하고 재연결하고, 이때 발생하는 연결 끊김 이벤트로 클라이언트는 DNS 쿼리를 다시 조회한다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2644&quot; data-origin-height=&quot;1190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bryp8H/dJMcagT2KL9/jw3DlvFMT6irLf8R4jmrRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bryp8H/dJMcagT2KL9/jw3DlvFMT6irLf8R4jmrRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bryp8H/dJMcagT2KL9/jw3DlvFMT6irLf8R4jmrRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbryp8H%2FdJMcagT2KL9%2Fjw3DlvFMT6irLf8R4jmrRk%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;2644&quot; height=&quot;1190&quot; data-origin-width=&quot;2644&quot; data-origin-height=&quot;1190&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;gRPC는 연결이 유지된다. &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만약 gRPC 서버가 죽으면 그 연결은 어떻게 될까.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;롤링 배포와 같은 정상적인 종료 상황에서는 서버가 종료 신호 (GOAWAY)를 전달하여 깔끔하게 연결을 재수립한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;특히 Spring boot에선 Graceful shutdown을 지원하기에, gRPC의 연결은 정상 종료된다.&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;Deadline &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;background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&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;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; 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;&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;&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;문제는 OOM, SIGKILL, 노드 다운과 같은 비정상 종료이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;종료 신호가 안 오면, TCP 연결은 끊긴 줄도 모르고 클라이언트는 해당 파드에 요청을 전송하게 된다.&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;KeepAlive&lt;/b&gt; 설정은 주기적으로 PING을 보내고, 죽은 연결을 찾아 종료하는 역할을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클라이언트는 PING을 보낼 주기와 응답 타임아웃을 설정하고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버는 너무 많은 PING을 막기 위한 허용치를 설정한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉 클라이언트의 PING 주기 + 응답 타임아웃이 장애 연결 유지 최대 시간이 된다.&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;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;이 길이가 너무 길면 장애 시 종료된 연결로 요청이 가 실패될 수 있고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;반대로 PING이 너무 잦으면 네트워크 부하가 되어 부담이 될 수 있다.&lt;/span&gt;&lt;/p&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://grpc.io/docs/guides/keepalive/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;gRPC 공식 문서 / KeepAlive&lt;/a&gt; 에선 1분 이상을 권장하고 있고, 팀의 공동 설정에 근거가 되었다.&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;keepalive가 죽은 연결을 감지하기까지의 시간 동안, 해당 연결로 간 요청은 여전히 실패한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 구간의 실패를 줄이려면 서비스마다 retry 설정이 필요하다.&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&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;실패한 요청을 다른 살아있는 파드로 다시 보내면, 사용자 입장에서는 느린 성공이 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, keepalive = 죽은 연결 제거, deadline = 개별 요청 시간제한, retry = 죽은 연결로의 요청 실패를 성공으로 전환하는 역할을 한다.&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;공통&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;내가 찾아 팀에 공유했던 내용은 이&amp;nbsp;&lt;b&gt;KeepAlive &lt;/b&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;팀 내 gRPC 클라이언트 전체가 keepalive 설정이 없었고, 서버의 max-connection-age도 일부 서비스에만 개별로 들어가 있었다.&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;이에 공통 모듈에 gRPC 설정을 정의했다.&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;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;1. 공통 모듈에 넣은 것&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;클라이언트 keepalive, 서버 permit, 서버 max-connection-age 세 개를 넣었다.&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;서버 설정은 EnvironmentPostProcessor로 기본 프로퍼티를 주입했다. &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;최저 우선순위로 들어가기 때문에 서비스 yml에서 오버라이딩할 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;클라이언트 keepalive는 프로퍼티가 아닌 GrpcChannelBuilderCustomizer로 적용했다. &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;spring-grpc의 default-channel 프로퍼티는 명시적으로 정의한 채널에 상속되지&amp;nbsp;않는다.&amp;nbsp; &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;따라서 round_robin 처럼 채널을 명시가 필수적인 서비스는 keepalive 설정을 못 받는 문제가 있었다. &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;Customizer는 모든 채널 빌더에 실행되어 이&amp;nbsp;문제를 우회한다. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1786102560765&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean       
GrpcChannelBuilderCustomizer&amp;lt;NettyChannelBuilder&amp;gt; grpcClientKeepAliveCustomizer(
   GrpcClientKeepAliveProperties props) {
      return (name, builder) -&amp;gt; builder                         
          .keepAliveTime(props.getTime().toMillis(), TimeUnit.MILLISECONDS)                                        
          .keepAliveTimeout(props.getTimeout().toMillis(), TimeUnit.MILLISECONDS)                                        
          .keepAliveWithoutCalls(props.isWithoutCalls()
  );
}&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;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;2. 공통 모듈에서 뺀 것&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;클라이언트의 round_robin 은 Headless DNS에서만 의미가 있다.&amp;nbsp;&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;즉 적용하려는 서비스의 인프라 환경에 의존된다.&amp;nbsp;&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;또, Deadline은 타임 아웃으로 각 서비스마다 적절한 값이 다르다.&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;이 두 값은 전역 기본값으로 관리하기 부적합하다고 판단했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1786033993921&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 클라이언트
keep-alive-time: 60s        # gRPC 공식 문서는 1분 미만 비권장
keep-alive-timeout: 10s
keep-alive-without-calls: true

# 서버
permit-keep-alive-time: 50s  # 클라이언트보다 낮게. 여유 10s로 too_many_pings 방지
permit-keep-alive-without-calls: true
max-connection-age: 60s
max-connection-age-grace: 10s&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;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;최소한의 gRPC 서버&amp;middot;클라이언트를 개발 환경 k8s에 띄워 확인했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버는&amp;nbsp;자기&amp;nbsp;파드&amp;nbsp;이름을&amp;nbsp;응답하고,&amp;nbsp;클라이언트는&amp;nbsp;0.4초마다&amp;nbsp;호출해&amp;nbsp;어느&amp;nbsp;파드가&amp;nbsp;받았는지&amp;nbsp;누적&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;1. 쏠림 현상&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;- gRPC 서버를 3개 Pod로 띄우고 클라이언트 측에서 41번의 요청을 전송했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 기존 ClusterIP + pick_first(기본값)에서는 41번의 요청이 전부 한 파드로만 갔다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- headless + round_robin으로 전환하고 세 파드로 균등하게 분산되는 것을 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1786034365170&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# pick_first + ClusterIP
$ kubectl logs -l app=whoami-client --tail=1
counts={5wdhn: 41}

# round_robin + headless service
$ kubectl set env deploy/whoami-client TARGET=dns:///whoami-headless:9090 LB=round_robin
$ kubectl logs -l app=whoami-client --tail=1
counts={5wdhn: 11, mx5rc: 10, ncdtp: 10}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. 스케일아웃 미감지 현상&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;- 서버 파드를 3개에서 5개로 늘렸다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #111111; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- MAX_CONNECTION_AGE 없이는 45초가 지나도 새 파드에 트래픽이 0, 감지를 못하는 모습을 볼 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 연결 유지 시간을 설정하니 서버가 주기적으로 연결을 닫고, 클라이언트가 재연결하면서 새 파드를 자동으로 찾아 분산되었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1786034639235&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# MAX_CONNECTION_AGE 없이 스케일아웃
$ kubectl scale deploy/whoami-server --replicas=5
$ kubectl logs -l app=whoami-client --tail=1   # 45초 뒤
counts={5wdhn: 65, mx5rc: 64, ncdtp: 63}   # 새 파드 2개 = 0

# MAX_CONNECTION_AGE 설정 후 스케일아웃
$ kubectl set env deploy/whoami-server GRPC_SERVER_MAX_CONNECTION_AGE=20s
$ kubectl scale deploy/whoami-server --replicas=5   # 40초 뒤
counts={cz6pt: 46, 48bzx: 44, lj2cq: 44, v8jqb: 13, 6r5hb: 13}&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;&lt;b&gt;3. 비정상 종료&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;- 파드를 삭제하는 경우에는 서버가 GOAWAY를 전송하여 정상 종료, 클라이언트가 재연결한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 진짜 문제는 OOMKill이나 노드 장애처럼 신호 없이 죽는 경우다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 공용 개발 환경에서 처리할 수 없어, 로컬에 Kind를 설치, 네트워크 패킷을 iptables로 DROP 해서, 노드 장애 상황을 재현했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- ACK가 안 오니까 KeepAlive PING이 응답을 못 받아 죽음을 감지할 수 있었고, 연결이 풀에서 제거되어 더 이상 요청이 가지 않았다.&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;1308&quot; data-origin-height=&quot;332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFtLWE/dJMcagl9gEs/1AGxGBwvPH8dryQ0x77xX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFtLWE/dJMcagl9gEs/1AGxGBwvPH8dryQ0x77xX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFtLWE/dJMcagl9gEs/1AGxGBwvPH8dryQ0x77xX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFtLWE%2FdJMcagl9gEs%2F1AGxGBwvPH8dryQ0x77xX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;714&quot; height=&quot;181&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;332&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;- REST에서 gRPC로 전환하면서 k8s 환경에서의 연결 고정, 스케일아웃 미감지, 비정상 종료 시 죽은 연결 지속 문제를 발견, 팀 공유&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; &amp;nbsp;- 부하 쏠림: headless + round_robin으로 클라이언트 단에서 직접 분산&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 스케일아웃 미감지: MAX_CONNECTION_AGE로 주기적 연결 재수립, DNS 재조회 유도&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 비정상 종료: keepalive로 죽은 연결 감지 및 제거, deadline + retry로 감지 전 실패 대응&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 공통 모듈: gRPC 서버, 클라이언트 공통 설정을 공동 라이브러리에 기본값으로 정의&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; &amp;nbsp;- 개발 환경에서 쏠림, 스케일아웃, 노드 장애를 재현하여 각 설정의 효과를 실측&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&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;&amp;nbsp; &amp;nbsp;- iptables DROP으로 비정상 종료를 재현, keepalive ON/OFF 대조로 복구 여부 확인&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&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;&amp;nbsp; &amp;nbsp;- 실 EKS stg에서 동일 현상 발견 및 수정 확인&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/1200</guid>
      <comments>https://ecsimsw.tistory.com/entry/gRPC-balancing#entry1200comment</comments>
      <pubDate>Fri, 7 Aug 2026 00:06:47 +0900</pubDate>
    </item>
    <item>
      <title>Apache APISIX Kafka TLS 연결 지원 추가</title>
      <link>https://ecsimsw.tistory.com/entry/APISIX-kafka-logger-TLS</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;회사에서 레거시 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;나는 그중 &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;게이트웨이로는 다양한 플러그인과 활발한 커뮤니티, NginX 기반의 빠른 성능을 갖고 있는 ApiSix를 선정했다.&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;액세스 로그를 Kafka로 전달하고, 이를 OLAP(Starrocks)가 수신, 집계 처리하는 구성이었는데,&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;사용자별 사용량, 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;&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;관리 포인트 최소화를 위해, 가급적 커스텀 없이 플러그인의 기본 설정을 활용하려 노력했고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ApiSix는 이미 플러그인으로 이 기능을 지원하여 간단한 설정만으로 액세스 로그를 카프카로 전달할 수 있었다.&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;온프레미스로 구성된 개발 환경에서의 PoC와 달리, AWS로 구성된 Stage 환경에선 정상 동작하지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용하고자 했던 ApiSix의 kafka-logger 플러그인은 TLS를 지원하지 않았고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;TLS를 강제하는 MSK의 환경에선 카프카 연결이 실패했던 것이 원인이었다.&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;액세스 로그를 파일로 남겼고, Fluentbit 를 사이드카로 액세스 로그를 카프카에 전달했다.&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;플러그인은 Kafka producer로 &quot;&lt;span style=&quot;text-align: start;&quot;&gt;lua-resty-kafka&quot;라는 오픈소스 라이브러리를 사용하고 있었다.&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;해당 라이브러리는 TLS를 지원하지만, &lt;b&gt;ApiSix에서 이를 사용하는 과정에서 TLS 옵션을 전달하지 않고 있음&lt;/b&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;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;크게 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;1. &lt;span style=&quot;text-align: start;&quot;&gt;메인 테이너의 시간을 뺏지 않을 것&lt;/span&gt;&lt;/b&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; 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; text-align: start;&quot;&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;- 메인테이너의 판단이 필요할 땐, 열린 질문이 아닌&amp;nbsp;선택지를&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;span style=&quot;text-align: start;&quot;&gt;2. &lt;span style=&quot;text-align: start;&quot;&gt;하위호환이 가능할 것&lt;/span&gt;&lt;/span&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; text-align: start;&quot;&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; text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;- 문제 플러그인 외 다른 플러그인도 동일한 Kafka producer를 사용, 영향 범위에 있었고, 같은 문제를 갖고 있었다.&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;&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;&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;- PR의 소개에서는 작업 결과를 짧고 깔끔하게 정리하는 데 집중하고, 실제 동작은 테스트를 통해 증명되도록 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 성공 / 실패해야 하는 케이스를 나눠 유닛, 통합 테스트를 작성하고, 로컬에 CI 환경을 재현하여 실제로 동작하는지 검증했다.&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;대안을 PR 코멘트에 남겨 리뷰어가 판단할 수 있게 했다.&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;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;작업&lt;/span&gt;&lt;/span&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;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; - APISIX(OpenResty/Lua), lua-resty-kafka, Perl Test::Nginx, GitHub Actions CI&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; - kafka-logger, error-log-logger 플러그인에 TLS 연결 옵션을 추가하여 MSK 같은 TLS 브로커 연결을 지원&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;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 플러그인 코드를 따라가 하위 라이브러리(lua-resty-kafka)가 TLS를 지원함을 확인&lt;/span&gt;&lt;br /&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; - 같은 라이브러리를 사용하는 다른 플러그인 검토, 동일 버그 확인&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;- 기능 추가&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 두 플러그인에 tls object 스키마 추가, broker_config에 ssl/ssl_verify 옵션 전달&lt;/span&gt;&lt;br /&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; - 사용 문서에 TLS 속성 테이블과 사용 예시 추가&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;- 테스트 추가&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 단위: 스키마 검증(tls 유효, 하위호환, 타입 오류 거부), 보안 경고 검증&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 통합: 공용 CI 환경에 SSL Kafka 리스너 추가, TLS 연결&amp;nbsp;성공과 인증서 검증 거부를 대비시켜 옵션 동작 증명&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 검증: 로컬에 CI 환경을 재현, 영향 범위 전체 통과 확인&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;- 프로젝트:&lt;/b&gt; &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/apache/apisix&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Apache APISIX&lt;/a&gt; / OpenResty, Lua 기반 API 게이트웨이&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;- PR:&lt;/b&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/apache/apisix/pull/13607&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/apache/apisix/pull/13607&lt;/a&gt;&lt;/span&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 data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 사내 API 서비스 유료화, 이를 위한 ApiSix 구성과 액세스 로그 카프카 전달 기능 개발&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; - kafka-logger 플러그인이 TLS를 지원하지 않아 MSK 같은 TLS 강제 브로커에 연결 불가&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 작업:&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 하위 라이브러리(lua-resty-kafka)는 TLS 지원, 플러그인이 옵션 전달을 누락한 것이 원인&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - kafka-logger, error-log-logger 두 플러그인에 tls 스키마 추가, broker_config에 ssl/ssl_verify 전달&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 공용 CI 환경에 SSL Kafka 리스너 추가, 성공/실패 대비 통합 테스트 작성&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 보안 경고, 하위호환 검증, 문서 업데이트 포함&lt;b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&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;2404&quot; data-origin-height=&quot;1487&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvDZUL/dJMcadQfbM1/r6K4Z4HfpKWKu8cfoSkWY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvDZUL/dJMcadQfbM1/r6K4Z4HfpKWKu8cfoSkWY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvDZUL/dJMcadQfbM1/r6K4Z4HfpKWKu8cfoSkWY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvDZUL%2FdJMcadQfbM1%2Fr6K4Z4HfpKWKu8cfoSkWY0%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;2404&quot; height=&quot;1487&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2404&quot; data-origin-height=&quot;1487&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/1197</guid>
      <comments>https://ecsimsw.tistory.com/entry/APISIX-kafka-logger-TLS#entry1197comment</comments>
      <pubDate>Fri, 17 Jul 2026 12:42:24 +0900</pubDate>
    </item>
    <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;Parquet 형식&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;데이터는 기존 로그 형태가 아닌, Parquet 형식으로 저장하기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일반적인 RDBMS나 로그는 행 기반으로 데이터가 저장되어, 한 행의 모든 컬럼이 연속으로 디스크에 놓인다.&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;반면 사용하고자 하는 Parquet는 컬럼 기반의 데이터다.&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;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;필요한 컬럼만 스캔, 그조차도 압축률이 좋으니 조회 시 I/O가 줄어든다.&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;Parquet 파일은 내부에 메타데이터를 갖고 있다.&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;Predicate&amp;nbsp;Pushdown이라&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;이렇기에 집계를 위한 데이터로 Parquet 형식을 사용하고자 한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컬럼&amp;nbsp;기반으로&amp;nbsp;필요한&amp;nbsp;컬럼만&amp;nbsp;읽고,&amp;nbsp;압축률이&amp;nbsp;높아&amp;nbsp;I/O가&amp;nbsp;적고,&amp;nbsp;메타데이터로&amp;nbsp;파일&amp;nbsp;자체를&amp;nbsp;건너뛸&amp;nbsp;수&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;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;S3에 저장된 Parquet 파일의 조회는 AWS 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;&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. 적절한 파일 분리&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;한 파일로 저장할 사이즈를 보통 100 ~ 500MB로 묶어 관리하는 것을 권장한다.&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;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;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 style=&quot;text-align: start;&quot;&gt;이것을&amp;nbsp;&lt;/span&gt;&lt;b&gt;Hive&amp;nbsp;스타일&amp;nbsp;파티셔닝&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;이라고&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;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;2026&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yBJdP/dJMcajb5Nln/yWfpcZUvo5tpKFJk47Bkr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yBJdP/dJMcajb5Nln/yWfpcZUvo5tpKFJk47Bkr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yBJdP/dJMcajb5Nln/yWfpcZUvo5tpKFJk47Bkr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyBJdP%2FdJMcajb5Nln%2FyWfpcZUvo5tpKFJk47Bkr1%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;2026&quot; height=&quot;774&quot; data-origin-width=&quot;2026&quot; data-origin-height=&quot;774&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;</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, 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;b&gt;이벤트 파이프라인은 기기의 이벤트를 수신하고, 이를 연동된 플랫폼으로 전달하는 서비스&lt;/b&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;&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 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;앱 상 UI 변경만이 아닌,&amp;nbsp;&lt;/span&gt;&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 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;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 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;br /&gt;한 기기로부터의 이벤트는 단일 스레드를 할당하여 순차 처리, &lt;b&gt;서로 다른 기기의 이벤트는 서로 다른 스레드를 할당하여 병렬 처리&lt;/b&gt;하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Micrometer로 처리량, 대기열, 처리 속도를 수집했고, 이를 표시하는 스크립트와 대시보드를 구성했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;별도 테스트 토픽을 구성하여 실제 이벤트의 2~3배 부하를&amp;nbsp; 흘리며 워커 수를 조정했다.&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;그렇게 기기별 이벤트 순서는 보장하면서도, 초당 처리량을 기존 약 4,200개에서 18,000개까지 높일 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;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;이벤트 단건의 처리 시간은 늘었다&lt;/b&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;동시 처리를 늘린 만큼, &lt;b&gt;DB 액세스가 늘어 CP가 부족해졌고 커넥션 대기에 시간이 필요&lt;/b&gt;했던 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;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;&lt;span style=&quot;color: #000000;&quot;&gt;커넥션을 오래 잡고 있는 경우는 없을까 락과 놓친 인덱스를 점검했고, 처리 후 3ms 이상 걸리는 쿼리는 없도록 했다.&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;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 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 style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;조회 성능이 중요한 서비스이기 때문에, 캐시 업데이트를 위해 다른 캐시 처리가 받는 영향을 최소화하고 싶었다.&lt;/span&gt;&lt;br /&gt;실제 조회가 발생했을 때 캐싱하여,&amp;nbsp;&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;&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; 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; 11ms)&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 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;DB 커넥션 풀 고갈이나 외부 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;카프카 메시지 수신 스레드 풀과 별개로, 메시지 처리를 위한 워커 스레드 풀을 구성했다.&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;b&gt;스레드 풀의 대기열을 버퍼로 사용하여, 처리되지 못한 메시지를 담을 임시 공간&lt;/b&gt;을 만들었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-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 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 style=&quot;text-align: start;&quot;&gt;수신처에서 &lt;/span&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;미리 설정한 수로 사용할 리소스를 제한하고, 대기열 상태에 따라 다음 메시지 수신을 조절할 수 있는 시스템을 만들 수 있었다.&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;&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;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;이벤트 파이프라인은 전달에 집중한다.&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&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;br /&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;단순하지만, 그 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;또 장애 후 복구 시 관련 캐시를 전체 삭제하는 것도 방법이었지만, Cold Start에 영향이 큰 서비스라고 생각했다.&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; 짧은 장애는 TTL 만료에 맡기고, 일정 시간 이상의 지속에는 관련 캐시를 전체 삭제&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;br /&gt;&lt;b&gt;외부 API 장애는 서킷 브레이커로 대응한다.&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;앞서 테스트한 시간에 따라 장애 기준을 잡고, 장애로 판단되면 회로를 열어 외부 API 호출에 더 이상 시간을 쏟지 않는다.&amp;nbsp;&lt;br /&gt;&lt;br /&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;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;&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;기본 서버 상태 매트릭 외에도, 처리량, 외부 API&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;&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;특히 '해당 시간대엔 몇 건의 유실이 발생했고, 평균 Nms로 처리되었다.'를 보이는&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;&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; 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;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;- 버퍼, 백&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 데이터 관리 (TTL, Cold-start)&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>웹 소켓 서버 구조 개선 : 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;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;웹 소켓, SSE 같은 연결 유지가 필요한 서비스는 관리가 어렵다.&lt;/span&gt;&lt;b&gt;&lt;/b&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;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;&lt;span style=&quot;text-align: start;&quot;&gt;스케일 아웃이 자유롭지 않고, 배포 시에도&amp;nbsp;&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;&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 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 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;&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;연결 정보가 있는 경우에만 람다를 호출하는 것으로, 불필요한 레디스 부하를 크게 줄일 수 있었다.&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;br /&gt;Lambda도 같은 Redis를 다시 조회하여 connectionId를 찾고 @connections API를 호출한다.&lt;br /&gt;같은 Redis를 두 번 찌르는 꼴이었다.&lt;br /&gt;&lt;br /&gt;그렇다면 알림 서버에서 직접 @connections API를 호출하면 Lambda가 필요 없다.&lt;br /&gt;Lambda 비용도, cold start 걱정도, 람다를 관리하기 위한 관리 포인트도 사라진다.&lt;br /&gt;&lt;br /&gt;이에 Lambda를 제거하고, 알림 서버에서 Redis 조회와 push를 직접 처리하는 구조로 변경했다.&lt;br /&gt;$connect/$disconnect 이벤트도 API Gateway HTTP 통합으로 알림 서버가 직접 받는다.&lt;br /&gt;Lambda라는 관리 포인트가 하나 줄었고, 흐름이 단순해져 장애 시 추적할 서비스 홉도 줄었다.&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 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-filename=&quot;IMG_6143.PNG&quot; data-origin-width=&quot;2550&quot; data-origin-height=&quot;1312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sCpkF/dJMcaiqxauP/JhMNG31VeOhnASy2w3ZFe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sCpkF/dJMcaiqxauP/JhMNG31VeOhnASy2w3ZFe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sCpkF/dJMcaiqxauP/JhMNG31VeOhnASy2w3ZFe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsCpkF%2FdJMcaiqxauP%2FJhMNG31VeOhnASy2w3ZFe0%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;2550&quot; height=&quot;1312&quot; data-filename=&quot;IMG_6143.PNG&quot; data-origin-width=&quot;2550&quot; data-origin-height=&quot;1312&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;/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 로 구조를 변경하고는 높은 안정성에 관리 부담이 크게 줄었다.&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 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>프로젝트 경험 소개 : 플랫폼 키 관리 게이트웨이 개발</title>
      <link>https://ecsimsw.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B2%BD%ED%97%98-%EC%86%8C%EA%B0%9C-%EB%B3%B4%EC%95%88-%EB%A0%88%EC%9D%B4%EC%96%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0</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 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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;우리 팀도 경각심을 느껴 점검했고 &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;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;&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;위험 요소&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;우리 팀은 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;&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;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. 키&amp;nbsp;&lt;b&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;플랫폼 접근 키가 여러 환경에 분산되어 있었던 것을 큰 문제로 삼았다.&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 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;color: #000000; 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;b&gt;&amp;nbsp;동일&amp;nbsp;&lt;/b&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;서비스마다 필요한 플랫폼 기능이 다르다. 그럼에도&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;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; 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;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;background-color: #ffffff; color: #000000; text-align: start;&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;background-color: #ffffff; color: #000000; text-align: start;&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;&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;게이트웨이에선 요청을 검증하고, 플랫폼으로 전달하게 된다.&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 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;color: #000000; 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;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;키 관리 게이트웨이, 암복호화를 위한 서비스 외에는 플랫폼 키를 확인할 수 있는 IAM 권한이 없다.&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;게이트웨이가 사용하는 NAT IP만을 허용하여, 그 외에서는 어디서도 직접 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;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; 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;b&gt; &lt;/b&gt;&lt;b&gt;권한 분리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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 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;color: #000000; 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;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;레디스에 비허용 IP와 키를 관리하여 탈취당한 키나 토큰, 블랙리스트로부터의 추가 접근을 막을 수 있다.&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;4. 관리 주체 이전&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;&lt;span style=&quot;color: #000000;&quot;&gt;모든 서비스를 Lambda, Fargate 등 AWS에서 추상화한 완전 관리형 서비스를 사용한다.&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 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;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;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. 테스트&lt;/span&gt;&lt;/b&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;- 기존 요청, 응답 포맷과 동일하게 동작하는가 : 테스트 코드와 개발 환경에서의 QA로 확인했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 부하를 감당할 수 있는가 : 피크 타임의 요청 수를 가상 유저, N분간 동시 요청을 수행했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 중단은 없는가 : 적은 수의 가상 유저로 N시간 동안 꾸준히 요청을 반복했다.&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;&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 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;2912&quot; data-origin-height=&quot;1032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zg9Zr/dJMcagjL0mJ/RlykpQm5PDUioS1keueIG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zg9Zr/dJMcagjL0mJ/RlykpQm5PDUioS1keueIG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zg9Zr/dJMcagjL0mJ/RlykpQm5PDUioS1keueIG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzg9Zr%2FdJMcagjL0mJ%2FRlykpQm5PDUioS1keueIG1%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;288&quot; data-origin-width=&quot;2912&quot; data-origin-height=&quot;1032&quot;/&gt;&lt;/span&gt;&lt;/figure&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;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;그렇기 위해, 기존에 플랫폼을 직접 사용하던 모든 서비스의 API 호출 URL을 변경해야 했다.&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;Lambda와 Spring boot를 주로 사용하고 있고, 서비스 특성마다 빠른 전환을 위한 전략이 달랐다.&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;1. Lambda의 환경 변수 변경&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;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;Lambda가 트리거에 의해 매번 실행된다는 이유로 단순히 설정 서버의 값만 바꾸면 된다고 생각해선 안된다.&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;AWS Lambda에는 실행 컨텍스트 영역이 있다.&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;우리 팀은 플랫폼 url이나 키 값 등, 재사용하는 값을 실행 컨텍스트에 올려, Warm start 상황에서 마치 캐시처럼 사용한다.&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; 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;2. 스프링 부트의 RefreshScope&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;스프링 부트에선 @RefreshScope를 사용하면, 동적으로 빈을 재생성할 수 있다.&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;예를 들어 Config 서버에서 설정 값을 바꿔주고, Actuator 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;변경되는 설정 값을 플랫폼의 설정 값들로 하여, 배포 없이 플랫폼 엔드포인트를 변경할 수 있었다.&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;&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;3. Blue-Green&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 style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;@RefreshScope를 우선으로 검토했지만, 서비스 별로 사용할 수 없는 경우도 있었다.&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;서비스 중에는 동적 Config 서버를 사용하지 않는 경우도 있고, &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;이 경우 새로운 버전의 Service는 한 부 더 만들어두고, 전면에 LB에서 라우팅 규칙을 변경하는 꼴로 배포했다.&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;@RefreshScope 보다 확실하지만, 더 많은 &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;리소스가 필요했고 LB의 규칙을 변경해야 하는 관리가 필요했다.&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;대신 롤백은 설정 값을 다시 바꿔야 하는 @RefreshScope 보다 이미 이전 버전이 준비된 블루-그린이 더 빨랐다고 평가한다.&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;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;이슈 :&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; - 서비스 별로 동일 키를 갖고 있어, 권한 분리, 노출 시 접근 제한 불가&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;&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; - IP, 토큰 사용 제한&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; - 플랫폼 키 보관 중앙화, AWS&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;결과 :&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; - 플랫폼 접근 제한 강화 / 게이트웨이에서만 접근 가능하도록&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;&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/1156</guid>
      <comments>https://ecsimsw.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B2%BD%ED%97%98-%EC%86%8C%EA%B0%9C-%EB%B3%B4%EC%95%88-%EB%A0%88%EC%9D%B4%EC%96%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry1156comment</comments>
      <pubDate>Thu, 11 Dec 2025 18:43:51 +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&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;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 data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 스트리밍 요청&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 모바일 앱에서 &quot;카메라 보기&quot; 요청&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 모바일 앱 &amp;rarr; 플랫폼 &amp;rarr; 시그널링 서버로 요청 전달&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &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. STUN/TURN 서버 정보 전달&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 시그널링 서버 &amp;rarr; (HTTP) &amp;rarr; 모바일 앱: Turn, Stun 서버 정보 전달&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 시그널링 서버 &amp;rarr; (MQTT) &amp;rarr; 카메라: Turn, Stun 서버 정보 전달&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 양쪽이 이 정보로 RTCPeerConnection 생성&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; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &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;3. SDP 교환&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 모바일 앱 &amp;rarr; 시그널링 서버 &amp;rarr; (MQTT) &amp;rarr; 카메라 / SDP OFFER (코덱, 암호화 정보)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 카메라 &amp;rarr; (MQTT) &amp;rarr; 시그널링 서버 &amp;rarr; 모바일 앱 / SDP ANSER (코덱, 암호화 정보)&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. STUN으로 각자 외부 주소 확인&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 모바일 앱 &amp;rarr; STUN &amp;rarr; IP 주소 확인 &quot;49.x.x.38:58829&quot;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 카메라&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr; STUN &amp;rarr; IP 주소 확인 &quot;211.x.x.123:45930&quot;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &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;5. ICE Candidate 교환&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 양쪽이 STUN으로 알아낸 외부 주소를 포함한 후보군을 교환&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - host: 내부 IP (같은 네트워크면 바로 연결)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - srflx: STUN으로 알아낸 외부 IP:PORT (홀펀칭용)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - relay: TURN 서버 주소 (fallback)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &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;6. 연결 시도 (ICE)&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;- host &amp;rarr; srflx(홀펀칭) &amp;rarr; relay(TURN) 순&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;7. 스트리밍&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 연결된 경로로 미디어 데이터 전송 (DTLS-SRTP 암호화)&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 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>
  </channel>
</rss>