<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>왈왈이의 기술블로그</title>
    <link>https://walwaldev.tistory.com/</link>
    <description>Backend, Fraud &amp;amp; Abuse Prevention</description>
    <language>ko</language>
    <pubDate>Wed, 27 May 2026 18:14:17 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>왈왈디</managingEditor>
    <image>
      <title>왈왈이의 기술블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/6223454/attach/92059eaca959429abe794c1dde72dc19</url>
      <link>https://walwaldev.tistory.com</link>
    </image>
    <item>
      <title>gRPC 알아보기</title>
      <link>https://walwaldev.tistory.com/189</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;gRPC란 무엇인가&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 개발을 하다 보면 결국 같은 문제에 부딪힌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;서버끼리 어떻게 대화할까?&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 &amp;rarr; 서버&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 &amp;rarr; 다른 서버&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 &amp;rarr; 분석 시스템&lt;/p&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&gt;&lt;b&gt;REST API&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;하지만 규모가 커지면 REST가 조금 버거워지는 순간이 온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 자주 등장하는 기술이 &lt;span&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;먼저 REST부터 생각해보자&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 서비스는 이런 식으로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 서버에 요청을 보낸다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;POST /login
{
  &quot;email&quot;: &quot;user@email.com&quot;,
  &quot;password&quot;: &quot;1234&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 JSON을 읽고 처리한 뒤 응답을 보낸다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;success&quot;: true,
  &quot;user_id&quot;: 1234
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 특징은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 사용&lt;/li&gt;
&lt;li&gt;JSON 데이터&lt;/li&gt;
&lt;li&gt;URL 기반 API&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 바로 &lt;span&gt;&lt;b&gt;REST API&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 서비스가 커지면 문제가 하나 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서버끼리 통신이 많아진다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 이런 구조다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;API 서버
  ├ 유저 서비스
  ├ 결제 서비스
  ├ 포인트 서비스
  └ 어뷰징 탐지 서비스&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모든 서버가 서로 REST API로 통신하면 다음 문제가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JSON이 커서 느림&lt;/li&gt;
&lt;li&gt;HTTP 요청 비용 증가&lt;/li&gt;
&lt;li&gt;API 관리 복잡&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 등장한 것이 &lt;span&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;gRPC를 한 문장으로 설명하면&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC는&lt;/p&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;ldquo;다른 서버의 함수를 직접 호출하는 것처럼 통신하는 기술&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST는 이런 느낌이다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;URL 요청 &amp;rarr; 서버 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC는 이런 느낌이다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;함수 호출 &amp;rarr; 서버 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 서버에 이런 기능이 있다고 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Fraud 검사&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST 방식이라면 이렇게 호출한다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;POST /fraud/check
{
  &quot;user_id&quot;: 123,
  &quot;ip&quot;: &quot;1.2.3.4&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC에서는 이렇게 된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;FraudService.Check(user_id, ip)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;마치 &lt;/span&gt;&lt;b&gt;다른 서버의 함수를 호출하는 느낌&lt;/b&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;그래서 이름도 RPC (Remote Procedure Call)다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Remote = 원격&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Procedure Call = 함수 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원격 서버의 함수를 호출하는 방식&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;gRPC가 빠른 이유&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에는 기술적인 이유가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. JSON 대신 Protobuf 사용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST는 보통 JSON을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;user_id&quot;: 123,
  &quot;ip&quot;: &quot;1.2.3.4&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;gRPC는 &lt;/span&gt;&lt;b&gt;Protocol Buffers (protobuf)&lt;/b&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&gt;이건 &lt;/span&gt;&lt;b&gt;바이너리 데이터&lt;/b&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 크기가 작다&lt;/li&gt;
&lt;li&gt;파싱 속도가 빠르다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 &lt;/span&gt;&lt;b&gt;네트워크 비용이 줄어든다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. HTTP/2 사용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST는 보통 &lt;span&gt;&lt;b&gt;HTTP/1.1&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;gRPC는 &lt;span&gt;&lt;b&gt;HTTP/2&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;HTTP/2는 다음 기능이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 연결에서 여러 요청 처리&lt;/li&gt;
&lt;li&gt;헤더 압축&lt;/li&gt;
&lt;li&gt;스트리밍&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 &lt;/span&gt;&lt;b&gt;대량 요청 처리에 유리하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;gRPC는 어떻게 사용하는가&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC는 먼저 &lt;span&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&gt;&lt;b&gt;proto 파일&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;/p&gt;
&lt;pre class=&quot;protobuf&quot;&gt;&lt;code&gt;service FraudService {
  rpc CheckFraud (FraudRequest) returns (FraudResponse);
}

message FraudRequest {
  string user_id = 1;
  string ip = 2;
}

message FraudResponse {
  int32 risk_score = 1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 보면 구조가 보인다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;FraudService
   └ CheckFraud()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CheckFraud라는 함수를 원격 서버에서 실행한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 놀라운 부분이 하나 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 proto 파일을 기반으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 코드&lt;/li&gt;
&lt;li&gt;클라이언트 코드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 &lt;/span&gt;&lt;b&gt;자동 생성한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 개발자는 이렇게 사용한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;fraudClient.CheckFraud(request)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 내부적으로 gRPC 요청이 날아간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;gRPC는 언제 사용하는가&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC는 주로 &lt;span&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&gt;대표적인 예는 &lt;/span&gt;&lt;b&gt;마이크로서비스 구조&lt;/b&gt;&lt;span&gt;다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;API Server
   ├ User Service
   ├ Payment Service
   ├ Point Service
   └ Fraud Service&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 서비스들이 서로 통신해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 gRPC를 사용하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빠르고&lt;/li&gt;
&lt;li&gt;타입 안전하고&lt;/li&gt;
&lt;li&gt;관리가 쉽다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대형 서비스들이 많이 사용한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Google&lt;/li&gt;
&lt;li&gt;Netflix&lt;/li&gt;
&lt;li&gt;Square&lt;/li&gt;
&lt;li&gt;Cloudflare&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 회사들이 내부 통신에 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;어뷰징 방지 시스템에서 gRPC&lt;/b&gt;&lt;/h1&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&gt;어뷰징 탐지는 &lt;/span&gt;&lt;b&gt;속도가 중요하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 이런 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 포인트를 적립한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;유저 행동
   &amp;darr;
API 서버
   &amp;darr;
어뷰징 검사
   &amp;darr;
허용 / 차단&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정이 느리면 서비스가 느려진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 보통 어뷰징 로직을 &lt;span&gt;&lt;b&gt;별도 서비스&lt;/b&gt;&lt;/span&gt;로 분리한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;API Server
   &amp;darr;
Fraud Detection Service&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 gRPC를 사용하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;응답 속도 빠름&lt;/li&gt;
&lt;li&gt;많은 요청 처리 가능&lt;/li&gt;
&lt;li&gt;서비스 구조 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 이런 작업에 잘 맞는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Risk score 계산&lt;/li&gt;
&lt;li&gt;IP 평판 검사&lt;/li&gt;
&lt;li&gt;디바이스 검사&lt;/li&gt;
&lt;li&gt;이메일 위험도 검사&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC를 한 문장으로 다시 정리하면 이렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;서버끼리 빠르게 함수 호출 방식으로 통신하는 기술&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST와 비교하면&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;REST&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;gRPC&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;데이터&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;JSON&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Protobuf&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;속도&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;보통&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;빠름&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;통신 방식&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;URL&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;함수 호출&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;주 사용처&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;외부 API&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;내부 서비스&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 대부분의 시스템은 이렇게 구성된다.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;외부 API &amp;rarr; REST
내부 서비스 &amp;rarr; gRPC&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;외부에는 REST, 내부에는 gRPC&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조가 가장 흔하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흥미로운 사실 하나.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google 내부에서는 오래전부터 &lt;span&gt;&lt;b&gt;RPC 기반 시스템&lt;/b&gt;&lt;/span&gt;을 사용해왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC는 그 연구의 공개 버전이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 시스템에서는 결국 같은 결론에 도달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;네트워크가 느리다.&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 사람들은 계속해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;더 빠른 통신 방법&lt;/b&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;gRPC도 그 진화의 한 단계다.&lt;/p&gt;</description>
      <category>컴퓨터 공학 &amp;amp; 통신</category>
      <author>왈왈디</author>
      <guid isPermaLink="true">https://walwaldev.tistory.com/189</guid>
      <comments>https://walwaldev.tistory.com/189#entry189comment</comments>
      <pubDate>Mon, 16 Mar 2026 01:27:57 +0900</pubDate>
    </item>
    <item>
      <title>K집안 기술 탐구 - Kafka, Kubernetes, Kinesis</title>
      <link>https://walwaldev.tistory.com/186</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;K로 시작한다는 점 외에는 공통점이 없는 세 기술.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka&lt;/li&gt;
&lt;li&gt;Kubernetes&lt;/li&gt;
&lt;li&gt;Kinesis&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 사용해본 적이 없고, K로 시작하는 이름때문에 자꾸 헷갈려서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 기술들의&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;정의,&lt;span&gt; &lt;/span&gt;&lt;/b&gt;&lt;b&gt;운영 주체, 등장 배경, 역할, 사용 사례, 유사 경쟁 기술&lt;/b&gt;을 정리해보려고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Apache Kafka&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1346&quot; data-origin-height=&quot;1613&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDBV9W/dJMcagEcutk/V0lQNjufszrrA4PiH6dTeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDBV9W/dJMcagEcutk/V0lQNjufszrrA4PiH6dTeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDBV9W/dJMcagEcutk/V0lQNjufszrrA4PiH6dTeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDBV9W%2FdJMcagEcutk%2FV0lQNjufszrrA4PiH6dTeK%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;450&quot; height=&quot;539&quot; data-origin-width=&quot;1346&quot; data-origin-height=&quot;1613&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 분산 이벤트 스트리밍 플랫폼이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대량의 이벤트를 순서대로, 오래, 안전하게 저장하고 전달하는 로그 시스템이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka의 핵심 아이디어는 &quot;시스템에서 일어난 모든 일을 로그로 남기자.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 로그를 여러 소비자가 각자 필요에 맞게 읽는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;운영 주체&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈 소스 프로젝트로, Apache Software Foundation이 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Confluent 등 다수의 기업이 상용 배포와 운영 지원을 하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;등장 배경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 LinkedIn 내부 문제에서 출발했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2010년대 초반, Linked In은 아래와 같은 문제에 직면했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 곳곳에서 이벤트 로그가 폭발적으로 발생&lt;/li&gt;
&lt;li&gt;각 팀이 각자 로그 파이프라인을 만들어 관리&lt;/li&gt;
&lt;li&gt;데이터 유실, 순서 꼬임, 재처리 불가능 문제 반복&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 모든 &quot;이벤트를 하나의 신뢰 가능한 로그로 모으자&quot;는 니즈가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 따라 Kafka는 처음부터&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 큐가 아니라, 분산 커밋 로그(distributed commit log)로 설계되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 지금 kafka가 &quot;재처리 기능&quot;, &quot;순서 보장&quot;에 집착하는 이유다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka의 역할은&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이벤트를 append-only 로그로 저장&lt;/li&gt;
&lt;li&gt;이벤트 순서 보장&lt;/li&gt;
&lt;li&gt;여러 consumer가 독립적으로 읽을 수 있게 제공&lt;/li&gt;
&lt;li&gt;장애 발생 시 데이터 유실 없이 복구 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 &lt;b&gt;처리&lt;/b&gt;를 하는 것이 아니라, &lt;b&gt;기록&lt;/b&gt;하고 &lt;b&gt;전달&lt;/b&gt;하는 역할이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용 사례&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 백엔드와 데이터팀의 경계면에 자주 위치된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클릭, 노출, 결제 포인트 적립 등의 이벤트 로그 수집&lt;/li&gt;
&lt;li&gt;하나의 이벤트를 아래 서비스들이 각각 다르게 소비해야 할 때
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정산 시스템&lt;/li&gt;
&lt;li&gt;어뷰징 방지&lt;/li&gt;
&lt;li&gt;통계 집계&lt;/li&gt;
&lt;li&gt;실시간 알림&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;재처리(replay)가 필요한 데이터 파이프라인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;유사 경쟁 기술&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RabbitMQ&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 메시지 브로커로 작업 큐에 강점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Apache Pulsar&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 대체를 목표로 하는 스트리밍 플랫폼이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Amazon MSK&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka를 AWS가 관리해주는 형태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 지금도 가장 널리 쓰이는 이벤트 로그 표준에 가깝다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kubernetes&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2704&quot; data-origin-height=&quot;1298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7f2Jk/dJMcabpkqJo/XEVAFcO05rj4B9h351rDqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7f2Jk/dJMcabpkqJo/XEVAFcO05rj4B9h351rDqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7f2Jk/dJMcabpkqJo/XEVAFcO05rj4B9h351rDqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7f2Jk%2FdJMcabpkqJo%2FXEVAFcO05rj4B9h351rDqK%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;546&quot; height=&quot;262&quot; data-origin-width=&quot;2704&quot; data-origin-height=&quot;1298&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;864&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/01Gtp/dJMcab3Whfu/VIwmaHZNO2LHSgWvqPFOek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/01Gtp/dJMcab3Whfu/VIwmaHZNO2LHSgWvqPFOek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/01Gtp/dJMcab3Whfu/VIwmaHZNO2LHSgWvqPFOek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F01Gtp%2FdJMcab3Whfu%2FVIwmaHZNO2LHSgWvqPFOek%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;510&quot; height=&quot;437&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;864&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubernetes는 컨테이너 기반 애플리케이션 오케스트레이션 시스템이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너를 대규모로 안정적으로 운영하기 위한 플랫폼이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Kubernetes로 관리할 수 있는 것은 &quot;이 애플리케이션을 언제, 어디서, 몇 개로, 어떻게 살려둘 것인가&quot; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;운영 주체&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈 소스 프로젝트로 처음 만든 회사는 Google이지만, 현재는 CNCF(Cloud Native Computing Foundation)가 공식 관리 주체이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CNCF는 리눅스 재단 산하의 중립적인 오픈소스 재단으로, 특정 기업이 마음대로 끌고 갈 수 없게 하는 장치다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;등장 배경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google 내부에서 10년 이상 검증된 운영 철학의 공개판이라고 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google은 오래전부터 Borg, Omega 라는 내부 컨테이너 관리 시스템을 운영하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시스템들이 해결한 문제는 &lt;b&gt;수십만 개의 프로세스를 사람이 관리할 수 없다&lt;/b&gt;는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes는 Borg의 개념을 정리해&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너&lt;/li&gt;
&lt;li&gt;선언형 상태 관리&lt;/li&gt;
&lt;li&gt;자동 복구&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 형태로 외부에 공개한 결과물이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Kubernetes는 처음부터 개발 도구가 아니라 운영 도구였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes의 관심사는 &lt;b&gt;운영&lt;/b&gt;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너 실행 및 종료&lt;/li&gt;
&lt;li&gt;장애 발생 시 자동 재기동&lt;/li&gt;
&lt;li&gt;트래픽 분산&lt;/li&gt;
&lt;li&gt;오토스케일링&lt;/li&gt;
&lt;li&gt;무중단 배포&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes는 애플리케이션 내부 로직을 전혀 모른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 서버든, 배치 작업이든, Kafka Consumer든 &lt;b&gt;그냥 컨테이너&lt;/b&gt;로 취급한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용 사례&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes가 필수인 팀은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마이크로서비스 구조&lt;/li&gt;
&lt;li&gt;트래픽 변동이 큰 서비스&lt;/li&gt;
&lt;li&gt;수십~수백 개의 서버를 사람 손으로 관리하기 어려운 환경&lt;/li&gt;
&lt;li&gt;배포 안정성이 중요한 서비스&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes는 개발 속도보다 운영 안정성을 극적으로 높이는 도구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;유사 경쟁 기술&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Docker Swarm&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 자체 오케스트레이션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Apache Mesos&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;범용 클러스터 리소스 관리자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Nomad(HashiCorp)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순한 워크로드 오케스트레이터다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 사실상 Kubernetes가 표준이 되었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Amazon Kinesis&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1260&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xLB5R/dJMcabJEv65/O1InAVYICZruggtCJ5tMjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xLB5R/dJMcabJEv65/O1InAVYICZruggtCJ5tMjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xLB5R/dJMcabJEv65/O1InAVYICZruggtCJ5tMjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxLB5R%2FdJMcabJEv65%2FO1InAVYICZruggtCJ5tMjK%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;701&quot; height=&quot;265&quot; data-origin-width=&quot;1260&quot; data-origin-height=&quot;476&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kinesis는 AWS에서 제공하는 &lt;b&gt;완전관리형 실시간 데이터 스트리밍 서비스&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka와 같은 문제를 풀지만, 운영을 AWS가 대신한다는 점이 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 띄우지 않고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터를 관리하지 않아도,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 스트림을 쓸 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;운영 주체&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Amazon Web Services에서 운영하는 완전 관리형 서비스(Closed source)다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;등장 배경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS의 문제 의식은&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;고객들이 로그를 실시간으로 모으고 싶어한다.&lt;/li&gt;
&lt;li&gt;Kafka의 운영 난이도가 높다.&lt;/li&gt;
&lt;li&gt;AWS 서비스와 자연스럽게 연결되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래와 같이 설계되었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 관리 없음&lt;/li&gt;
&lt;li&gt;AWS IAM, S3, Redshift와 즉시 연동&lt;/li&gt;
&lt;li&gt;운영 세부 사항은 사용자에게 숨김&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Kinesis는 스트리밍을 쓰고 싶지만 스트리밍 시스템을 운영하고 싶진 않은 사용자를 위해 탄생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대량 이벤트 스트림 수집&lt;/li&gt;
&lt;li&gt;실시간 처리 또는 저장소로 전달&lt;/li&gt;
&lt;li&gt;AWS 서비스와 즉시 연동&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kinesis는 내부적으로 여러 컴포넌트로 나뉜다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Data Streams: 실시간 스트림 처리&lt;/li&gt;
&lt;li&gt;Firehose: S3, Redshift 등으로 자동 적재&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용 사례&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인프라가 100% AWS인 경우&lt;/li&gt;
&lt;li&gt;운영 인력이 제한적&lt;/li&gt;
&lt;li&gt;로그를 바로 S3 혹은 Redshift로 쌓고 싶은 경우&lt;/li&gt;
&lt;li&gt;빠른 구축이 중요한 프로젝트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kinesis는 &lt;b&gt;유연성보다 관리 편의성에 최적화&lt;/b&gt;된 선택이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;유사 경쟁 기술&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Google Pub/Sub&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GCP의 완전관리형 메시징/스트리밍 서비스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Azure Event Hubs&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Azure의 스트리밍 플랫폼이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Apache Kafka&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자유도가 높고 책임도 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Kinesis는 &lt;/span&gt;&lt;b&gt;AWS 생태계 최적화형 선택지&lt;/b&gt;&lt;span&gt;다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;</description>
      <author>왈왈디</author>
      <guid isPermaLink="true">https://walwaldev.tistory.com/186</guid>
      <comments>https://walwaldev.tistory.com/186#entry186comment</comments>
      <pubDate>Sun, 4 Jan 2026 22:48:05 +0900</pubDate>
    </item>
    <item>
      <title>IP주소로 알 수 있는 정보</title>
      <link>https://walwaldev.tistory.com/185</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqrgQM/dJMcahptFNZ/eWGgJUZ1v5uedqRfx84DD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqrgQM/dJMcahptFNZ/eWGgJUZ1v5uedqRfx84DD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqrgQM/dJMcahptFNZ/eWGgJUZ1v5uedqRfx84DD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqrgQM%2FdJMcahptFNZ%2FeWGgJUZ1v5uedqRfx84DD0%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;549&quot; height=&quot;302&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP 주소는 인터넷 세계의 &amp;ldquo;발신 흔적&amp;rdquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 흔적이라고 해서 지문처럼 정확하다고 생각하면 곤란하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어뷰징 방지 관점에서 IP는 &lt;span&gt;&lt;b&gt;강력한 단서이면서도 단독 증거로는 항상 부족한 신호&lt;/b&gt;&lt;/span&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP 주소로 무엇을 알 수 있고, 무엇은 알 수 없는지를 정리해보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;IP주소로 알수 있는 정보와 알 수 없는 정보&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3076&quot; data-origin-height=&quot;1814&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfVfoO/dJMb99ZgHlS/BAAUAKC1AeImbfvpOvz650/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfVfoO/dJMb99ZgHlS/BAAUAKC1AeImbfvpOvz650/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfVfoO/dJMb99ZgHlS/BAAUAKC1AeImbfvpOvz650/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfVfoO%2FdJMb99ZgHlS%2FBAAUAKC1AeImbfvpOvz650%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;567&quot; height=&quot;334&quot; data-origin-width=&quot;3076&quot; data-origin-height=&quot;1814&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IP 주소로&lt;span&gt; &lt;/span&gt;확실히 알 수 있는 것&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;국가&amp;middot;대륙 단위의 위치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP 주소는 대체로 &lt;span&gt;&lt;b&gt;국가 수준의 위치 정보&lt;/b&gt;&lt;/span&gt;를 제공한다. MaxMind, IP2Location 같은 상용 DB 기준으로 보면 국가 정확도는 꽤 높은 편이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 &amp;ldquo;서울/부산&amp;rdquo; 같은 도시 단위는 통계적 추정일 뿐이며, 모바일 환경에서는 오차가 급격히 커진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어뷰징 방지에서는 이 정보를 이렇게 쓴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 허용 국가 / 차단 국가 판단&lt;/li&gt;
&lt;li&gt;비정상 트래픽 국가 급증 감지&lt;/li&gt;
&lt;li&gt;계정 생성 국가와 이후 행동 국가 불일치 탐지&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;네트워크 유형 (모바일 / 고정 / 데이터센터)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP를 보면 대략적인 네트워크 성격은 파악 가능하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;통신사 LTE/5G&lt;/li&gt;
&lt;li&gt;가정용 ISP&lt;/li&gt;
&lt;li&gt;클라우드&amp;middot;데이터센터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어뷰징 방지에서 이건 꽤 쓸모 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &amp;ldquo;모바일 앱인데 데이터센터 IP에서 대량 요청&amp;rdquo;은 그 자체로 이상 징후다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 모바일 IP라고 해서 무조건 안전하다고 믿는 것도 위험하다. 프록시&amp;middot;테더링&amp;middot;에뮬레이터는 항상 이 경계를 흐린다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ASN (Autonomous System Number)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ASN은 IP가 &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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 이런 패턴을 자주 본다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 ASN에서만 반복적으로 어뷰징 발생&lt;/li&gt;
&lt;li&gt;국가가 달라도 동일 ASN 클러스터 형성&lt;/li&gt;
&lt;li&gt;신규 ASN 등장 후 비정상 트래픽 급증&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 IP 문자열 자체보다 &lt;span&gt;&lt;b&gt;ASN 단위 집계&lt;/b&gt;&lt;/span&gt;가 더 의미 있는 경우도 많다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IP 주소로&lt;span&gt;&amp;nbsp;&lt;/span&gt;추정할 수는 있지만 위험한 것&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실제 사용자 위치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP 위치 = 사용자 위치는 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 모바일 환경에서는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기지국 위치&lt;/li&gt;
&lt;li&gt;통신사 NAT&lt;/li&gt;
&lt;li&gt;트래픽 라우팅 경로&lt;/li&gt;
&lt;li&gt;이런 요소 때문에 물리적 위치와 쉽게 어긋난다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어뷰징 분석에서 &amp;ldquo;IP가 한국이니까 한국 사용자&amp;rdquo;라는 결론은 &lt;span&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 IP = 같은 사람&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 공식은 거의 항상 틀린다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회사&amp;middot;학교&amp;middot;카페 Wi-Fi&lt;/li&gt;
&lt;li&gt;통신사 NAT&lt;/li&gt;
&lt;li&gt;가족 단위 네트워크&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로, &lt;span&gt;&lt;b&gt;같은 사람인데 IP는 계속 바뀌는 경우&lt;/b&gt;&lt;/span&gt;가 모바일에서는 훨씬 흔하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 IP는 &amp;ldquo;동일인 판별&amp;rdquo;이 아니라 &lt;span&gt;&lt;b&gt;동시성&amp;middot;군집성 신호&lt;/b&gt;&lt;/span&gt;로 쓰는 게 맞다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IP 주소로&lt;span&gt;&amp;nbsp;&lt;/span&gt;알 수 없는 것&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;개인 신원&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름, 전화번호, 정확한 주소는 IP만으로 알 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP는 개인 식별 정보라기보다 &lt;span&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;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;디바이스 고유성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP는 디바이스를 식별하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디바이스 단위 어뷰징 탐지는 IP가 아니라&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디바이스 지문&lt;/li&gt;
&lt;li&gt;OS/앱 무결성 신호&lt;/li&gt;
&lt;li&gt;계정 행동 패턴&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 것들의 조합 문제다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;어뷰징 방지에서 IP를&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;사용하는 방식&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;IP는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;1차 필터가 아니라 보조 신호&lt;/b&gt;&lt;span&gt;다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;단일 IP 차단은 효과보다 부작용이 크다&lt;/li&gt;
&lt;li&gt;국가/ASN/네트워크 유형 단위의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;집계와 패턴&lt;/b&gt;&lt;/span&gt;이 핵심이다&lt;/li&gt;
&lt;li&gt;계정, 디바이스, 행동 로그와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;항상 결합&lt;/b&gt;&lt;/span&gt;해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;IP만 보고 &amp;ldquo;이건 어뷰저다&amp;rdquo;라고 말하는 순간, 정상 유저도 함께 맞는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반대로 IP를 완전히 무시하면, 값싼 대량 공격을 초기에 걸러낼 기회를 잃는다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VPN 사용 여부 판별법&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1290&quot; data-origin-height=&quot;934&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yJxiX/dJMcafFbV9d/g9AXoY8yOKc0lyWhYfMlo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yJxiX/dJMcafFbV9d/g9AXoY8yOKc0lyWhYfMlo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yJxiX/dJMcafFbV9d/g9AXoY8yOKc0lyWhYfMlo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyJxiX%2FdJMcafFbV9d%2Fg9AXoY8yOKc0lyWhYfMlo0%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;468&quot; height=&quot;339&quot; data-origin-width=&quot;1290&quot; data-origin-height=&quot;934&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;vpnapi.io 같은 서비스는 IP를 보고 vpn 사용 여부를 어떻게 판별할까.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IP 하나만 보고 &amp;lsquo;확정&amp;rsquo;하는 게 아니라, 여러 간접 신호를 겹쳐서 확률적으로 판단&lt;/b&gt;&lt;span&gt;한다. IP는 단서 묶음의 출발점일 뿐이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 실제로 쓰이는 방법들이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ASN&amp;middot;네트워크 소속 분석 (가장 기본)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;IP는 항상 특정 &lt;/span&gt;&lt;b&gt;ASN(Autonomous System)&lt;/b&gt;&lt;span&gt; 에 속한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 VPN 사업자들이 &lt;span&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;이 ASN은 역사적으로 VPN/Proxy 트래픽이 90% 이상&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;데이터센터 전용 ASN인데 모바일 앱 요청이 계속 옴&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&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;vpnapi.io 같은 서비스는 &amp;ldquo;이 ASN은 VPN 성향&amp;rdquo;이라는 &lt;span&gt;&lt;b&gt;사전 확률(prior)&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;rarr; &lt;/span&gt;&lt;b&gt;IP 하나가 아니라 ASN 전체가 신호&lt;/b&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;h3 data-ke-size=&quot;size23&quot;&gt;데이터센터 IP 여부 (Residential vs DC)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPN 서버 대부분은:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS / GCP / Azure&lt;/li&gt;
&lt;li&gt;Hetzner, OVH, DigitalOcean&lt;/li&gt;
&lt;li&gt;&lt;span&gt;같은 &lt;/span&gt;&lt;b&gt;데이터센터 IP 대역&lt;/b&gt;&lt;span&gt;에 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이런 판단이 가능해진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모바일 앱인데 &amp;rarr; 데이터센터 IP&lt;/li&gt;
&lt;li&gt;개인 유저 행동인데 &amp;rarr; 서버형 네트워크&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 오탐도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사 Wi-Fi, 학교, 일부 ISP는 DC처럼 보이기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 &lt;/span&gt;&lt;b&gt;단독 판단은 안 쓰고 점수로만 반영&lt;/b&gt;&lt;span&gt;한다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;IP 재사용 패턴 (강력한 신호)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPN IP의 특징:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;짧은 시간에 수십~수백 명이 공유&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;국가&amp;middot;디바이스&amp;middot;OS가 뒤섞임&lt;/li&gt;
&lt;li&gt;계정 생성/로그인 이벤트가 몰림&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vpnapi.io는 이런 패턴을 본다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동일 IP에서 서로 다른 국가 계정 생성&lt;/li&gt;
&lt;li&gt;동일 IP에서 User-Agent 다양성 폭증&lt;/li&gt;
&lt;li&gt;세션 길이가 비정상적으로 짧거나 균일&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 &amp;ldquo;IP가 VPN 같다&amp;rdquo;가 아니라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;이 IP는 사람이 쓰는 방식이 아니다&amp;rdquo;라는 판단이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;포트&amp;middot;라우팅&amp;middot;네트워크 지문&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 네트워크 쪽으로 들어가면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 포트 패턴 (OpenVPN / WireGuard 계열)&lt;/li&gt;
&lt;li&gt;RTT(왕복 지연) 분포&lt;/li&gt;
&lt;li&gt;TTL(Time To Live) 특성&lt;/li&gt;
&lt;li&gt;라우팅 경로의 비정상적인 단순화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 단일 서비스가 직접 측정한다기보다,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여러 소스에서 수집된 네트워크 관측 데이터&lt;/b&gt;&lt;span&gt;를 결합한 결과다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상호 참조 데이터 (Crowd Intelligence)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vpnapi.io 같은 서비스는 보통 &lt;span&gt;&lt;b&gt;단독으로 판단하지 않는다&lt;/b&gt;&lt;/span&gt;.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 보안 업체&lt;/li&gt;
&lt;li&gt;봇 방어 서비스&lt;/li&gt;
&lt;li&gt;광고 트래픽 필터&lt;/li&gt;
&lt;li&gt;과거 고객들의 차단 결과&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;우리 고객 300곳 중 180곳에서 이 IP를 VPN으로 봤다&amp;rdquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 같은 집단 지성이 작동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 기술이라기보다 &lt;span&gt;&lt;b&gt;통계와 네트워크 효과&lt;/b&gt;&lt;/span&gt;다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 &amp;ldquo;100% 정확&amp;rdquo;하지 않은지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조상 한계가 명확하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;통신사 NAT + 해외 로밍 &amp;rarr; VPN처럼 보임&lt;/li&gt;
&lt;li&gt;회사 VPN &amp;rarr; 개인 VPN과 구분 어려움&lt;/li&gt;
&lt;li&gt;신규 VPN IP &amp;rarr; 초반엔 탐지 안 됨&lt;/li&gt;
&lt;li&gt;주거용 프록시(residential proxy) &amp;rarr; 거의 정상처럼 보임&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 &lt;/span&gt;&lt;b&gt;판결문이 아니라 참고 자료&lt;/b&gt;&lt;span&gt;다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어뷰징 방지 관점에서의 정리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;VPN 여부는 &lt;/span&gt;&lt;b&gt;차단 조건이 아니라 가중치&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;IP 기반 VPN 판별은 &lt;span&gt;&lt;b&gt;단독 사용 금지&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;디바이스 무결성, 계정 히스토리, 행동 패턴과 결합 필수&lt;/li&gt;
&lt;li&gt;&amp;ldquo;VPN 쓴다 = 어뷰저&amp;rdquo;는 거의 항상 잘못된 명제&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ASN 더 알아보기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2102&quot; data-origin-height=&quot;1484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djGE7S/dJMcaiWakvv/pxEr2oBdaYPzWK7r8VOZR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djGE7S/dJMcaiWakvv/pxEr2oBdaYPzWK7r8VOZR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djGE7S/dJMcaiWakvv/pxEr2oBdaYPzWK7r8VOZR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjGE7S%2FdJMcaiWakvv%2FpxEr2oBdaYPzWK7r8VOZR0%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;537&quot; height=&quot;379&quot; data-origin-width=&quot;2102&quot; data-origin-height=&quot;1484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ASN이란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ASN(Autonomous System Number)&lt;/b&gt;&lt;span&gt; 은&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;이 IP 대역을 내가 운영한다&amp;rdquo;고 인터넷 전체에 선언한 네트워크 사업자의 고유 번호다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통신사, 클라우드 사업자, 대형 기업 네트워크는 각자 ASN을 갖고 있고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;IP는 항상 &lt;/span&gt;&lt;b&gt;어떤 ASN 소속인지가 정해져 있다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그걸 누가, 어떻게 정하나&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 중앙집권적 데이터가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;인터넷은 &lt;/span&gt;&lt;b&gt;BGP(Border Gateway Protocol)&lt;/b&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;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 사업자가 IP 대역을 확보&lt;/li&gt;
&lt;li&gt;&amp;ldquo;이 IP들은 우리 ASN이 라우팅한다&amp;rdquo;라고 BGP로 전 세계에 광고&lt;/li&gt;
&lt;li&gt;다른 네트워크들이 그 광고를 받아 라우팅 테이블에 반영&lt;/li&gt;
&lt;li&gt;결과적으로&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IP &amp;rarr; ASN 매핑이 전 세계적으로 합의됨&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 ASN 정보는:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실시간에 가깝고&lt;/li&gt;
&lt;li&gt;위조가 거의 불가능하며&lt;/li&gt;
&lt;li&gt;VPN 업체도 마음대로 숨기기 어렵다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IP로 ASN을 알아내는 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술적으로는 매우 단순하다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;IP를 입력한다&lt;/li&gt;
&lt;li&gt;BGP 라우팅 테이블을 조회한다&lt;/li&gt;
&lt;li&gt;그 IP를 광고한 ASN을 반환한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 데이터는:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;글로벌 라우터&lt;/li&gt;
&lt;li&gt;RIR(지역 인터넷 등록 기관)&lt;/li&gt;
&lt;li&gt;BGP 수집 프로젝트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 곳에서 공개적으로 수집된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vpnapi.io 같은 서비스는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;IP &amp;rarr; ASN&amp;rdquo;을 직접 계산하거나, 신뢰 가능한 BGP 스냅샷 DB를 사용&lt;/b&gt;&lt;span&gt;한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ASN으로 구별 가능한 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터 어뷰징 방지에 의미가 생긴다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;통신사 vs 데이터센터&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모바일/가정용 ISP ASN&lt;/li&gt;
&lt;li&gt;&amp;rarr; 사용자 네트워크 성격&lt;/li&gt;
&lt;li&gt;클라우드&amp;middot;IDC ASN&lt;/li&gt;
&lt;li&gt;&amp;rarr; 서버 네트워크 성격&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 IP 문자열보다 훨씬 안정적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;IP는 바뀌어도 &lt;/span&gt;&lt;b&gt;ASN은 잘 안 바뀐다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;ldquo;VPN 냄새&amp;rdquo; 나는 ASN&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPN 사업자들은 보통:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자체 ASN을 갖거나&lt;/li&gt;
&lt;li&gt;특정 데이터센터 ASN을 장기간 사용한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이런 히스토리가 쌓인다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 ASN에서 나오는 트래픽의 80%가 VPN 판정&lt;/li&gt;
&lt;li&gt;이 ASN은 여러 국가 IP를 동시에 광고&lt;/li&gt;
&lt;li&gt;일반 사용자 서비스에서 비정상 비율 과다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 &amp;ldquo;의심&amp;rdquo;이 아니라 &lt;span&gt;&lt;b&gt;관측 통계&lt;/b&gt;&lt;/span&gt;다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;국가보다 강한 신호가 되는 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;국가는 바뀔 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해외 로밍&lt;/li&gt;
&lt;li&gt;위성 인터넷&lt;/li&gt;
&lt;li&gt;라우팅 우회&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ASN은 훨씬 덜 흔들린다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한국 통신사 ASN은 계속 한국 통신사다&lt;/li&gt;
&lt;li&gt;AWS ASN은 어디서 써도 AWS다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 실무에서는:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;국가 + ASN 조합&quot;이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;국가 단독보다 훨씬 강한 신호다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그런데 왜 ASN만으로도 부족한가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 한계가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;주거용 프록시는 &lt;/span&gt;&lt;b&gt;정상 ISP ASN을 그대로 쓴다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;회사 VPN은 &lt;/span&gt;&lt;b&gt;정상 ASN처럼 보인다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;대기업 내부 트래픽은 &lt;/span&gt;&lt;b&gt;데이터센터처럼 보일 수 있다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, ASN은 &amp;ldquo;정체&amp;rdquo;를 말해주지 &amp;ldquo;의도&amp;rdquo;를 말해주진 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 ASN은:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;차단 조건 ❌&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가중치 신호 ⭕&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;집계&amp;middot;군집 분석 ⭕&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포지션이 가장 안전하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;</description>
      <category>어뷰징 방지</category>
      <author>왈왈디</author>
      <guid isPermaLink="true">https://walwaldev.tistory.com/185</guid>
      <comments>https://walwaldev.tistory.com/185#entry185comment</comments>
      <pubDate>Sun, 21 Dec 2025 23:31:13 +0900</pubDate>
    </item>
    <item>
      <title>빅쿼리 알아보기</title>
      <link>https://walwaldev.tistory.com/184</link>
      <description>&lt;h1&gt;&lt;b&gt;빅쿼리(BigQuery)는 어떤 DB인가?&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 성장할수록 데이터는 폭발적으로 늘어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 처리(DB에 기록하는 것)는 MySQL&amp;middot;PostgreSQL 같은 RDB로 충분하지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 분석(대량 데이터 집계&amp;middot;리포팅)을 같은 DB에서 처리하기 시작하면 금방 한계가 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 지점에서 등장하는 대표적인 선택지가 &lt;span&gt;&lt;b&gt;Google BigQuery&lt;/b&gt;&lt;/span&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 문법을 사용하지만, 내부 구조와 목적은 우리가 익숙한 RDBMS와 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;BigQuery는 트랜잭션 DB가 아니다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 짚어야 하는 사실:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BigQuery는 MySQL/PostgreSQL 같은 트랜잭션 DB(OLTP)가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BigQuery는 &lt;b&gt;대규모 데이터 분석&lt;/b&gt;을 위해 설계된 컬럼 기반 &lt;b&gt;데이터 웨어하우스(OLAP)&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목적 자체가 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;목적&lt;/b&gt;&lt;b&gt;도구&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;사용자 로그인/주문/결제 등 실시간 트랜잭션 처리&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;MySQL, PostgreSQL&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;수억&amp;middot;수십억 건 데이터에서 집계/패턴 분석/리포팅&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;BigQuery&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무적으로 구분하면 이렇게 된다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;애플리케이션&lt;/b&gt;&lt;/span&gt;은 MySQL에서 데이터를 기록하고&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;분석 플랫폼&lt;/b&gt;&lt;/span&gt;은 BigQuery에서 데이터를 읽고 요약한다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;BigQuery가 빠른 이유: 컬럼형 저장과 병렬 처리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BigQuery는 데이터를 &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&gt;country&lt;/span&gt;, &lt;span&gt;created_at&lt;/span&gt;, &lt;span&gt;revenue&lt;/span&gt; 3개의 컬럼이 있을 때,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;revenue&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;일반 RDBMS(행 기반 저장)와 다르게&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 컬럼 읽지 않아도 되므로 속도도 빨라지고 비용도 줄어든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 BigQuery는 기본적으로 &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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 많아질수록 내부에서 그냥 더 큰 클러스터를 붙여 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집약하면 BigQuery의 성능 비밀은 두 가지다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컬럼 기반 저장&lt;/li&gt;
&lt;li&gt;자동 병렬 처리&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;서버를 운영할 필요가 없다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL을 운영하면 고민해야 하는 것:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CPU/메모리 스펙&lt;/li&gt;
&lt;li&gt;인덱스 최적화&lt;/li&gt;
&lt;li&gt;쿼리 튜닝&lt;/li&gt;
&lt;li&gt;샤딩 확장&lt;/li&gt;
&lt;li&gt;스토리지 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BigQuery는 전부 필요 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 신경 쓸 건 오직 두 가지:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터를 넣는다&lt;/li&gt;
&lt;li&gt;SQL을 날린다&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;BigQuery 비용 모델: 저장은 싸고, 조회가 비싸다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 DB들과 확연히 다른 부분.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;저장 비용 &amp;rarr; 저렴함&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쿼리 비용 &amp;rarr; 스캔한 데이터 양 기준으로 과금&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래가 실무에서 흔한 최적화 포인트다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SELECT *&lt;span&gt; 금지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;파티션 / 클러스터 사용&lt;/li&gt;
&lt;li&gt;필요한 컬럼만 스캔&lt;/li&gt;
&lt;li&gt;FILTER 조건을 파티션키에 맞춰 설계&lt;/li&gt;
&lt;li&gt;미리 집계한 마트(mart) 테이블 운용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL처럼 CPU 점유율로 장애가 오는 게 아니라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BigQuery는 &lt;span&gt;&lt;b&gt;비효율적인 쿼리 = 비용 폭탄&lt;/b&gt;&lt;/span&gt;의 문제가 생긴다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;파티션과 클러스터는 선택이 아니라 필수&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BigQuery 성능/비용을 결정하는 가장 핵심 개념.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파티션(partition): 테이블을 날짜/범주 기준으로 물리적으로 잘라두는 기능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클러스터(cluster): 파티션 내부를 특정 컬럼 기준으로 정렬해두는 기능&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;created_at&lt;span&gt; 기준으로 파티션&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;파티션 내부를 &lt;span&gt;country&lt;/span&gt;, &lt;span&gt;platform&lt;/span&gt;으로 클러스터링&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 설계하면&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;WHERE created_at &amp;gt;= '2025-12-01'
AND country = 'US'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 쿼리는 스캔량이 극적으로 줄어든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;BigQuery를 쓰면 좋은 시나리오&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 중 하나라도 해당하면 BigQuery는 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GA(Google Analytics)4 로그 분석&lt;/li&gt;
&lt;li&gt;플레이어/유저 행동 데이터 분석&lt;/li&gt;
&lt;li&gt;광고/매출 리포팅&lt;/li&gt;
&lt;li&gt;리텐션/코호트 분석&lt;/li&gt;
&lt;li&gt;통합 로그 데이터 레이크 구축&lt;/li&gt;
&lt;li&gt;머신러닝/추천 시스템용 대량 데이터 피쳐 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 RDBMS는 데이터를 기록하기 위한 DB이고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;BigQuery는 &lt;/span&gt;&lt;b&gt;규모가 커진 데이터를 읽고 가공하기 위한 DB&lt;/b&gt;&lt;span&gt;다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;BigQuery를 적대적으로 대해야 하는 상황도 있다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점만 있는 건 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실시간 트랜잭션 업데이트&amp;middot;JOIN이 많으면 부적합&lt;/li&gt;
&lt;li&gt;UPDATE/DELETE가 상대적으로 느리고 비용도 큼&lt;/li&gt;
&lt;li&gt;조인 테이블 수가 지나치게 많을 때 비효율적&lt;/li&gt;
&lt;li&gt;개발자가 비용 개념 없이 쿼리 작성하면 바로 폭탄&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빅쿼리는 도구가 아니라 _전략_이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;그냥 SQL 비슷하네&amp;rdquo;라고 접근하면 손해보기 쉽다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빅쿼리는 MySQL을 대체하는 DB가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MySQL 이후 데이터가 커졌을 때 해결하는 DB&lt;/b&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQL을 지원하지만 내부 방식은 완전히 다르다&lt;/li&gt;
&lt;li&gt;저장은 싸고, 읽는 게 비싸다&lt;/li&gt;
&lt;li&gt;파티션/클러스터 제대로 설계하면 비용과 속도가 갈린다&lt;/li&gt;
&lt;li&gt;분석/리포팅/머신러닝 기반 데이터 파이프라인에 최적화된 플랫폼이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 기반 비즈니스를 운영한다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 시점에서든 BigQuery를 이해하고 활용할 줄 아는 능력은 선택이 아니라 필수가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DataBase</category>
      <author>왈왈디</author>
      <guid isPermaLink="true">https://walwaldev.tistory.com/184</guid>
      <comments>https://walwaldev.tistory.com/184#entry184comment</comments>
      <pubDate>Sun, 7 Dec 2025 21:45:47 +0900</pubDate>
    </item>
    <item>
      <title>구글 Play Integrity API로 안드로이드 악성 사용자 차단하기</title>
      <link>https://walwaldev.tistory.com/181</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모바일 리워드 앱은 사용자 보상 구조상 &lt;i&gt;&amp;ldquo;정상 사용자 vs 자동화/매크로/다계정 유저 등의 어뷰저&amp;rdquo;&lt;/i&gt; 간의 싸움이 항상 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 글로벌 시장(예: 미국, 캐나다 등)에서는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국과 달리 본인 인증 수단이 약하기 때문에 디바이스 단위의 보안 중요하다.&lt;/p&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&gt;&lt;b&gt;구글 Play Integrity API&lt;/b&gt;&lt;/span&gt;는 구글에서 제공하는 안드로이드 보안 도구로, 앱과 디바이스의 무결성을 확인해 악성 사용자(루팅 디바이스, 클론 앱 사용자 등)를 식별하고 차단하는 데 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스에 이 API를 어떻게 활용해 어뷰저를 차단하고, 어떤 값들을 어떤 방식으로 활용할 수 있는지 알아보자.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Play Integrity API란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Play Integrity API는 사용자의 디바이스와 앱이 &lt;span&gt;&lt;b&gt;정상적인 환경에서 실행되고 있는지&lt;/b&gt;&lt;/span&gt; 확인할 수 있도록 구글이 제공하는 보안 API이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디바이스 루팅 여부, 앱 위변조, 인증 상태 등을 체크할 수 있으며, &lt;span&gt;&lt;b&gt;클라이언트가 발급한 토큰을 서버에서 검증하는 구조&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;주요 기능&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;디바이스 무결성 검사&lt;/b&gt;&lt;/span&gt; (루팅, 에뮬레이터 등)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;앱 무결성 검사&lt;/b&gt;&lt;/span&gt; (APK 위변조, 비공식 설치 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;앱 라이선스 상태 확인&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;요청 유효성 검증&lt;/b&gt;&lt;/span&gt; (nonce, timestamp)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;사용 흐름 요약&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;클라이언트 앱&lt;/b&gt;&lt;/span&gt;&lt;span&gt;에서 &lt;/span&gt;integrityToken&lt;span&gt;을 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;해당 토큰을 &lt;/span&gt;&lt;b&gt;백엔드 서버에 전달&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;서버는 &lt;/span&gt;&lt;b&gt;Google API에 토큰 검증 요청&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Google이 반환한 &lt;span&gt;&lt;b&gt;Payload(JSON)&lt;/b&gt;&lt;/span&gt; 을 바탕으로 유저 차단 여부 판단&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;응답 Payload 주요 프로퍼티&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;deviceIntegrity&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디바이스의 무결성 수준&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;각 값의 &lt;/b&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;MEETS_DEVICE_INTEGRITY&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;정상 디바이스 (루팅/에뮬 아님)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;MEETS_BASIC_INTEGRITY&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;기본 무결성은 충족 (루팅 추정됨)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;NO_INTEGRITY&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;루팅/에뮬레이터/위조 환경일 가능성 높음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;appIntegrity&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱의 설치 상태 및 서명 정보&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;각 값의&amp;nbsp;&lt;/b&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;PLAY_RECOGNIZED&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Play 서명으로 설치된 앱&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;UNRECOGNIZED_VERSION&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;비공식 빌드 or 변경된 APK&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;UNEVALUATED&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;평가 불가 (일시적 오류)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;accountDetails.appLicensingVerdict&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 라이선스 검증 결과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;각 값의&amp;nbsp;&lt;/b&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;LICENSED&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;정상 사용자&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;UNLICENSED&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Play Store 외부 설치 사용자 (불법 복제 가능성 포함)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;UNKNOWN&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;평가 불가&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;requestDetails&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청의 상세 정보&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;requestPackageName&lt;/span&gt;: 클라이언트에서 설정한 패키지명&lt;/li&gt;
&lt;li&gt;&lt;span&gt;nonce&lt;/span&gt;: 요청 위변조 방지를 위한 임의 문자열&lt;/li&gt;
&lt;li&gt;&lt;span&gt;timestampMillis&lt;/span&gt;: 토큰 발급 시각 (UTC)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실무 적용 시 고려할 것들&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;어떤 시점에 API를 호출할 것인가?&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;로그인 시점&lt;/b&gt;&lt;span&gt; 또는 &lt;/span&gt;&lt;b&gt;앱 실행 초기&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;너무 잦은 호출은 비용 낭비&lt;/li&gt;
&lt;li&gt;신뢰 수준 높은 시점에 1회 호출하는 것이 일반적&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;차단 기준 설정하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;if (
  response.deviceIntegrity.includes(&quot;NO_INTEGRITY&quot;) ||
  response.appIntegrity === &quot;UNRECOGNIZED_VERSION&quot;
) {
  blockUser(&quot;Integrity policy violation&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote style=&quot;color: #0e0e0e;&quot; data-ke-style=&quot;style1&quot;&gt;블락 외에도 &lt;span&gt;모니터링&lt;/span&gt;, &lt;span&gt;기능 제한&lt;/span&gt;, &lt;span&gt;추가 인증 요구&lt;/span&gt; 등 유연한 대응이 가능&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;비용 정책&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;계정별 일일 무료 호출 한도가 정해져 있음&lt;/li&gt;
&lt;li&gt;초과 시 비용 청구됨&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;보안 강화 팁&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;nonce&lt;/span&gt;는 서버에서 생성하여 클라이언트에 전달 (Replay 방지)&lt;/li&gt;
&lt;li&gt;결과 토큰은 반드시 서버에서 검증&lt;/li&gt;
&lt;li&gt;검증 실패 시(유효하지 않거나 디코드 불가) &amp;rarr; 차단 or 로깅&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;흔한 실수 &amp;amp; 주의사항&lt;/b&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;클라이언트에서만 검증&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;무의미. 반드시 &lt;span&gt;&lt;b&gt;서버에서 검증&lt;/b&gt;&lt;/span&gt;해야 함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;nonce를 사용하지 않음&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;동일 토큰 재사용 가능성 생김&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;결과 로그 저장 안 함&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;분석/이슈 대응 시 어려움&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;모든 NO_INTEGRITY를 블락&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;오탐 가능성 있음 (기업용 디바이스, 일부 개발자 환경 등)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;테스트 방법&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;루팅된 디바이스, 에뮬레이터, 클론 앱 등 다양한 환경에서 테스트 필요&lt;/li&gt;
&lt;li&gt;Play Integrity 응답은 &lt;span&gt;&lt;b&gt;Google 서버에서 평가되므로&lt;/b&gt;&lt;/span&gt;, 일부 결과는 수 분 지연 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;분석에 활용 가능한 항목&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일일 무결성 실패율 (&lt;span&gt;NO_INTEGRITY&lt;/span&gt; 비율)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;앱 무결성 위반률 (&lt;/span&gt;UNRECOGNIZED_VERSION&lt;span&gt;)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;requestHash, IP, Device ID 기반 다계정 시도 탐지&lt;/li&gt;
&lt;li&gt;Integrity 응답 결과 기반 사용자 클러스터링&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;참고 자료&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/google/play/integrity/overview?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Google Play Integrity API 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1760279568260&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Play Integrity API 개요 &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;이 페이지는 Cloud Translation API를 통해 번역되었습니다. Play Integrity API 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Play Integrity API를 사용하면 사용자 &quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/google/play/integrity/overview?hl=ko&quot; data-og-url=&quot;https://developer.android.com/google/play/integrity/overview?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/kRFsq/hyZLqKiQZy/zjy8Ms04DrgLJxNsZoeOv1/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676,https://scrap.kakaocdn.net/dn/9rqlk/hyZKbVyWVo/MU4rFvx3kjJ4mTPB5P00y1/img.png?width=1182&amp;amp;height=384&amp;amp;face=0_0_1182_384&quot;&gt;&lt;a href=&quot;https://developer.android.com/google/play/integrity/overview?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/google/play/integrity/overview?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/kRFsq/hyZLqKiQZy/zjy8Ms04DrgLJxNsZoeOv1/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676,https://scrap.kakaocdn.net/dn/9rqlk/hyZKbVyWVo/MU4rFvx3kjJ4mTPB5P00y1/img.png?width=1182&amp;amp;height=384&amp;amp;face=0_0_1182_384');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Play Integrity API 개요 &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지는 Cloud Translation API를 통해 번역되었습니다. Play Integrity API 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Play Integrity API를 사용하면 사용자&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>어뷰징 방지</category>
      <author>왈왈디</author>
      <guid isPermaLink="true">https://walwaldev.tistory.com/181</guid>
      <comments>https://walwaldev.tistory.com/181#entry181comment</comments>
      <pubDate>Sun, 12 Oct 2025 23:32:51 +0900</pubDate>
    </item>
    <item>
      <title>Promise.all() VS Promise.allSettled() 비동기 작업 실행하기</title>
      <link>https://walwaldev.tistory.com/180</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;배경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 작업에 작업 실패 시 경보를 추가하는 작업을 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Promise.allSettled() 메서드를 실행하면서 이 구문을 try catch로 묶어 처리하는 코드를 발견했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Promise.allSettled()는 병렬 작업 중 에러가 발생하더라도 호출부에서 에러가 throw 되지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 작업 처리를 위해 흔히 사용되는 Promise.all()과 Promise.allSettled()에 대해 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Promise.all()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Promsie.all()은 일반적으로 다음 코드를 실행하기 전에, 서로 연관된 비동기 작업 여러 개가 모두 실행되어야 할 때 사용된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과값으로 실행된 프로미스들의 결과값을 담은 배열을 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과값의 순서는 매개변수로 전달된 프로미스 순서와 동일하다.&lt;/p&gt;
&lt;pre id=&quot;code_1757857971479&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 비동기 함수 예시
function fetchUser(id) {
  return new Promise(resolve =&amp;gt; {
    setTimeout(() =&amp;gt; {
      resolve({ id, name: `User${id}` });
    }, 1000 * id); // id에 따라 응답 속도를 다르게
  });
}

// 여러 유저 정보 가져오기
async function getUsers() {
  const promises = [fetchUser(1), fetchUser(2), fetchUser(3)];

  try {
    // 모든 요청이 끝날 때까지 기다림
    const results = await Promise.all(promises);
    console.log(&quot;모든 유저 정보:&quot;, results);
	// 모든 유저 정보: [ {id:1, name:&quot;User1&quot;}, {id:2, name:&quot;User2&quot;}, {id:3, name:&quot;User3&quot;} ] 
  } catch (error) {
    console.error(&quot;에러 발생:&quot;, error);
  }
}

getUsers();&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;Promise.all()은 인자로 들어온 프로미스 중 하나라도 거부 당하면 (에러 발생) &lt;br /&gt;Promise.all은 즉시 거부하여 catch 구문으로 넘어가게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 여기서 주의할 점은 Promise.all()이 reject 상태가 되었다고 해서, 인자의 프로미스 작업들이 모두 중단되는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 작업들은 비동기적으로 여전히 진행되고, Promise.all()만 에러를 반환하며 catch 구문으로 넘어가는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Promise.allSettled()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 달리 Promise.allSettled()는 인자로 들어온 프로미스들을 모두 실행하거나, 거부한 후&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 프로미스 결과 객체들을 담은 배열을 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 서로의 성공/실패 여부와 관계없이 여러 비동기 작업을 실행해야 하거나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 각 프로미스의 결과를 알고 싶을 때 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Promise.all()은 서로 연관된 작업을 수행하거나, 하나라도 거부 당했을 때 즉시 거부하고 싶을 때 적합하다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환된 결과 객체는 &lt;b&gt;status&amp;nbsp;&lt;/b&gt;프로퍼티를 갖는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;status&lt;/b&gt; 값에 따라서 값이 &lt;b&gt;&quot;fulfilled&quot;&lt;/b&gt; 라면&lt;b&gt; value&lt;/b&gt; 프로퍼티를&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;rejected&quot;&lt;/b&gt; 라면 &lt;b&gt;reason&lt;/b&gt; 프로퍼티를 갖는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;value, reason&lt;/b&gt;을 통해서 프로미스가 어떻게 실행/거부 됐는지 알 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1757859301515&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Promise.allSettled([
  Promise.resolve(33),
  new Promise((resolve) =&amp;gt; setTimeout(() =&amp;gt; resolve(66), 0)),
  99,
  Promise.reject(new Error(&quot;an error&quot;)),
]).then((values) =&amp;gt; console.log(values));

// [
//   {status: &quot;fulfilled&quot;, value: 33},
//   {status: &quot;fulfilled&quot;, value: 66},
//   {status: &quot;fulfilled&quot;, value: 99},
//   {status: &quot;rejected&quot;,  reason: Error: an error}
// ]&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;async 함수로 표현하면 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1757859423798&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function successTask() {
  return &quot;성공 결과&quot;; // resolve(&quot;성공 결과&quot;)와 같음
}

async function failTask() {
  throw new Error(&quot;실패 발생!&quot;); // reject(new Error(...))와 같음
}

async function run() {
  const tasks = [successTask(), failTask(), Promise.resolve(123), Promise.reject(&quot;단순 실패&quot;)];

  const results = await Promise.allSettled(tasks);

  results.forEach((result, index) =&amp;gt; {
    if (result.status === &quot;fulfilled&quot;) {
      console.log(`Task ${index + 1} 성공:`, result.value);
    } else {
      console.log(`Task ${index + 1} 실패:`, result.reason);
    }
  });
}

run();

/*
Task 1 성공: 성공 결과
Task 2 실패: Error: 실패 발생!
Task 3 성공: 123
Task 4 실패: 단순 실패
*/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Promise.allSettled()를 사용하는 부분에 경보를 붙이는 코드는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;try catch를 사용하지 않고,&lt;br /&gt;Promise.allSettled() 결과 배열에서 status가 rejected인 객체를 필터링하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그에 따라 경보가 작동하도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고 자료&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/all&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MDN Promise.all()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MDN Promise.allSettled()&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1757859563353&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Promise.all() - JavaScript | MDN&quot; data-og-description=&quot;이 메서드는 여러 프로미스의 결과를 집계할 때 유용하게 사용할 수 있습니다. 일반적으로 다음 코드를 계속 실행하기 전에 서로 연관된 비동기 작업 여러 개가 모두 이행되어야 하는 경우에 사&quot; data-og-host=&quot;developer.mozilla.org&quot; data-og-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/all&quot; data-og-url=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/all&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/all&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/all&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Promise.all() - JavaScript | MDN&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 메서드는 여러 프로미스의 결과를 집계할 때 유용하게 사용할 수 있습니다. 일반적으로 다음 코드를 계속 실행하기 전에 서로 연관된 비동기 작업 여러 개가 모두 이행되어야 하는 경우에 사&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.mozilla.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Node.js/JavaScript</category>
      <author>왈왈디</author>
      <guid isPermaLink="true">https://walwaldev.tistory.com/180</guid>
      <comments>https://walwaldev.tistory.com/180#entry180comment</comments>
      <pubDate>Sun, 14 Sep 2025 23:19:35 +0900</pubDate>
    </item>
    <item>
      <title>HTTP ETag 응답 헤더 (+ If-Match, If-None-Match 요청 헤더)</title>
      <link>https://walwaldev.tistory.com/179</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;ETag란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Etag는 http 응답 헤더의 일종으로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 버전의 리소스를 식별하는 식별자이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ETag는 리소스의 지문과 같은 역할로,&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;만약 특정 URL 의 리소스가 변경된다면, 새로운 ETag 가 생성된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ETag를 비교하면 리소스가 동일한지 빠르게 판단할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스가 변동 없고 ETag가 동일하다면 서버는 새로운 응답을 보낼 필요가 없기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐쉬를 더 효율적으로 사용할 수 있고, 대역폭 사용량을 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; 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;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;span&gt;&quot;mid-air collisions&quot;라는 리소스 간의 동시 다발적 수정 및 덮어쓰기 현상을 막는데 유용하다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문법&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #edeef0; color: #000000; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;ETag: W/&quot;&amp;lt;etag_value&amp;gt;&quot;
ETag: &quot;&amp;lt;etag_value&amp;gt;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Weak ETag(&lt;/b&gt;&lt;b&gt;W/) &lt;/b&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;비트 단위로는 달라도 &amp;ldquo;같은 콘텐츠&amp;rdquo;라고 볼 수 있는 경우에 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;W/&lt;/span&gt;가 붙은 ETag는 &amp;ldquo;이 리소스는 대충 같은 거야&amp;rdquo;라는 서버의 힌트이고, &lt;br /&gt;&lt;span&gt;W/&lt;/span&gt;가 없는 건 trong validator가 사용되었다는 의미로 &amp;ldquo;비트까지 완전히 같아&amp;rdquo;라는 약속이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 사용 예시이다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot; style=&quot;background-color: #edeef0; color: #000000; text-align: start;&quot;&gt;&lt;code&gt;ETag: &quot;33a64df551425fcc55e4d42a148795d9f25f89d4&quot;
ETag: W/&quot;0815&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ETag는 응답 값을&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt; ASCII 코드와 같이 고유한 형태로 나타낸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ETag&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;의 값을 생성하는 방법(Method)은 단순히 한가지로 정해져있진 않다. &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;예를들어, MDN은 wiki(콘텐츠)의 16진수 해시를 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;사용법1 - 충돌 피하기 (mid-air collisions)&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;ETag와 http 요청 헤더인 &lt;b&gt;If-Match&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;리소스의 충돌을 예측할 수 있다.&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;예를 들어, 웹 페이지에서 사용자가 다른 사용자도 수정할 수 있는 문서를 수정하고 있는 상황이다.&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;이때 이 사용자가 수정한 문서를 저장하기 위해 POST 요청을 보냈다.&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;이 POST 요청은 If-Match 헤더로 사용자가 수정을 시작한 시점의 리소스의 Etag 값을 담아 보낸다.&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;서버는 요청의 If-Match 헤더에 담긴 ETag 값이 현재의 ETag값과 동일한지 확인하고,&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;b&gt;412 Precondition Failed&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; 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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;사용법2 - 변경되지 않은 리소스의 캐싱&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 Etag의 사용 목적은 변경되지 않은 리소스를 캐시하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 ETag를 가지고 있다면 ETag를 If-None-Match 헤더 필드에 담아 요청을 전송한다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot; style=&quot;background-color: #edeef0; color: #000000; text-align: start;&quot;&gt;&lt;code&gt;If-None-Match: &quot;33a64df551425fcc55e4d42a148795d9f25f89d4&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서버는 클라이언트가 요청 헤더에 담아 보낸 ETag를 현재 버전 리소스의 Etag와 비교하고,&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두 값이 일치하는 경우에는 &lt;b&gt;304 Not Modified&lt;/b&gt; 상태 코드를 반환한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 때 응답 body는 비어있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;304 응답 코드를 받으면 클라이언트는 캐시된 버전이 여전히 유효하다는 것을 알 수 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;참고 자료&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/Reference/Headers/ETag#:~:text=ETag%20HTTP%20%EC%9D%91%EB%8B%B5%20%ED%97%A4%EB%8D%94%EB%8A%94%20%ED%8A%B9%EC%A0%95%20%EB%B2%84%EC%A0%84%EC%9D%98%20%EB%A6%AC%EC%86%8C%EC%8A%A4%EB%A5%BC,%EC%95%8A%EC%95%98%EC%9C%BC%EB%A9%B4%2C%20%EC%9B%B9%20%EC%84%9C%EB%B2%84%EB%A1%9C%20full%20%EC%9A%94%EC%B2%AD%EC%9D%84%20%EB%B3%B4%EB%82%B4%EC%A7%80%20%EC%95%8A%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MDN ETag&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>컴퓨터 공학 &amp;amp; 통신</category>
      <author>왈왈디</author>
      <guid isPermaLink="true">https://walwaldev.tistory.com/179</guid>
      <comments>https://walwaldev.tistory.com/179#entry179comment</comments>
      <pubDate>Sun, 31 Aug 2025 23:11:11 +0900</pubDate>
    </item>
    <item>
      <title>Prisma 컬럼 삭제 에러 &amp;amp; Select문 동작 방식</title>
      <link>https://walwaldev.tistory.com/178</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;이슈 발생&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJS, Prisma, MySQL 환경의 프로젝트에서 MySQL 테이블의 컬럼을 삭제하는 작업을 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이하에서는 해당 컬럼을 &quot;컬럼a&quot;라고 칭하겠다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 코드에서도 컬럼a를 참조하는 모든 코드를 확인하고,&lt;br /&gt;컬럼a를 사용하지 않도록 수정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정된 코드가 포함된 API들은 배치 작업 용이었어서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 도중 실시간 트래픽이 없어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 스키마 변경과 애플리케이션 코드 배포 사이 시간 동안의 이슈는 고려하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 컬럼a를 전혀 참조하지 않는 실시간 트래픽이 많은 API에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 시간 동안 에러가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 컬럼a를 명시적으로 사용하는 곳이 없는 API라 에러가 발생한 이유를 이해할 수 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Prisma가 생성하는 쿼리를 확인해보니,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인을 알 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈의 원인은 Prisma의 find 메서드 동작 방식에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prisma의 find 메서드에서 아래와 같이 select 대상 컬럼을 명시하지 않으면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블의 모든 컬럼을 조회하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1755438344750&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; const employee = await this.prisma.employees.findUnique({
      where: { id: employeeId }
    });&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;이때 DB에 전달되는 쿼리가 아래와 같을 것이라 기대했다.&lt;/p&gt;
&lt;pre id=&quot;code_1755438516868&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
FROM employees
WHERE id = ${employeeId};&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;그러나 실제로 Prisma가 생성하는 쿼리는 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(정확히 이와 동일한 쿼리가 실행되는 것은 아님, 설명을 위해 단순화함)&lt;/p&gt;
&lt;pre id=&quot;code_1755438599018&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT id, name, phone_number, weight, height, age
FROM employees
WHERE id = ${employeeId};&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;테이블 내 모든 컬럼을 SELECT 문에서 명시하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때문에 컬럼a를 삭제했을 때,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어플리케이션 코드 상에서는 컬럼a를 사용하는 곳이 없었는데도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서 컬럼a가 삭제된 후 컬럼a를 조회하는 쿼리가 실행되어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러가 발생한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방안&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방안은 간단하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬럼이 삭제될 것을 고려하여 find 문을 사용할 때 select 프로퍼티를 명시적으로 작성하여,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도치 않은 컬럼이 SELECT 절에 포함되지 않도록 하는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1755438935150&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; const employee = await this.prisma.employees.findUnique({
     select: {
      	id: true,
        age: true
      },
      where: { id: employeeId }
    });&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;SELECT문에서 필요한 컬럼만 명시하는 것은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬럼을 삭제할 때 의식하지 못한 에러의 발생을 방지할 뿐만 아니라 아래와 같은 장점들이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 성능 측면&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;네트워크 전송량 감소: &lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;불필요한 컬럼까지 가져오지 않으니, DB에서 애플리케이션으로 넘어가는 데이터 크기가 줄어든다.&lt;/li&gt;
&lt;li&gt;특히 컬럼이 많고 레코드 수가 많을 때 효과가 크다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB 서버의 I/O 부담 감소&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스토리지에서 읽어야 하는 양이 줄어들어 쿼리 속도가 개선될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 애플리케이션 안정성&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;불필요한 데이터 노출 방지&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;민감한 컬럼(예: 비밀번호 해시, 내부 관리용 필드 등)이 의도치 않게 애플리케이션 레이어로 전달되는 것을 막을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스키마 변경에 대한 영향 최소화 (컬럼 삭제 등)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;SELECT *&lt;/span&gt;는 컬럼이 추가되면 쿼리 결과 구조가 달라지는데, 지정된 컬럼만 가져오면 이런 변경에 덜 민감하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 코드 가독성 및 유지보수&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;쿼리의 의도가 명확해짐&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 데이터를 쓰려는지 명시적으로 드러나서 협업 시 이해하기 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;불필요한 매핑/파싱 코드 감소&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ORM이나 DTO 매핑 과정에서 실제로 필요 없는 필드를 다루지 않아도 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 비용 효율성 (운영 환경)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;클라우드 환경에서의 비용 절감&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 요금, DB 사용량(쿼리 실행 시간, I/O 비용)이 줄어드는 효과가 있을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Node.js</category>
      <category>Prisma</category>
      <category>Prisma SELECT *</category>
      <category>Prisma SELECT ALL</category>
      <category>Prisma 컬럼 삭제 에러</category>
      <author>왈왈디</author>
      <guid isPermaLink="true">https://walwaldev.tistory.com/178</guid>
      <comments>https://walwaldev.tistory.com/178#entry178comment</comments>
      <pubDate>Sun, 17 Aug 2025 23:06:32 +0900</pubDate>
    </item>
    <item>
      <title>Github Actions로 EC2 배포 자동화하기 (feat. NestJS, prisma)</title>
      <link>https://walwaldev.tistory.com/177</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;이슈 발생&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이드 프로젝트 배포를 수동으로 진행하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 변경 사항이 발생한다면&amp;nbsp;EC2 인스턴스에 접속하여 git pull origin main으로 코드를 내려 받아서,&lt;br /&gt;npx prisma generate, npm run build를 실행하여 빌드하고, pm2로 restart 시켰다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느날 평소와 같이 변경 사항을 pull 받고, build를 실행하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cpu 사용량이 100%에 도달하여 인스턴스가 먹통이 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스를 재부팅해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 중인 인스턴스 유형은 t2.micro인데, 비용 상 인스턴스 유형을 업그레이드 하고 싶지는 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 build는 다른 서버에서 하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build 산출물을 EC2 인스턴스에 업로드하여 EC2에서는 서버 실행만 하는 것이 옳다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 Github Actions로 구현해보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Github Actions 배포 자동화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github의 Actions 기능은 CI/CD 등을 자동화할 수 있는 툴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트 디렉토리 &lt;b&gt;.github/workflows&lt;/b&gt; 경로에 자동화 스크립트를 작성하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건에 따라 자동으로 실행시킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, on: 조건이 아래와 같이 작성된 경우 main 브랜치에 push가 된 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jobs 이하의 작업이 실행된다는 의미이다.&lt;/p&gt;
&lt;pre id=&quot;code_1754230680782&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Build and Deploy NestJS App

on:
  push:
    branches: ['main']&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 배포 자동화를 위해 작성한 스크립트는 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체를 한 번에 이해하기 보다는 단계별로 쪼개어 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;build-and-deploy.yml&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1754229890016&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: Build and Deploy NestJS App

on:
  push:
    branches: ['main']

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js 18.x
        uses: actions/setup-node@v4
        with:
          node-version: 18.x
          cache: 'npm'

      - name: Install Dependencies (Production)
        run: npm ci --omit=dev

      - name: Build Nest App
        run: npm run build

      - name: Generate Prisma Client
        run: npx prisma generate

      - name: Check Prisma Client generated
        run: |
          test -f node_modules/@prisma/client/index.js || (echo &quot;❌ Prisma client not found!&quot; &amp;amp;&amp;amp; exit 1)
          test -f node_modules/.prisma/client/default.js || echo &quot;⚠️ .prisma/client/default.js 없음 (Prisma 5.x에서는 선택적일 수 있음)&quot;

      - name: Prepare Output Directory
        run: |
          mkdir output
          cp -r dist node_modules prisma package*.json output/
          if [ -d .prisma ]; then cp -r .prisma output/; fi

      - name: Archive Build Output
        run: |
          cd output &amp;amp;&amp;amp; tar czf ../nest-app.tar.gz .

      - name: Get Public IP
        id: ip
        uses: haythem/public-ip@v1.3

      - name: Print Public IP
        run: |
          echo &quot;GitHub Actions IP: ${{ steps.ip.outputs.ipv4 }}&quot;

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: 'ap-northeast-2'

      - name: Add GitHub Actions IP to Security Group
        run: |
          aws ec2 authorize-security-group-ingress \
            --group-id ${{ secrets.SECURITY_GROUP_ID }} \
            --protocol tcp \
            --port 22 \
            --cidr ${{ steps.ip.outputs.ipv4 }}/32

      - name: Upload Build to Server via SCP
        uses: appleboy/scp-action@v0.1.6
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          port: 22
          source: 'nest-app.tar.gz'
          target: '~/deploy'

      - name: Execute Remote Deploy Script via SSH
        uses: appleboy/ssh-action@v0.1.10
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          port: 22
          script: |
            bash ~/deploy/deploy.sh

      - name: Remove GitHub Actions IP from Security Group
        if: always()
        run: |
          aws ec2 revoke-security-group-ingress \
            --group-id ${{ secrets.SECURITY_GROUP_ID }} \
            --protocol tcp \
            --port 22 \
            --cidr ${{ steps.ip.outputs.ipv4 }}/32&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 배포 자동화 스크립트의 전체적인 흐름은 아래와 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;GitHub Actions&lt;/b&gt;&lt;/span&gt;: CI 파이프라인에서 npm ci, &lt;span&gt;nest build, npx prsima generate&lt;/span&gt; 실행 &lt;br /&gt;&amp;rarr; node_modules, prisma, &lt;span&gt;dist/&lt;/span&gt; 디렉토리 생성&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;빌드 산출물(zip/tar)&lt;/b&gt;&lt;/span&gt;: &lt;span&gt;dist/&lt;/span&gt;, node_modules, prisma, 필요한 설정파일만 포함하여 압축&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;서버 전송&lt;/b&gt;&lt;/span&gt;: &lt;span&gt;scp&lt;/span&gt;로 대상 Linux 서버에 업로드&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;서버에서 실행&lt;/b&gt;&lt;/span&gt;: 서버에 미리 작성해둔 deploy.sh 파일로 pm2 서버&amp;nbsp;실행&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. node_modules, 빌드 산출물 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 자동화 작업의 첫 번째 단계는 서버를 구동하기 위해 필요한 파일들을 빌드하는 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) node 설치&lt;/h4&gt;
&lt;pre id=&quot;code_1754231016850&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js 18.x
        uses: actions/setup-node@v4
        with:
          node-version: 18.x
          cache: 'npm'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 필요한 node 버전을 설치한다.&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;2) npm ci&lt;/h4&gt;
&lt;pre id=&quot;code_1754231117167&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;      - name: Install Dependencies (Production)
        run: npm ci --omit=dev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 패키지를 install 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;--omit=dev &lt;/b&gt;는 상용 서버 구동에 불필요한 dev dependencies 패키지를 제외한다는 의미다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 서버 구동에 필요한 패키지는 모두 dependencies로 설치되어 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 @nestjs/cli, prisma, @prisma/client 패키지가 dev dependencies 로 설치되어 있었어서 수정해주었다.&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;3) npm run build&lt;/h4&gt;
&lt;pre id=&quot;code_1754231349399&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;      - name: Build Nest App
        run: npm run build&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm run build( =nest build) 로 typescript 코드를 빌드한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) npx prisma generate&lt;/h4&gt;
&lt;pre id=&quot;code_1754231434531&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;      - name: Generate Prisma Client
        run: npx prisma generate

      - name: Check Prisma Client generated
        run: |
          test -f node_modules/@prisma/client/index.js || (echo &quot;❌ Prisma client not found!&quot; &amp;amp;&amp;amp; exit 1)
          test -f node_modules/.prisma/client/default.js || echo &quot;⚠️ .prisma/client/default.js 없음 (Prisma 5.x에서는 선택적일 수 있음)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prisma를 사용하는 프로젝트여서 prisma generate 산출물도 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 EC2에서 npx prisma를 실행하려고 했으나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prsima 모듈을 찾을 수 없다는 에러가 계속 발생해서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(아마 node_modules를 압축하고 해제하는 과정에서 이슈가 발생한 게 아닐까 싶다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github actions 파이프라인에서 npx prisma generate 까지 실행하여 산출물을 서버로 전송하는 방식으로 변경했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;generate 결과물이 잘 생성되었는지 확인하기 위해 test 절차를 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prisma 버전에 따라 .prisma 는 없을 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 산출물 압축&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 필요한 결과물만 output 디렉토리로 이동&lt;/h4&gt;
&lt;pre id=&quot;code_1754231723582&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;      - name: Prepare Output Directory
        run: |
          mkdir output
          cp -r dist node_modules prisma package*.json output/
          if [ -d .prisma ]; then cp -r .prisma output/; fi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;output이라는 이름의 디렉토리를 생성하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dist, node_modules, prisma, package.json, package-lock.json 등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 구동에 필요한 파일/디렉토리만 담는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.prisma는 버전에 따라 있거나 없을 수 있으므로 분기처리 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 서버로 보낼 대상 디렉토리의 구조는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1754234385535&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;output/
├── dist/
├── node_modules/
├── package.json
├── package-lock.json
├── prisma/
└── .prisma/&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;2) 압축&lt;/h4&gt;
&lt;pre id=&quot;code_1754231827467&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;      - name: Archive Build Output
        run: |
          cd output &amp;amp;&amp;amp; tar czf ../nest-app.tar.gz .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고&amp;nbsp; output 디렉토리로 이동하여 그 안의 모든 파일을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 서버에 전송할 nest-app.tar.gz 파일&lt;span&gt;로 압축한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;3. 산출물 서버 전송&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;위 단계까지 실행하면 EC2 서버에 전송할 압축 파일이 완성된다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;rsync, scp 등의 방식으로 파일을 서버로 전송할 수 있는데,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;나는 scp 방식을 사용했다.&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&gt;1) github actions 서버 public ip 접근 허용하기&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;파일을 전송하기 전에 점검해야 하는 부분이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scp는 ssh 프로토콜을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, EC2 서버의 보안 그룹에서 &lt;b&gt;github actions 서버의 ssh port 접근이 허용&lt;/b&gt;되어 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://api.github.com/meta&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://api.github.com/meta&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답의 &lt;b&gt;actions&lt;/b&gt;&amp;nbsp;배열을 확인하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github actions 서버들의 public ip 리스트를 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 사용되는 인스턴스 ip는 변경될 수 있는데, 리스트의 ip&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; 수가 매우 많아서&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;모두 보안 그룹에서 허용해두기에는 부담이 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 사용 가능한 방식이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적으로 현재의 github actions 인스턴스의 public ip를 확인하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 ip를 보안그룹 허용 ip에 추가하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포가 완료되면 보안그룹에서 ip를 제거하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 사용 중인 인스턴스의 public ip를 확인한다.&lt;/p&gt;
&lt;pre id=&quot;code_1754232904663&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;      - name: Get Public IP
        id: ip
        uses: haythem/public-ip@v1.3

      - name: Print Public IP
        run: |
          echo &quot;GitHub Actions IP: ${{ steps.ip.outputs.ipv4 }}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 AWS에 접근하여 보안 그룹에 현재 public ip를 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1754233015799&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: 'ap-northeast-2'

      - name: Add GitHub Actions IP to Security Group
        run: |
          aws ec2 authorize-security-group-ingress \
            --group-id ${{ secrets.SECURITY_GROUP_ID }} \
            --protocol tcp \
            --port 22 \
            --cidr ${{ steps.ip.outputs.ipv4 }}/32&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 아래 세 가지 환경 변수가 필요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;AWS_ACCESS_KEY_ID&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AWS_SECRET_ACCESS_KEY&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SECURITY_GROUP_ID&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Github Actions에서 사용되는 환경변수는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레포지토리의 &lt;b&gt;Settings &amp;gt; Security &amp;gt; Secrets and variables &amp;gt; Actions&lt;/b&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;아래 두 값은 AWS IAM 에서 사용자를 생성하여 발급할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 관리를 위해 github actions에서만 사용할 사용자를 별도로 발급하는 것이 좋다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS_ACCESS_KEY_ID&lt;/li&gt;
&lt;li&gt;AWS_SECRET_ACCESS_KEY&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 값은 EC2 인스턴스의 보안 그룹 ID이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SECURITY_GROUP_ID&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 step을 거치면 보안 그룹 22포트 허용 ip에 현재 github actions 서버 public ip가 추가된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 압축 파일 전송&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접근이 허용되면 scp로 압축 파일을 전송한다.&lt;/p&gt;
&lt;pre id=&quot;code_1754233456249&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;      - name: Upload Build to Server via SCP
        uses: appleboy/scp-action@v0.1.6
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          port: 22
          source: 'nest-app.tar.gz'
          target: '~/deploy'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 서버의 디렉토리 구조는 아래와 같이 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 ~/deploy 디렉토리로 압축 파일을 전송한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;디렉토리 구조&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1754229289882&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;~/deploy/nest-app.tar.gz        &amp;larr; GitHub Actions에서 전송
~/app/nest-app/                 &amp;larr; 실제 실행 위치
~/app/nest-app/dist/main.js     &amp;larr; 엔트리포인트
~/app/nest-app/prisma/          &amp;larr; schema 및 migrations&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 전송 시 필요한 환경 변수는 아래 세 가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SERVER_HOST&lt;/b&gt;: EC2 서버의 ip 또는 도메인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SERVER_USER&lt;/b&gt;: EC2 서버 접속 시 사용하는 user&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SERVER_SSH_KEY&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 SERVER_SSH_KEY는 공개키-개인키 방식으로 생성 방법은 아래와 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ssh key 생성 방법&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 터미널에서 키 생성 명령어 실행&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1754233960250&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh-keygen -t rsa -b 4096 -C &quot;github-deploy&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions는 비밀번호 없이 사용해야 하므로, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;패스프레이즈는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;github_deploy_key&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;b&gt;개인 키 (이걸 GitHub Secret에 등록)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;github_deploy_key.pub&lt;span&gt;: 공개 키 (이걸 서버에 등록)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;2. 서버에 공개키 등록&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;서버에서 &lt;/span&gt;~/.ssh/authorized_keys&lt;span&gt;에 공개 키 추가하거나&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1754234055065&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cat github_deploy_key.pub &amp;gt;&amp;gt; ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 편집할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1754234086434&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;vi ~/.ssh/authorized_keys&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;b&gt;3. Github Actions Secrets에 개인키 등록&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SERVER_SSH_KEY로&amp;nbsp;github_deploy_key&lt;span&gt; 파일의 내용 전체를 복사한다. (예: &lt;/span&gt;cat github_deploy_key&lt;span&gt; 결과)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 권한 확인(선택 사항)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions에서 사용할 사용자 계정이 서버에 실제로 접근 가능한지 사전에 확인 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1754234191081&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh -i github_deploy_key youruser@your.server.ip&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 서버 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 단계들을 통해 EC2 인스턴스로 서버 실행에 필요한 파일들이 모두 옮겨졌다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에 미리 작성해둔 deploy.sh 파일로 pm2 서버를 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;~/deploy/deploy.sh&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1754234538232&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash

set -e

APP_DIR=~/app/nest-app
DEPLOY_DIR=~/deploy
ARCHIVE=$DEPLOY_DIR/nest-app.tar.gz

echo &quot;[1/3] 아카이브 해제 중...&quot;

ENV_BACKUP=&quot;$APP_DIR/.env&quot;

if [ -f &quot;$ENV_BACKUP&quot; ]; then
  cp &quot;$ENV_BACKUP&quot; /tmp/.env.bak
fi

rm -rf $APP_DIR
mkdir -p $APP_DIR
tar -xzf $ARCHIVE -C $APP_DIR

# 복원
if [ -f /tmp/.env.bak ]; then
  mv /tmp/.env.bak &quot;$APP_DIR/.env&quot;
fi

cd $APP_DIR

echo &quot;[2/3] PM2로 앱 시작 또는 재시작...&quot;
if pm2 describe nest-app &amp;gt; /dev/null; then
  pm2 restart nest-app
else
  pm2 start dist/main.js --name nest-app
fi

echo &quot;[3/3] 배포 완료!&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;deploy.sh의 내용은 압축을 해제하고, pm2로 서버를 실행하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 여기서 주의할 점은 .env 파일은 미리 해당 디렉토리 내에 저장되어 있어야 하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제되면 안되기 때문에 .env 파일이 삭제되지 않도록 예외처리 해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또, PM2는 인스턴스 내에 글로벌 설치 되어 있어야 한다. (npm install -g pm2)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;deploy.sh가 잘 실행되면 서버가 구동된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 마지막으로 EC2 보안 그룹 허용 ip에 추가되었던 github actions 서버 ip를 제거해주면 끝이다.&lt;/p&gt;
&lt;pre id=&quot;code_1754234744663&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;      - name: Remove GitHub Actions IP from Security Group
        if: always()
        run: |
          aws ec2 revoke-security-group-ingress \
            --group-id ${{ secrets.SECURITY_GROUP_ID }} \
            --protocol tcp \
            --port 22 \
            --cidr ${{ steps.ip.outputs.ipv4 }}/32&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 팁/Git &amp;amp; Github</category>
      <author>왈왈디</author>
      <guid isPermaLink="true">https://walwaldev.tistory.com/177</guid>
      <comments>https://walwaldev.tistory.com/177#entry177comment</comments>
      <pubDate>Sun, 3 Aug 2025 23:52:08 +0900</pubDate>
    </item>
    <item>
      <title>MySQL 복제(레플리카) 설정하기</title>
      <link>https://walwaldev.tistory.com/176</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3Zuki/btsPpiY5UyU/mtf0FIQeaB3SEA8A9O81s0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3Zuki/btsPpiY5UyU/mtf0FIQeaB3SEA8A9O81s0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3Zuki/btsPpiY5UyU/mtf0FIQeaB3SEA8A9O81s0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3Zuki%2FbtsPpiY5UyU%2Fmtf0FIQeaB3SEA8A9O81s0%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;397&quot; height=&quot;397&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복제란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복제는 한 서버에서 다른 서버로 데이터가 동기화되는 것을 말한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;소스(Source) 서버&lt;/b&gt;: 원본 데이터를 가진 서버&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레플리카(Replica) 서버&lt;/b&gt;: 복제된 데이터를 가지는 서버&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스 서버에서 데이터 및 스키마에 대한 변경이 최초로 발생한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레플리카 서버에서는 이러한 변경 내역을 소스 서버로부터 전달 받아 &lt;br /&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;복제의 목적은 주로 다음 4가지이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스케일 아웃(Scale-out)&lt;/li&gt;
&lt;li&gt;데이터 백업&lt;/li&gt;
&lt;li&gt;레플리카 서버를 데이터 분석용 서버로 사용&lt;/li&gt;
&lt;li&gt;데이터의 지리적 분산&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복제 아키텍처&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복제 아키텍처를 이해하기 위해서두 가지 로그를 알아두어야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;바이너리 로그(Binary Log)&lt;/b&gt;: MySQL 서버에서 발생하는 모든 변경 사항을 순서대로 기록한다. &lt;br /&gt;데이터의 변경 내역 뿐만 아니라, 데이터베이스나 테이블의 구조 변경과 계정이나 권한의 변경 정보까지 모두 저장된다.&lt;br /&gt;복제는 이 바이너리 로그를 기반으로 구현됐다. 소스 서버에서 생서된 바이너리 로그가 레플리카 서버로 전송되고 레플리카 서버에서는 해당 내용을 로컬 디스크에 저장한 뒤 자신이 가진 데이터에 반영함으로써 동기화가 이루어진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;릴레이 로그(Relay Log)&lt;/b&gt;: 레플리카 서버에서 소스 서버의 바이너리 로그를 읽어 로컬 디스크에 저장해둔 파일이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 복제는 세 개의 스레드에 의해 작동한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;바이너리 로그 덤프 스레드(Binary Log Dump Thread)&lt;/b&gt;: 레플리카 서버가 소스 서버에 접속해 바이너리 로그 정보를 요청한다.&lt;br /&gt;소스 서버에서 바이너리 로그 덤프 스레드를 생성해서 바이너리 로그의 내용을 레플리카 서버로 전송한다.&lt;br /&gt;각 이벤트를 읽을 때 일시적으로 바이너리 로그에 잠금을 수행하며, 이벤트를 읽고난 후에는 바로 잠금을 해제한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레플리케이션 I/O 스레드(Replication I/O Thread)&lt;/b&gt;: 레플리카 서버에서 소스 서버의 바이너리 로그 이벤트를 가져와 로컬 서버의 파일(릴레이 로그)로 저장하는 역할이다. &lt;br /&gt;복제가 시작되면 레플리카 서버는 I/O 스레드를 생성하고, 복제가 멈추면 종료된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레플리케이션 SQL 스레드(Replication SQL Thread)&lt;/b&gt;: 레플리카 서버에서 작성된 릴레이 로그 파일의 이벤트들을 읽고 실행한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복제가 시작되면 레플리카 서버는 총 세 가지 유형의 복제 관련 데이터를 생성하고 관리한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;릴레이 로그(Relay Log)&lt;/b&gt;: 레플리케이션 I/O 스레드에 의해 작성되는 파일로, 소스 서버의 바이너리 로그에서 읽어온 이벤트(트랜잭션) 정보가 저장된다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커넥션 메타데이터(Connection Metadata)&lt;/b&gt;: 레플리케이션 I/O 스레드에서 소스 서버에 연결할 때 사용하는 DB 계정 정보 및 현재 읽고 있는 소스 서버의 바이너리 파일명과 파일 내 위치 값등이 담겨 있다.&lt;br /&gt;mysql.slave_master_info 테이블에 저장된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;어플라이어 메타데이터(Applier Metadata)&lt;/b&gt;: 레플리케이션 SQL 스레드에서 릴레이 로그에 저장된 소스 서버의 이벤트들을 레플리카 서버에 적용(Relay)하는 컴포넌트를 어플라이어(Applier)라고 한다.&amp;nbsp;&lt;br /&gt;최근 적용된 이벤트에 대해 해당 이벤트가 저장돼 있는 릴레이 로그 파일명과 파일 내 위치 정보 등을 담고 있고, 이 정보들을 바탕으로 레플리카 서버에 나머지 이벤트들을 적용한다.&lt;br /&gt;mysql.slave_relay_log_info 테이블에 저장된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복제 타입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복제는 소스 서버의 바이너리 로그에 기록된 변경 내역들을 식별하는 방식에 따라 두가지로 나뉜다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;바이너리 로그 파일 위치 기반 복제&lt;/li&gt;
&lt;li&gt;글로벌 트랜잭션 ID 기반 복제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 바이너리 로그 파일 위치 기반 복제에 대해서만 살펴보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;바이너리 로그 파일 위치 기반 복제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복제 기능이 처음 도입됐을 때부터 제공된 방식으로,&lt;br /&gt;레플리카 서버에서 소스 서버의 바이너리 로그 파일명과 파일 내에서의 위치(Offset 또는 Position)로&lt;br /&gt;개별 바이너리 로그 이벤트를 식별해서 복제가 진행되는 형태이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복제를 처음 구축할 때 레플리카 서버에 &lt;b&gt;소스 서버의 어떤 이벤트부터 동기화를 수행할 것인가&lt;/b&gt;에 대한 정보를 설정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레플리카 서버는 소스 서버의 어느 이벤트까지 로컬 디스크로 가져왔고 또 적용했는지에 대한 정보를 관리하며&lt;br /&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;br /&gt;바이너리 로그 파일 위치 기반 복제 방식에서는 파일명과 파일 내에서의 위치 값(File Offset)의 조합으로 식별한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 중요한 점은 복제에 참여한 MySQL 서버들이 모두 &lt;b&gt;고유한 server_id 값&lt;/b&gt;을 가지고 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;server_id는 시스템 변수 중 하나로, 사용자가 설정할 수 있으며 기본값은 1이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복제 구축&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이너리 로그 파일 위치 기반의 복제 구축 방식에 대해 알아보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 설정 준비&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복제를 사용하려면 소스 서버에서 반드시 &lt;b&gt;바이너리 로그가 활성화&lt;/b&gt;돼 있어야 하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복제 구성원이 되는 각 MySQL 서버가 고유한 server_id를 가져야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0에서는 바이너리 로그가 기본적으로 활성화돼 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이너리 로그 파일 위치나 파일명을 따로 설정하고 싶다면 log_bin 시스템 변수를 통해 원하는 값으로 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기화 방식이나, 캐시를 위한 메모리 크기, 파일 크기, 보관 주기 등도 지정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령으로 소스 서버에서 바이너리 로그가 정상적으로 기록되고 있는지 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1753009290196&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SHOW MASTER STATUS;&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;b&gt;log_slave_updates&lt;/b&gt; 시스템 변수를 명시하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 레플리카 서버는 복제에 의한 데이터 변경 사항은 자신의 바이너리 로그에 기록하지 않는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;log_slave_updates 시스템 변수를 설정하면 복제에 의한 데이터 변경 내용도 자신의 바이너리 로그에 기록하게 된다.&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;2. 복제용 계정 준비&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레플리카 서버가 사용할 복제용 계정이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복제에 사용되는 권한만 주어진 별도의 계정을 생성해 사용하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 계정은 반드시 &lt;b&gt;REPLICATION SLAVE&lt;/b&gt; 권한을 가지고 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 데이터 복사&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복제를 시작하기 전에 소스 서버의 데이터를 레플리카 서버로 가져와서 적재해 두어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 엔터프라이즈 백업이나 mysqldump 같은 툴을 이용해서 데이터를 복사하면 된다.&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;4. 복제 시작&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 복사까지 완료되면 복제를 시작하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mysqldump를 이용해 소스 서버의 데이터를 백업 받아 11:20 쯤에 레플리카 서버에 모두 적재된 경우,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복제를 시작하는 시점이 11:45이라면 &lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;레플리카 서버 &lt;/span&gt;데이터가&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;11:20 부터 11:45까지의&lt;span&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: #333333; 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;b&gt;CHANGE REPLICATION SOURCE TO (또는 CHANGE MASTER TO)&lt;/b&gt; 명령으로 mysqldump로 백업 받은 파일의 헤더 부분에서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 명령어를 참조할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략 24번째 줄에 있는 &quot;CHANGE MASTER&quot;로 시작하는 줄만 텍스트 편집기에 복사해둔다.&lt;/p&gt;
&lt;pre id=&quot;code_1753010698547&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;linux&amp;gt; less /tmp/source_data.sql
...

-- Position to start replication or point-in-time recovery from
--

-- CHANGE MATSER TO MASTER_LOG_FILE='binary-log.000002', MASTER_LOG_POS=2708;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 편집기에 복사해 둔 내용에&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소스 서버 MySQL 서버의 호스트명&lt;/li&gt;
&lt;li&gt;포트&lt;/li&gt;
&lt;li&gt;복제용 사용자 계정&lt;/li&gt;
&lt;li&gt;비밀 번호&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등을 다음과 같이 추가해 복제 설정 명령을 준비한다.&lt;/p&gt;
&lt;pre id=&quot;code_1753010961412&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- // MySQL 8.0.23 이상 버전
CHANGE REPLICATION SOURCE TO
	SOURCE_HOST='source_server_host',
    SOURCE_PORT=3306,
    SOURCE_USER='repl_user',
    SOURCE_PASSWORD='repl_user_password',
    SOURCE_LOG_FILE='binary-log.000002',
    SOURCE_LOG_POS=2708,
    GET_SOURCE_PUBLIC_KEY=1;
    
-- // MySQL 8.0.23 미만 버전
CHANGE MASTER TO
	MASTER_HOST='source_server_host',
    MASTER_POST=3306,
    MASTER_USER='repl_user',
    MASTER_PASSWORD='repl_user_password',
    MASTER_LOG_FILE='binary-log.0000002',
    MATSER_LOG_POS=2708,
    GET_MASTER_PUBLIC_KEY=1;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SOURCE_HOST&lt;/b&gt;는 소스 서버를 의미하고 소스 서버의 IP 혹은 도메인 정보를 넣으면 도니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GET_SOURCE_PUBLIC_KEY&lt;/b&gt;는 RSA 키 기반 비밀번호 교환 방식의 통신을 위해 공개키(Public Key)를 소스 서버에 요청할 것인지 여부를 나타낸다. 에러가 발생할 수 있으므로 반드시 설정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령을 그대로 레플리카 서버의 MySQL에 로그인해서 실행한 뒤 아래 명령을 실행해보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복제 관련 정보가 레플리카 서버 MySQL에 등록돼 있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1753011117735&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SHOW REPLICA STATUS;
-- 또는 SHOW SLAVE STATUS&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;b&gt;Replica_IO_Running&lt;/b&gt;, &lt;b&gt;Replica_SQL_Running&lt;/b&gt; 컬럼값이 &quot;No&quot; 로 되어있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 복제 관련 정보 등록만 된 것이지 동기화가 시작되지 않았음을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서 아래 명령을 실행하면 두 값이 &quot;Yes&quot;로 바뀌면서 &lt;br /&gt;동기화가 지연된 기간의 변경 사항들을 소스 서버로부터 가져와 적용하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1753011212277&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;START REPLICA;
-- 또는 START SLAVE&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;br /&gt;에러가 발생해 복제가 멈추는 현상이 발생하기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분은 사용자의 실수로 인한 경우가 많고, 대표적인 에러가 &lt;b&gt;중복 키 에러&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이너리 로그 위치 기반 복제에서는 &lt;b&gt;sql_slave_skip_counter&lt;/b&gt; 시스템 변수를 이용해 문제되는 트랜잭션을 건너뛸 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 복제를 중단한 후 sql_slave_skip_counter 변수의 값을 1로 지정해 레플리케이션 SQL 스레드를 재시작하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레플리카 서버는 에러가 발생한 INSERT 쿼리를 건너뛰고 정상적으로 복제를 재개하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1753011566070&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;STOP SLAVE SQL THREAD;
SET GLOBAL sql_slave_skip_counter=1;
START SLAVE SQL_THREAD;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고 자료&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Real MySLQ 8.0 2권 - 16. 복제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DataBase/MySQL</category>
      <author>왈왈디</author>
      <guid isPermaLink="true">https://walwaldev.tistory.com/176</guid>
      <comments>https://walwaldev.tistory.com/176#entry176comment</comments>
      <pubDate>Sun, 20 Jul 2025 20:42:37 +0900</pubDate>
    </item>
  </channel>
</rss>