<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>ProbeHub</title>
    <link>https://probehub.tistory.com/</link>
    <description>가볍게, 오랫동안 기록하고 싶은 블로그입니다.</description>
    <language>ko</language>
    <pubDate>Fri, 10 Apr 2026 18:18:38 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>개발프로브</managingEditor>
    <image>
      <title>ProbeHub</title>
      <url>https://tistory1.daumcdn.net/tistory/6907305/attach/4f468594215e43e282734257fec5ce6c</url>
      <link>https://probehub.tistory.com</link>
    </image>
    <item>
      <title>Django WSGI 최적화 - Granian 과 ARM64(Graviton)</title>
      <link>https://probehub.tistory.com/119</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 최근 Django WSGI 를 다루게 된 백엔드 개발자가 문제를 인식하는 과정, 실험을 통해 데이터를 확보하고 실제 성능과 비용을 최적화한 경험을 다룹니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TL;DR&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 style=&quot;color: #333333; text-align: start;&quot;&gt;linux/amd64 를 linux/arm64&lt;span&gt;&amp;nbsp;로 교체하여 Req/s 지표를 2배 높일 수 있었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Gunicorn, gthread 기반 HTTP 서버를 Granian 으로 교체하여 부하 상황에서 발생하던 502 에러를 방지했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 문제 정의 및 가설 수립&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;왜 우리 서비스만 Task가 70%나 더 많을까?&quot; AWS 콘솔을 모니터링하던 중, 타 프로덕트 대비 비정상적으로 높은 ECS Task 수와 일일 최대 6만 건에 달하는 502 Bad Gateway 에러를 발견했습니다. 분석 결과, Python Worker(WSGI)의 부하로 인한 CPU Spike가 오토스케일링(Horizontal Scaling)과 에러의 주원인임을 확인했습니다.&lt;/p&gt;
&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;HTTP Server(WSGI)의 세팅 최적화 또는 교체가 CPU 효율을 높이고 비용 및 에러율을 낮출 것이라는 가설&lt;/b&gt;을 세웠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 실험 설계&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 원인을 명확히 격리하고 최적값을 찾기 위해 변인을 통제하여 총 4단계의 실험을 설계했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.1 변수 설정&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;width: 731px; height: 169px; border-collapse: collapse;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 138px; height: 19px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 165px; height: 19px;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 428px; height: 19px;&quot;&gt;&lt;b&gt;내용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 138px; height: 74px;&quot; rowspan=&quot;2&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;통제 변수(고정값)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 165px; height: 19px;&quot;&gt;하드웨어 스펙&lt;/td&gt;
&lt;td style=&quot;width: 428px; height: 19px;&quot;&gt;동일한 CPU 및 RAM 할당&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 55px;&quot;&gt;
&lt;td style=&quot;width: 165px; height: 55px;&quot;&gt;부하 테스트 시나리오&lt;/td&gt;
&lt;td style=&quot;width: 428px; height: 55px;&quot;&gt;&amp;bull; 단기 부하: 100 Users, 1분 (3개 Endpoint: Fast/Mid/Slow)&lt;br /&gt;&amp;bull; 중기 부하: 100 Users, 10분 (지속성 검증)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 138px; height: 57px;&quot; rowspan=&quot;3&quot;&gt;독립 변수(변경값)&lt;/td&gt;
&lt;td style=&quot;width: 165px; height: 19px;&quot;&gt;워커 구성&lt;/td&gt;
&lt;td style=&quot;width: 428px; height: 19px;&quot;&gt;Worker 개수, Thread 개수 조정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 165px; height: 19px;&quot;&gt;서버 엔진&lt;/td&gt;
&lt;td style=&quot;width: 428px; height: 19px;&quot;&gt;Gunicorn(기존) vs Granian(신규)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 165px; height: 19px;&quot;&gt;운영체제&lt;/td&gt;
&lt;td style=&quot;width: 428px; height: 19px;&quot;&gt;Linux OS 종류 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 138px; height: 19px;&quot;&gt;종속 변수(측정값)&lt;/td&gt;
&lt;td style=&quot;width: 165px; height: 19px;&quot;&gt;성능 지표&lt;/td&gt;
&lt;td style=&quot;width: 428px; height: 19px;&quot;&gt;CPU Spike, RPS(Req/s), Error Rate, Task Count&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;왜 Granian 인가?&lt;/b&gt;&lt;br /&gt;Granian 은 Rust 를 기반으로하는 Python HTTP Server 입니다.&lt;br /&gt;&lt;a href=&quot;https://github.com/emmett-framework/granian/blob/master/benchmarks/vs.md&quot;&gt;그들이 제시하는 성능 벤치마크&lt;/a&gt;에 따르면 WSGI 에서 gunicorn 대비 20배 빠른 성능을 보인다고 합니다.&lt;br /&gt;또한 개인적으로는 Python 이 아니기 때문에 GC 로 인한 jitter 현상이 없을 것으로 기대도 됩니다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.2 실험 단계&lt;/b&gt;&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;Worker/Thread 튜닝: 기존 환경에서 파라미터 튜닝만으로 개선 가능한지 확인 (단기 부하)&lt;/li&gt;
&lt;li&gt;HTTP Server 교체: Gunicorn을 대체할 서버 테스트 (단기 부하)&lt;/li&gt;
&lt;li&gt;OS 변경: 운영체제 레벨의 오버헤드 확인 (단기 부하)&lt;/li&gt;
&lt;li&gt;최종 검증: 도출된 최적 조합으로 장시간 안정성 테스트 (중기 부하)&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 실험 결과 데이터 및 해석&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 4단계의 실험을 통해 &lt;b&gt;Worker 튜닝, HTTP Server 교체, 그리고 아키텍처 변경&lt;/b&gt;이 성능에 미치는 영향을 분석했습니다. 모든 데이터는 동일한 하드웨어 스펙에서 측정되었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.1&amp;nbsp; Gunicorn 워커/스레드 튜닝 (단기 부하)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 기존 Gunicorn 환경에서 파라미터 튜닝만으로 개선이 가능한지 확인했습니다. CPU 경합(Context Switching)을 줄이기 위해 워커와 스레드 수를 줄이는 접근을 시도했습니다.&lt;/p&gt;
&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;기존 Worker / Thread 세팅&lt;/b&gt;은 &lt;b&gt;&lt;a href=&quot;https://docs.gunicorn.org/en/latest/design.html#how-many-workers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Gunicorn 공식문서에서 추천하는 규칙&lt;/a&gt;&lt;/b&gt;을 따르고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;Generally we recommend&lt;/span&gt;&lt;span&gt;(2&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt;$num_cores)&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;as the number of workers to start off with&quot; &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;이 규칙을 해석해서 대부분의 국내 Python 개발자들은 아래의 세팅을 따르는 중입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764480530793&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import multiprocessing
workers = multiprocessing.cpu_count() * 2 + 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;&lt;b&gt;신규 Worker / Thread 세팅&lt;/b&gt;은 vCPU 개수와 Worker 개수를 일치시키고, Thread 개수는 Worker * 2 를 따르도록 했습니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1.1 실험 데이터&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;height: 77px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot; align=&quot;left&quot;&gt;구분&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot; align=&quot;left&quot;&gt;설정 (Worker / Thread)&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot; align=&quot;left&quot;&gt;RPS (Req/s)&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot; align=&quot;left&quot;&gt;CPU Usage&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot; align=&quot;left&quot;&gt;Error Rate&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot; align=&quot;left&quot;&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;대조군&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;Gunicorn (기존 세팅)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;35.6&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;71.9%&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;3.33%&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;기존 운영 세팅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;실험군&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;Gunicorn (신규 세팅)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;46.4&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;63.6%&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;0.76%&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;최적 효율&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1.2 해석, 클라우드 환경의 vCPU 가 갖는 특수성&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;워커와 스레드 개수를 줄였음에도 불구하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RPS는 약 30% 증가(35.6 &amp;rarr; 46.4)&lt;/b&gt;하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;에러율은 1/4 수준(3.33% &amp;rarr; 0.76%)&lt;/b&gt;으로 대폭 감소했습니다. 이는 과도한 Thread 생성으로 인한 Context Switching 오버헤드가 성능 저하의 주원인이었음을 시사합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이상합니다. 어째서 Gunicorn 공식문서 추천 세팅이 더 비효율적인걸까요? 공식문서의 &lt;b&gt;&quot;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;&lt;b&gt;Obviously, your particular hardware and application are going to affect the optimal number of workers.&quot;&lt;/b&gt; 부분에 더 주의를 기울여야 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;현재 Django Application 은 Bear Metal 에서 돌아가는게 아니라 Docker 에서, 그것도 AWS ECS Fargate 의 vCPU 라는 가상의 CPU 를 이용하고 있습니다. 이런 특수한 상황을 고려한 실험이 필요했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;다행입니다. 첫번째 실험으로 상당한 인사이트를 얻었습니다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: justify;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.2&amp;nbsp; HTTP Server 교체 (Gunicorn vs Granian)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gunicorn 최적화(실험군 A) 상태에서도 간헐적인 에러(0.76%)가 잔존했습니다. 이를 근본적으로 해결하기 위해 Rust 기반의 &lt;b&gt;Granian&lt;/b&gt;을 도입하여 비교 실험을 진행했습니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.2.1 실험 데이터&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;height: 77px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot; align=&quot;left&quot;&gt;서버&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot; align=&quot;left&quot;&gt;설정 (Worker / Thread)&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot; align=&quot;left&quot;&gt;RPS (Req/s)&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot; align=&quot;left&quot;&gt;CPU Usage&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot; align=&quot;left&quot;&gt;Error Rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;Gunicorn&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;(기존 세팅)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;46.4&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;63.6%&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;0.76%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;Granian&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;(신규 세팅)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;40.2&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;69.3%&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;0.00%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;Granian&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;(기존 세팅)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;41.8&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;92.1%&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot; align=&quot;left&quot;&gt;0.00%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.2.2 해석, Python GIL 의 제약에 따른 502 에러&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Granian은 Gunicorn 대비 RPS가 소폭 낮거나 비슷했지만, 가장 중요한 지표인 &lt;b&gt;에러율이 0% (Zero Error)&lt;/b&gt;를 기록했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 502 에러가 바생하는 매커니즘&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;HTTP 요청이 들어오면 OS 레벨에서는 다음과 같은 과정이 일어납니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;sync-queue-and-accept-queue.webp&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;946&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKCaza/dJMcagRuVhC/tC1gkEne1KqufMkKfni1a0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKCaza/dJMcagRuVhC/tC1gkEne1KqufMkKfni1a0/img.webp&quot; data-alt=&quot;sync_queue, accept_queue 구조(https://blog.cloudflare.com/syn-packet-handling-in-the-wild/#the-tale-of-two-queues)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKCaza/dJMcagRuVhC/tC1gkEne1KqufMkKfni1a0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKCaza%2FdJMcagRuVhC%2FtC1gkEne1KqufMkKfni1a0%2Fimg.webp&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;560&quot; height=&quot;338&quot; data-filename=&quot;sync-queue-and-accept-queue.webp&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;946&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;sync_queue, accept_queue 구조(https://blog.cloudflare.com/syn-packet-handling-in-the-wild/#the-tale-of-two-queues)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;12&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;SYN Backlog:&lt;/b&gt; 클라이언트의 요청(SYN)이 들어오면 커널 큐에 쌓입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Accept Queue:&lt;/b&gt; TCP 3-way Handshake가 완료되면 연결이 성립되어 Accept Queue로 이동합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Application Accept:&lt;/b&gt; 애플리케이션(Gunicorn 등)이 accept() 시스템 콜을 통해 큐에서 연결을 가져가야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;이 과정에서 &lt;b&gt;문제의 핵심&lt;/b&gt;은 애플리케이션이 바빠서 Accept Queue를 제때 비우지 못하면 큐가 가득 차게 되고, 커널은 들어오는 연결을 거부(RST)하는 현상을 서버 앞단의 ALB가 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&quot;서버가 죽었다&quot;고 판단하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;502 Bad Gateway&lt;/b&gt;를 반환하는 것으로 보입니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) Gunicorn 의 한계: Python 의 GIL 과 Accept Loop&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;Gunicorn, 특히 gthread 모드의 치명적인 단점은 연결 수락 과정이 &lt;b&gt;Python 런타임 내부&lt;/b&gt;에 갇혀 있다는 점입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;16&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;동기적 Accept:&lt;/b&gt; Gunicorn 워커는 Python 루프 안에서 accept()를 호출합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GIL 병목 (Bottleneck):&lt;/b&gt; 멀티 스레드를 써도 Python의 &lt;b&gt;GIL &lt;/b&gt;때문에 한 번에 하나의 스레드만 실행됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기아 상태 (Starvation):&lt;/b&gt; 요청이 폭주하면 워커 스레드들은 비즈니스 로직 처리와 I/O 대기로 바빠집니다. 이 와중에 GIL 획득 경쟁에서 밀리면 accept()를 호출할 기회조차 얻지 못합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 영향으로 &lt;b&gt;CPU가 남아도는데도(20~60% 구간), accept()를 못 해서 큐가 터지고 502 에러가 발생&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;3) Grarnian 의 한계 돌파: GIL-Free Accept&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;Granian이 에러율 0%를 달성한 비결은 &lt;b&gt;연결 수락과 HTTP 파싱을 Python 외부(Rust)로 끄집어낸 데&lt;/b&gt; 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;19&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Rust 기반 런타임:&lt;/b&gt; Granian은 내부적으로 Rust 기반의 고성능 네트워크 라이브러리인 Hyper와 Tokio를 사용합니다. Python 로직이 아무리 바빠도, Rust 레이어는 GIL의 영향을 받지 않고 즉각적으로 연결을 수락(Accept)하여 내부 큐에 안전하게 쌓아둡니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지연된 위임:&lt;/b&gt; 연결이 안전하게 확보된 후에야 Python 비동기 태스크로 변환하여 애플리케이션에 전달합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;Granian 환경에서는 Python 처리가 늦어져 &quot;응답 지연&quot;이 발생할 수는 있어도, 연결 자체가 거부되어 &lt;b&gt;&quot;502 에러&quot;가 발생하는 상황은 원천적으로 차단&lt;/b&gt;되는 것으로 보입니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.3 아키텍처 변경 (amd64/X86 vs arm64/Graviton)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용 효율과 기본 성능 향상을 위해 AWS Fargate의 아키텍처를 기존 Intel(x86) 기반에서 &lt;b&gt;arm64(Graviton)&lt;/b&gt;로 변경하여 테스트했습니다. arm64 는 클라우드 환경에서 더 비용효율적이며 성능도 더 좋다고 알려져 있습니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.3.1 실험 데이터&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아키텍처서버 / 설정RPS (Req/s)CPU UsageError Rate성능 향상&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 90.6977%; height: 77px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 12.2093%; height: 19px;&quot;&gt;&lt;b&gt;아키텍처&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.7442%; height: 19px;&quot;&gt;&lt;b&gt;서버 / 설정&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.093%; height: 19px;&quot;&gt;&lt;b&gt;RPS (Req/s)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.186%; height: 19px;&quot;&gt;&lt;b&gt;CPU Usage&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.7209%; height: 19px;&quot;&gt;&lt;b&gt;Error Rate&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.3953%; height: 19px;&quot;&gt;&lt;b&gt;성능 향상&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 12.2093%;&quot;&gt;amd64&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 21.7442%;&quot;&gt;Gunicorn(신규 세팅)&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 17.093%;&quot;&gt;46.4&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 14.186%;&quot;&gt;63.6%&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 13.7209%;&quot;&gt;0.76%&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 11.3953%;&quot;&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 12.2093%;&quot;&gt;arm64&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 21.7442%;&quot;&gt;Gunicorn (신규 세팅)&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 17.093%;&quot;&gt;67.6&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 14.186%;&quot;&gt;80.6%&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 13.7209%;&quot;&gt;0.40%&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 11.3953%;&quot;&gt;+45%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px; width: 12.2093%;&quot;&gt;&lt;b&gt;arm64&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 21.7442%;&quot;&gt;&lt;b&gt;Granian (신규 세팅)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 17.093%;&quot;&gt;&lt;b&gt;67.7&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 14.186%;&quot;&gt;&lt;b&gt;59.1%&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 13.7209%;&quot;&gt;&lt;b&gt;0.00%&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 11.3953%;&quot;&gt;&lt;b&gt;+46%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;도출된 최적의 조합(Arm64 + Granian)이 장시간 부하에서도 안정성을 유지하는지 검증했습니다. (10분 테스트)&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 87.5581%; height: 57px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 21.3953%; height: 19px;&quot;&gt;대상&lt;/td&gt;
&lt;td style=&quot;width: 19.3024%; height: 19px;&quot;&gt;RPS (Req/s)&lt;/td&gt;
&lt;td style=&quot;width: 21.0465%; height: 19px;&quot;&gt;CPU Usage&lt;/td&gt;
&lt;td style=&quot;width: 17.8661%; height: 19px;&quot;&gt;Eror Rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 21.3953%; height: 19px;&quot;&gt;Gunicorn (최적값)&lt;/td&gt;
&lt;td style=&quot;width: 19.3024%; height: 19px;&quot;&gt;69.1&lt;/td&gt;
&lt;td style=&quot;width: 21.0465%; height: 19px;&quot;&gt;87.9%&lt;/td&gt;
&lt;td style=&quot;width: 17.8661%; height: 19px;&quot;&gt;0.67%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 21.3953%; height: 19px;&quot;&gt;&lt;b&gt;Granian (최적값)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.3024%; height: 19px;&quot;&gt;&lt;b&gt;66.2&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 21.0465%; height: 19px;&quot;&gt;&lt;b&gt;86.0%&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.8661%; height: 19px;&quot;&gt;&lt;b&gt;0.00%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;br /&gt;3.3.2 해석, vCPU 하이퍼스레딩 vs 물리 코어&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아키텍처 변경만으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RPS가 40% 이상 폭발적으로 증가&lt;/b&gt;했습니다. 특히 Arm64 환경에서 Granian은 더 적은 CPU 자원(59.1%)을 사용하면서도 최고 수준의 처리량을 보여주었습니다. Python과 Rust 모두 Arm 아키텍처에서 효율적인 퍼포먼스를 보였습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10분간의 지속적인 부하 상황에서 Gunicorn은 여전히 0.67%의 502 에러가 발생했으나,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Granian은 단 한 건의 에러도 발생시키지 않았습니다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이를 통해 '&lt;b&gt;arm64 + Granian' 조합이 성능과 안정성 두 마리 토끼를 잡는 최적의 솔루션임을 확신&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;왜 CPU 를 변경하는 것 만으로 이렇게 큰 성능 향상이 나타났을까요? 이는 amd64 대비 arm64 CPU 의 메모리 대역폭이 더 큰 것 등 다양한 영향이 있겠지만 amd64 와&amp;nbsp; arm64 에서 vCPU 의 실체가 다르기 때문에 나타난 현상입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;amd64 에서는 vCPU 두개가 하나의 물리 CPU 코어를 공유하지만 arm64에서의 vCPU와 물리 CPU 코어가 1대1로 매핑됩니다.&lt;/b&gt; 이러한 특징은 Python Applicaiton 에서 더 크리티컬하게 작용했을 것으로 보이는데요, GIL 제약으로 실제 활용 가능한 CPU 코어개수가 요청 처리량에 직접적으로 작용하기 때문입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gunicorn 공식문서: &lt;a href=&quot;https://docs.gunicorn.org/en/latest/design.html#how-many-workers&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.gunicorn.org/en/latest/design.html#how-many-workers&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;garnian 벤치마크: &lt;a href=&quot;https://github.com/emmett-framework/granian/blob/master/benchmarks/vs.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/emmett-framework/granian/blob/master/benchmarks/vs.md&lt;/a&gt;&amp;nbsp;&lt;br /&gt;cloudflare SYN packet: &lt;a href=&quot;https://blog.cloudflare.com/syn-packet-handling-in-the-wild/#the-tale-of-two-queues&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.cloudflare.com/syn-packet-handling-in-the-wild/#the-tale-of-two-queues&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>탐구 생활/개발 탐구</category>
      <category>django 성능 개선</category>
      <category>granian</category>
      <category>python amd64</category>
      <category>python arn64</category>
      <category>python graviton</category>
      <category>python 성능 개선</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/119</guid>
      <comments>https://probehub.tistory.com/119#entry119comment</comments>
      <pubDate>Tue, 2 Dec 2025 22:43:48 +0900</pubDate>
    </item>
    <item>
      <title>ORM의 편리함 속 함정 - SELECT *와 수직 분할의 중요성</title>
      <link>https://probehub.tistory.com/117</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현대의 백엔드 개발자들(심지어 DB를 다루는 프론트엔드 개발까지)은 기본적으로&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;ORM(Object-Relational Mapper) 사용을 널리 받아들이고 있습니다. Python 생태계에서도 금쪽같은 Django의 ORM, 혹은 SQLAlchemy 가 있어서 Python 개발자들에게 정말 많은 편리함을 제공하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;하지만 &quot;지옥으로 가는 길은 선의로 포장되어 있다&quot; 라는 유명한 격언처럼, ORM 이 제공하는 편리함 속에는 함정이 숨어있습니다. 대표적인 문제인 N+1 현상은 널리 알려져있는 반면, 무심코 날리고 있는 &lt;b&gt;SELECT * 쿼리&lt;/b&gt;와 &lt;b&gt;Fat Model 문제&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;성능에 집착하는 개발자 중 한명으로서 ORM 의 또다른 함정을 명확히 정리하고 해결하는 방법을 정리하는 글이 필요하다고 생각했고, 이러한 이유로 이 글을 씁니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;SELECT * 를 쓰면 왜 안좋은가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 말은 왜 더 적은 칼럼을 가져올 수록 성능이 좋아지는가?&lt;/b&gt;와 같은 말입니다. 그리고 ORM 을 어느정도 사용해본 개발자라면 &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;우선 SELECT * 가 아니라 SELECT {some_columns} 를 사용하면 세 개의 서로 다른 구간에서 성능 개선이 일어납니다. 네트워크 영역과 백엔드 서버 그리고 데이터베이스 서버입니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 네트워크에서의 성능 개선&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;네트워크 레벨에서의 성능 개선도 쉽게 짐작할 수 있습니다. 메모리 레벨에서 Applicaiton 서버와 Database 서버간 데이터 교환이 일어나는 것이 아니라면 대부분 네트워크를 거치게 됩니다. 더 적은 칼럼을 조회하면 당연히 네트워크 트래픽도 줄어들기 때문에 성능 개선이 일어나는 것이죠.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 백엔드 서버에서의 성능 개선&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ORM 을 사용하는 백엔드 서버에서는 어떤 성능 개선이 일어날까요? 생각보다 아주 간단한데요. 바로 &lt;b&gt;메모리 사용 최적화&lt;/b&gt;입니다. 일반적으로 Value Object 를 만드는것보다 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ORM Object&lt;span&gt;&amp;nbsp;를 만드는 것이&lt;/span&gt;&lt;/span&gt; 더 많은 오버헤드를 발생시킵니다. 즉, 특정 필드만 가져오도록 해서 ORM Object 가 아니라 Value Object 를 메모리에 적재하게 된다면 그 만큼 성능 최적화가 일어난다는 것이죠. 이러한 성능 최적화는 여러 요청을 동시에 처리하는 백엔드 서버가 대량의 데이터를 한번에 많이 가져와야하는 경우 더욱 두드러지게 나타납니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 데이터베이스 서버에서의 성능 개선&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스에서 성능 개선이 일어나는 부분은 백엔드 서버보다 조금 더 깊은 이해를 필요로 합니다. 하지만 이 글을 이해하기 위해 RDBMS 의 구조를 다 뜯어보고 그 설계철학을 모두 이해하기 위해 노력할 필요는 없습니다. 필요한 개념만 간단히 정리하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;rdbms_storage_model.png&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;1526&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhy9TX/dJMcadG6ypz/Sq1lSOW1mUpvPlyWXFUG20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhy9TX/dJMcadG6ypz/Sq1lSOW1mUpvPlyWXFUG20/img.png&quot; data-alt=&quot;RDBMS 에서 데이터를 저장하고 읽는 방식&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhy9TX/dJMcadG6ypz/Sq1lSOW1mUpvPlyWXFUG20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhy9TX%2FdJMcadG6ypz%2FSq1lSOW1mUpvPlyWXFUG20%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;560&quot; height=&quot;558&quot; data-filename=&quot;rdbms_storage_model.png&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;1526&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;RDBMS 에서 데이터를 저장하고 읽는 방식&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터베이스에 캐시가 존재합니다.&lt;/b&gt; '버퍼 풀', '공유 버퍼' 라고 합니다. 이러한 캐시에는 쿼리 결과값 뿐만 아니라 인덱스도 저장됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터베이스에서 I/O 는 물리적 I/O 와&amp;nbsp;논리적 I/O 로 구분 됩니다.&lt;/b&gt; 물리적 I/O 는 Disk I/O 를 의미하며, 논리적 I/O 는 버퍼 풀에서 데이터를 복제하는 과정을 의미합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터베이스는 디스크에 데이터를 페이지 단위로 저장합니다.&lt;/b&gt; 이러한 페이지는 행의 데이터를 열의 순서에 맞게 저장합니다. 무슨 말이냐면 물리적 I/O 가 일어날때 일부 Column 만 선택해도 전체 Row 를 페이지 단위로 읽어오면서 메모리에 적재한다는 것입니다. 그리고 메모리와 캐시에 적재되는 데이터로 페이지 단위입니다.&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;[쿼리] -&amp;gt; [캐시 에서 조회]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; - 있으면 -&amp;gt; [논리 I/O, 페이지 단위] -&amp;gt; [결과 반환]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; - 없으면 -&amp;gt; [물리 I/O, 페이지 단위] -&amp;gt; [캐시 저장(논리 I/O, 페이지 단위)] -&amp;gt; [결과 반환]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;캐시Miss 가 발생하여 물리I/O 가 발생하면 적어도 캐시 Hit 시나리오보다 적어도 2배(현실은 수백배~수천배 더 느림)는 더 느립니다.&lt;/b&gt;&amp;nbsp; 즉, 동일한 쿼리에 대해서 데이터베이스의 물리 I/O 를 최대한 적게 만드는 것이 데이터베이스 조회 성능을 끌어오는 핵심입니다. 그렇다면 우리는 &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;SELECT * 와 SELECT {some_columns} 사이에는 어떤게 직접적으로 물리 I/O를 적게 일으키냐 결정할 수 없습니다. 칼럼 설계를 잘못해서 연속적이지 않은 칼럼을 조회할 경우 SElECT * 와 SELECT {some_columns} 가 물리 I/O 를 만들어내는 부하는 똑같을 것입니다.&lt;/p&gt;
&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;가 됩니다. 물리 I/O 과정을 통해 디스크에서 나온 수많은 페이지들이 메모리에 적재되어 정제된 후 캐시에 들어갈때는 훨씬 작은 페이지가 되기 때문입니다. 만약 SELECT * 를 하고, 해당 테이블에 BLOB 이나 TEXT 타입의 데이터가 있고 해당 칼럼은 관심이 있는 칼럼이 아니라면? 캐시 메모리를 굉장히 비효율적으로 쓰게 될 것입니다. &lt;b&gt;반면 SELECT {some_columns} 를 통해 필요한 칼럼만 캐시에 적재한다면 캐시 메모리를 더 효율적으로 쓰게되어 더 많은 유효한 데이터를 더 오랫동안 캐시에 모아둘 수 있게&lt;/b&gt; 됩니다. (새로운 데이터를 캐싱해야할때 캐시 메모리가 부족하다면 가장 참조가 덜된 데이터를 밀어내기 때문에)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;rdbms_covering_index.png&quot; data-origin-width=&quot;1534&quot; data-origin-height=&quot;1524&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMfPka/dJMcaj1CI4u/T03yGitacmyIqTbIFc3Dh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMfPka/dJMcaj1CI4u/T03yGitacmyIqTbIFc3Dh0/img.png&quot; data-alt=&quot;커버링 인덱스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMfPka/dJMcaj1CI4u/T03yGitacmyIqTbIFc3Dh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMfPka%2FdJMcaj1CI4u%2FT03yGitacmyIqTbIFc3Dh0%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;560&quot; height=&quot;556&quot; data-filename=&quot;rdbms_covering_index.png&quot; data-origin-width=&quot;1534&quot; data-origin-height=&quot;1524&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;커버링 인덱스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 캐시에는 인덱스 정보가 저장되기 때문에 인덱스에 맞는 특정 칼럼만 조회하는 것으로 성능 최적화(&lt;b&gt;커버링 인덱스&lt;/b&gt;)를 만들어 낼 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Q. 무슨 데이터베이스를 기준으로 말하는 건가요?&lt;br /&gt;A. 현대에는 여러 종류의 데이터베이스가 존재합니다. SQL, NoSQL 혹은 Row-Oriented, Column-Oriented 혹은 OLTP, OLAP 여러 조건들을 고려해야 할것입니다. 하지만 이 글은 일반적으로 많이 사용하는 SQL, Row-Oriented, OLTP 데이터베이스를 대상으로 작성되었습니다. 더 구체적으로 말하자면 MySQL InnoDB, PostgreSQL 을 생각하면서 글을 썼습니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Q. 데이터베이스 구조 설명이 너무 러프한데요?&lt;br /&gt;A. 우리가 일반적으로 많이 사용하는 MySQL, PostgreSQL 과 같은 OLTP 용 RDBMS 의 엄밀한 구조는 공식문서나 인터넷, 혹은 서적에서 더 엄밀하고 양질의 자료를 찾을 수 있습니다. 이 글에서는 &lt;b&gt;&quot;칼럼을 적게 선택(SELECT) 할 수록 성능이 좋아지는 이유&quot; 에 집중해서 설명하기 위해 데이터베이스 구조를 많이 단순화&lt;/b&gt;헀습니다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;왜 더 적은 칼럼을 조회할 수록 성능이 개선되는지 알게 되었습니다. 그럼 어떻게 성능을 최적화 할 수 있을 까요?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. ORM 에서 특정 칼럼만 가져오기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python 의 경우 아래와 같이 ORM 을 이용하면 모든 칼럼을 조회 (SELECT *) 하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1762591039967&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Django
users = User.objects.all() 
# SELECT &quot;user&quot;.&quot;id&quot;, &quot;user&quot;.&quot;username&quot;, &quot;user&quot;.&quot;email&quot;, &quot;user&quot;.&quot;bio&quot;, ... FROM &quot;user&quot;

# SQLAlchemy
users = session.query(User).all()
# SELECT user.id, user.username, user.email, user.bio, ... FROM user&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Django의 경우 .only() 혹은 .values() 를 이용하거나 SQLAlchemy 는 .options(load_only()) 와 query({column_names...}) 를 이용해서 특정 칼럼만 가져올 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 하나의 방법이 아닌 두개의 방법을 제공하는 걸까요? .only 와 .options(load_only()) 는 ORM 객체를 유지한다는 특징이 있습니다. 오...그러면 좋은걸까요? 백엔드 서버단에서의 성능 개선은 애매하지만 일단 데이터베이스와 네트워크에서의 성능 개선은 달성 하니까요? 일단 맞습니다. 하지만 일단 ORM 객체를 만들어주기 때문에 누구든지 load 되지 않은 필드에 접근을 한다면 아래와 같이&lt;b&gt; Deferred loading 현상&lt;/b&gt;을 만들어낼 위험이 존재합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1762591256550&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# SELECT &quot;user&quot;.&quot;id&quot;, &quot;user&quot;.&quot;username&quot; FROM &quot;user&quot;

# Django ORM
users = User.objects.only('id', 'username')

for user in users:
    print(user.username)
    print(user.email)    # 추가 쿼리 발생 (Deferred loading)
    

# SQLAlchemy
users = session.query(User).options(load_only(User.id, User.username)).all()
for user in users:
    print(user.username)
    print(user.email)    # 추가 쿼리 발생 (Deferred loading)&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;반면 values() 와 query({column_names...}) 는 ORM 객체가 아니라 dict 를 반환하게 됩니다. ORM 특징적인 기능은 사용하지 못하겠지만 불편한 만큼 성능상 이득과 더 명확히 side-effect 를 막는 효과가 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1762591628762&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# SELECT &quot;user&quot;.&quot;id&quot;, &quot;user&quot;.&quot;username&quot; FROM &quot;user&quot;

# Django ORM
users = User.objects.values('id', 'username')

# users[0] = {'id': 1, 'username': 'alice'}

# SQLAlchemy
users = session.query(User.id, User.username).all()
# users[0] = (1, 'alice')&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;2. 명확한 관심사 분리와 수직분리&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;vertical_partitioning.png&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;1522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dS7Tps/dJMcadNR9ip/eKj0tl6ViEKf02c1xlkHk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dS7Tps/dJMcadNR9ip/eKj0tl6ViEKf02c1xlkHk0/img.png&quot; data-alt=&quot;수직분할 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dS7Tps/dJMcadNR9ip/eKj0tl6ViEKf02c1xlkHk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdS7Tps%2FdJMcadNR9ip%2FeKj0tl6ViEKf02c1xlkHk0%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;560&quot; height=&quot;554&quot; data-filename=&quot;vertical_partitioning.png&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;1522&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수직분할 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 특정 필요로 하는 칼럼만 dict 로 만들면 모든 문제가 해결될까요? 아쉽게도 그렇지 않습니다. 데이터베이스에서 물리 I/O 가 아예 발생하지 않을 수 없습니다. 그리고 물리 I/O 가 발생할때 최종적으로 반환하려는 칼럼들이 서로 떨어진 페이지에 저장되어 있다면 그 구간의 모든 페이지를 모두 디스크에서 읽어와야하는 불상사가 발생합니다. &lt;b&gt;그리고 무엇보다 dict 로 데이터를 반환받을거면 ORM 을 왜 씁니까!&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;기본값(default) 자체가 빠르도록 RDBMS 설계를 변경하는 것이 더 근본적인 해결책입니다&lt;/b&gt;. 각 테이블마다 명확한 관심사를 갖도록 설계하고, 관심사가 동일하더라도 Usecase상 접근 빈도가 더 낮으면서 디스크공간을 많이 차지하는 데이터들을 &lt;b&gt;수직분할(Vertical Partitioning)&lt;/b&gt; 하는 방법을 적용하면 해결됩니다. 정말 RDBMS에서 테이블 설계가 잘 되었다면 모든 쿼리를 SELECT * 로 만들어도 큰 이슈가 없을 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;종종&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;과소 평가 됩니다. &quot;우리 고객들은 빠른 네트워크 환경에 있는데 100~200ms 정도 더 차이나는게 대수일까?&quot; 하는 생각 때문일텐데요. 이러한 생각이 갖는 문제는 해당 조직의 &lt;b&gt;성능 기준을&lt;/b&gt; 처음에는 100ms, 150ms 정도 더 느려지는 것을 용인하는 수준에서 나중에는 1000ms, 1500ms 까지 방치되는 수준으로 &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;a href=&quot;https://www.ericsson.com/en/press-releases/2016/2/streaming-delays-mentally-taxing-for-smartphone-users-ericsson-mobility-report&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Erricson의 보고서(2016)&lt;/a&gt;, &lt;a href=&quot;https://www.creativebloq.com/features/how-the-bbc-builds-websites-that-scale&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BBC의 사례(2018)&lt;/a&gt;, &lt;a href=&quot;https://web.dev/case-studies/rakuten?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Rakuten의 사례(2022)&lt;/a&gt; 에 따르면 사이트 로딩 속도가 느리면 사용자는 스트레스를 심하게 받으며, 이탈로 이어집니다. 반대로 사이트 로딩 속도가 빠르면 전환율이 오르기도 합니다. 즉, 속도는 고객의 이탈과 전환율에 직접적으로 관여하는 요소입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 만난 많은 개발자들은 언제나 비즈니스 임팩트까지 만들어내고 싶어하는 분들이었습니다. 그런 개발자들의 고민은 &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;저는 처음 개발자 커리어를 시작할 때부터 수억건의 데이터를 가진 N 개의 DB 테이블을 활용해서 200ms 내에게 데이터를 서빙하는데 주의를 기울였습니다. 이 속도를 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Latency 를 1000ms 에서 500ms 로, 200ms 로, 100ms 이내로 줄일때마다 고객들이 명확하게 이를 인지하고 더 기쁜 마음으로 서비스를 사용한다는 사실을 눈앞에서 보았습니다. 저는 이러한 경험을 통해 성능에 집착하는 개발자가 되었습니다. 우리 모두 더 좋은 사용자 경험과 이를 통해 비즈니스 임팩트를 만들어 내는 개발자가 되어봅시다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>탐구 생활</category>
      <category>ORM 문제</category>
      <category>ORM 성능</category>
      <category>ORM 성능 최적화</category>
      <category>RDBMS 성능 최적화</category>
      <category>관심사의 분리</category>
      <category>백엔드 성능</category>
      <category>수직분할</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/117</guid>
      <comments>https://probehub.tistory.com/117#entry117comment</comments>
      <pubDate>Sat, 8 Nov 2025 17:57:07 +0900</pubDate>
    </item>
    <item>
      <title>Python 과 zstd - middleware 적용하기</title>
      <link>https://probehub.tistory.com/116</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈 &lt;a href=&quot;https://probehub.tistory.com/112&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;첫글&lt;/a&gt;에서 Python 3.14 정식 배포 버전에서 zstd 알고리즘 native 지원에 더 눈길이 갔던 이유가&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;I/O Bounded API Server 의 응답속도 개선&lt;/b&gt;에 주의를 기울이고 있기 때문이라고 말했었습니다. 특히 지금 맡은 업무가 웹 브라우저와 직접적으로 상호작용하는 그렇다면 Python 으로 작성된 Web Server (FastAPI, Django) 에서 zstd 를 어떻게 사용할 수 있을지 공유하고자 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;전체 흐름 점검하기&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;우선 가장먼저 드는 의문점은 누가 언제 무엇을 인코딩하고 디코딩하냐는 겁니다. 정말 다양한 기준이 있겟지만 웹서버를 기준으로만 정리해보겠습니다.&amp;nbsp; 이제부터 말하는 &lt;b&gt;클라이언트는 웹브라우저(PC / Mobile)&lt;/b&gt; 이며 &lt;b&gt;서버는 웹 서버&lt;/b&gt;입니다. 그리고 주된 &quot;&lt;b&gt;흐름&quot;&lt;/b&gt;은 파일 데이터가 아니라 &lt;b&gt;HTML, CSS, JS, JSON 과 같은 현대적인 웹서비를 만드는데 필요한 포맷의 데이터를 주고받는 것&lt;/b&gt;을 지칭합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;encoding_decoding_flow_1.png&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ku8om/dJMcajHigvA/ncP0oSOjVFvPnFZlUeskE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ku8om/dJMcajHigvA/ncP0oSOjVFvPnFZlUeskE1/img.png&quot; data-alt=&quot;데이터 인코딩 / 디코딩 과정을 간략하게 본다면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ku8om/dJMcajHigvA/ncP0oSOjVFvPnFZlUeskE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKu8om%2FdJMcajHigvA%2FncP0oSOjVFvPnFZlUeskE1%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;820&quot; height=&quot;257&quot; data-filename=&quot;encoding_decoding_flow_1.png&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데이터 인코딩 / 디코딩 과정을 간략하게 본다면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;클라이언트: 디코딩의 책임자&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 HTTP 에서 연결이 이어지는 과정은 클라이언트와 서버간 SYNC, ACK 의 과정이 필요합니다. 이때 클라이언트가 먼저 서버에게 요청을 보내면서 연결이 확립되죠(server sent event 조차도 처음에는 클라이언트가 GET 요청을 보냅니다). 그렇다면 이때&lt;b&gt; 클라이언트가 &quot;나는 x, y, z 등등의 인코딩 데이터를 식별하고 디코딩할 수 있다&quot; 는 정보를 서버에게 보내줘야&lt;/b&gt; 할겁니다. HTTP 는 무상태(Stateless) 프로토콜이기 때문에 당연히 이러한 정보는 모든 요청정보에서 반복적으로 등장할 겁니다.&lt;/p&gt;
&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;a href=&quot;https://www.rfc-editor.org/rfc/rfc9110.html#name-accept-encoding&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;RFC 9110 에서는 &lt;/span&gt;이러한 Spec을 Accept-Encoding 헤더를 보내는것&lt;/a&gt;&lt;/b&gt;으로 표준을 잡았습니다. 또한 q-factor 라는 값을 추가하여 클라이언트가 처리할 수 있는 인코딩 알고리즘과 선호하는 인코딩 알고리즘을 서버에서 해석할 수 있도록 하였습니다.&lt;/p&gt;
&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;blockquote data-ke-style=&quot;style3&quot;&gt;Accept-Encoding: gzip;q=1.0, br;q=0.9, zstd;q=0.1&lt;br /&gt;&lt;br /&gt;// 아래와 같은 내용들을 사용가능,&lt;br /&gt;Accept-Encoding: gzip&lt;br /&gt;Accept-Encoding: compress&lt;br /&gt;Accept-Encoding: deflate&lt;br /&gt;Accept-Encoding: br&lt;br /&gt;Accept-Encoding: identity&lt;br /&gt;Accept-Encoding: *&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;서버: 인코딩의 책임자&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;서버는 Accept-Encoding 을 읽고 q-factor 의 우선순위에 따라 자신이 인코딩 할 수 있는 방식으로 Response 를 인코딩하여 반환합니다. 이때 서버는 Content-Encoding 헤더에 이 Response 가 어떤 알고리즘으로 인코딩되었는지 명시해서 보내야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Content-Encoding: gzip&lt;/blockquote&gt;
&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 style=&quot;color: #333333; text-align: start;&quot;&gt;Content-Encoding&lt;span&gt; 헤더에 담긴 이름이 다르다면 클라이언트를 데이터를 제대로 디코딩할 수 없고 브라우저 사용자에게 데이터가 표출되지 않는 오류가 발생하게 됩니다. 그리고 현대 웹프레임워크에서 이런 &quot;인코딩&quot; 과정은 비즈니스적인 관심사는 아니고 프레임워크 레벨에서의 관심사이기 때문에 필터(Filter) 혹은 미들웨어(Middleware) 단에서 구현되기 마련입니다.&lt;/span&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Python Backend 시스템에 도입하기&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Python Web Server Framework 에서 클라이언트에게 응답을 인코딩하는 역할은 Middleware 에게 주어져있습니다. FastAPI, Django 모두 GzipMiddleware 를 지원하며 이를 이용해서 단순 HTTP 뿐만 아니라 스트림 데이터도 인코딩할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 찾아본 Middleware 등의 정보를 간단히 공유합니다. Middleware 를 적용하는 방법은 너무 간단하고, 각 프로젝트의 README 에 잘 나타나 있으므로 굳이 내용을 추가하지 않겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Middleware (&lt;b&gt;ASGI)&lt;/b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ASGI 에서는 별 10개 이상 받은 Middleware 가 &lt;a href=&quot;https://github.com/tuffnatty/zstd-asgi&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;zstd-asgi&lt;/a&gt;, &lt;a href=&quot;https://github.com/developmentseed/starlette-cramjam&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;cramjam-asgi&lt;/a&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;zstd-asgi 와 cramjam-asgi 를 비교했을때 zstd-asgi 가 약간 더 빠른 압축 속도(Python 3.13.9-slim 기준)를 보여주었습니다. 다양한 환경에서 테스트가 필요하겠지만 모든 요청에 대응하는 Middleware 이므로 약간의 성능 차이가 크게 작용할 수 있다는 점, 그리고 zstd-asgi 는 Python 3.14 를 까지 지원하는 업데트가 최근에 일어났다는 점을 봤을때 zstd-asgi 가 더 나은 선택인 것으로 보입니다. (CPU 사용은 별 차이가 없었음)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Middleware (WSGI)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Django 에서 사용할 수 있는 Middleware 로는 가장 최근까지 유지보수되었던&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://github.com/adamchainz/django-http-compression&quot;&gt;django-http-compression&lt;/a&gt;&lt;span style=&quot;color: #333333; 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;p data-ke-size=&quot;size16&quot;&gt;djang-http-compression 는 굉장히 인상적이었습니다. RFC 9110 을 준수하기 위한 수 많은 옵션들과 DEFLATE, Brotli, Zstandard 등을 모두 지원하는 Middleware 를 구현해뒀다는 점이 인상적이었고, 역시 Python 백엔드 생태계에서는 Django 가 가장 성숙하다는 것을 다시 한번 확인할 수 있었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;인코딩을 꼭 해야하나요?&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우리는 근본적인 질문을 할 수 있습니다. 도대체 왜 인코딩을 해야하는걸까요? 인코딩을 하면 서버의 CPU 를 사용하게 되고 오버헤드가 추가 될겁니다. 그건 클라이언트(브라우저)도 마찬가지죠. 어떤 이득이 있기 때문에 웹 서버 생태계에서 인코딩을 위한 Middleware 들이 개발되고 있는걸까요?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인코딩을 적용하면 네트워크 레벨로 전송되는 절대저인 데이터량이 적어집니다. 당연히 브라우저가 다운로드 받아야하는 데이터량도 적어지는 것이죠. 이러한 이유로 네트워크 트래픽 감소에 따른 비용 절감과 다운로드 속도 향상으로 전반적인 웹 페이지 로딩 속도 개선을 기대할 수 있습니다. 물론 인코딩/디코딩에 따른 오버헤드(시간 및 CPU 사용)는 있으므로 트레이드 오프를 잘 보고 선택해야합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;단적으로 봐도 JSON Request/Response 를 처리하는 API 서버는 DEFLATE 알고리즘을 이용하는 Gzip Middleware 를 Zstd Middleware 로 교체하는게 이득이 있을 것으로 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;can_i_use_zstd.png&quot; data-origin-width=&quot;2862&quot; data-origin-height=&quot;1060&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biWlGX/dJMcahpaVQD/12v3Um7Jm9KjL6djhKETkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biWlGX/dJMcahpaVQD/12v3Um7Jm9KjL6djhKETkk/img.png&quot; data-alt=&quot;아직 safari 에서는 zstd를 완전히 지원하는게 아닙니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biWlGX/dJMcahpaVQD/12v3Um7Jm9KjL6djhKETkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiWlGX%2FdJMcahpaVQD%2F12v3Um7Jm9KjL6djhKETkk%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;820&quot; height=&quot;304&quot; data-filename=&quot;can_i_use_zstd.png&quot; data-origin-width=&quot;2862&quot; data-origin-height=&quot;1060&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아직 safari 에서는 zstd를 완전히 지원하는게 아닙니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 25년 10월을 기준으로 아직 모든 브라우저들이 zstd 알고리즘을 지원하는 것은 아닙니다. 따라서 단순히 Gzip Middleware 를 걷어내고 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Zstd Middleware&lt;span&gt; 로 갈아탄다! 의사결정을 하기 보다는 &lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://github.com/adamchainz/django-http-compression&quot;&gt;django-http-compression&lt;/a&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 와 같이 여러 알고리즘을 같이 적용할 수 있는 미들웨어를 선택하는 것이 현명할 것으로 보입니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;can_i_use_brotli.png&quot; data-origin-width=&quot;2960&quot; data-origin-height=&quot;1172&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciOUSf/dJMcaaXS8fR/bxyj0KseU7B6aI0ZrtyN7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciOUSf/dJMcaaXS8fR/bxyj0KseU7B6aI0ZrtyN7K/img.png&quot; data-alt=&quot;brotli 압축방식은 전반적으로 지원됩니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciOUSf/dJMcaaXS8fR/bxyj0KseU7B6aI0ZrtyN7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciOUSf%2FdJMcaaXS8fR%2Fbxyj0KseU7B6aI0ZrtyN7K%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;820&quot; height=&quot;325&quot; data-filename=&quot;can_i_use_brotli.png&quot; data-origin-width=&quot;2960&quot; data-origin-height=&quot;1172&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;brotli 압축방식은 전반적으로 지원됩니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zstd asgi: &lt;a href=&quot;https://github.com/tuffnatty/zstd-asgi&quot;&gt;https://github.com/tuffnatty/zstd-asgi&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zstd starlette: &lt;a href=&quot;https://github.com/developmentseed/starlette-cramjam&quot;&gt;https://github.com/developmentseed/starlette-cramjam&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zstd django: &lt;a href=&quot;https://github.com/adamchainz/django-http-compression&quot;&gt;https://github.com/adamchainz/django-http-compression&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zstd 지원 브라우저 정보: &lt;a href=&quot;https://caniuse.com/zstd&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://caniuse.com/zstd&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cloudflare blog: &lt;a href=&quot;https://blog.cloudflare.com/new-standards/#introducing-zstandard-compression&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.cloudflare.com/new-standards/#introducing-zstandard-compression&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>탐구 생활/Python 과 zstd</category>
      <category>django middleware</category>
      <category>fastapi middleware</category>
      <category>python gzip</category>
      <category>python middleware</category>
      <category>python zstd</category>
      <category>python 압축 미들웨어</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/116</guid>
      <comments>https://probehub.tistory.com/116#entry116comment</comments>
      <pubDate>Tue, 14 Oct 2025 21:55:47 +0900</pubDate>
    </item>
    <item>
      <title>Python 과 zstd - LZ77 과 허프만 코딩</title>
      <link>https://probehub.tistory.com/112</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;25년 10월 7일, 추석 연휴가 한창일때, Python 3.14 정식 버전 (3.14.0) 이 배포되었습니다. 많은 Python 사용자들이 Free-Threaded 에 대해 이야기했습니다. 하지만 저는 Python Native zstd 압축이 지원된다는 사실에 더 관심이 갔습니다. 최근 I/O Bounded API Server 의 응답속도 개선에 관심을 기울이고 있었기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 본격적으로 zstd 압축 알고리즘을 알아보고 활용법을 탐구하기 전에 대표적인 무손실 압축 알고리즘인 LZ77과 허프만 코딩을 알아보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;대표적인 무손실&lt;span&gt; &lt;/span&gt;압축 알고리즘&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zstd 압축에 대해서 이야기하기에 앞서 현대 컴퓨터 시스템에서 압축 알고리즘의 근간이 되는 두 알고리즘을 살펴보겠습니다. 앞으로 설명할 gzip, brotli 그리고 zstd 까지 모두 이 두 알고리즘을 적절히 적용한 사례입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LZ77&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 사전기반 (dictionary based) 압축 기법의 한 종류로 데이터 스트림(인풋 데이터)에서 반복적인 시퀀스를 처리하는 알고리즘입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 스트림내에서 반복적으로 나타는 바이트 시퀀스를 찾아내어 사전(dictionary)을 만들고 두 번째 등장부터는 해당 시퀀스가 처음 나타났던 위치에 대한 참조(reference)로 바이트 시퀀스를 교체하는 식으로 동작합니다. 즉, 반복적으로 등장하는 원본데이터를 작은 크기의 포인터로 표현하는 것이죠.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;슬라이딩 윈도우&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;LZ77 알고리즘은 일정한 크기의 데이터 스트림을 읽는데 이를 슬라이딩 윈도우라고 합니다. 이 슬라이딩 윈도우는 다시 2개로 나뉘는데요, 이미 인코딩된 데이터로 구성되며, 이후에 읽어들일 데이터에 사전 역할을 하는 &lt;b&gt;서치 버퍼(Search Buffer)&lt;/b&gt;와 이제 읽어들여서 인코딩될 대상인 &lt;b&gt;룩어헤드 버퍼(Look ahead Bugger)&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2552&quot; data-origin-height=&quot;726&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lMCsT/btsQ69LshGQ/lcZ3j8FfsFbA3GkEhnPmEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lMCsT/btsQ69LshGQ/lcZ3j8FfsFbA3GkEhnPmEK/img.png&quot; data-alt=&quot;슬라이딩 윈도우 구조(https://www.youtube.com/watch?v=yTQIONTE3Y8)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lMCsT/btsQ69LshGQ/lcZ3j8FfsFbA3GkEhnPmEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlMCsT%2FbtsQ69LshGQ%2FlcZ3j8FfsFbA3GkEhnPmEK%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;667&quot; height=&quot;190&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2552&quot; data-origin-height=&quot;726&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;슬라이딩 윈도우 구조(https://www.youtube.com/watch?v=yTQIONTE3Y8)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(Distance, Length) 쌍으로 인코딩&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;룩어헤드 버퍼의 시작 부분과 일치하는 가장 긴 시퀀스를 서치버퍼에서 발견하면, 알고리즘은 이 반복되는 시퀀스를 (Distance, Length) 쌍으로 인코딩합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Distance: 슬라이딩 윈도우의 시퀀스와 룩어헤드 버퍼의 시퀀스가 일치하는 시작점까지의 거리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Length: 일치하는 시퀀스가 몇 바이트로 구성되어 있는지&lt;/p&gt;
&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;- Step1: 처음 데이터를 읽었을때 서치버퍼의 시작부분과 룩어헤드 버퍼와 매치되는 시퀀스가 없습니다. 그래서 슬라이딩 윈도우가 한칸 이동했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Step2: 이제 서치버퍼와 룩어헤드 버퍼가 일치하는 시퀀스를 찾았습니다. 두 시퀀스의 시작점간의 거리(DIstance)가 7칸, 그리고 일치되는 시퀀스의 길이(Length)가 4칸, 그리고 다음 문자가 r 이어서 (7,4,'r') 로 데이터가 인코딩 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 데이터와 c 와 같이 인코딩 되지 않은 데이터를 &lt;b&gt;리터럴&lt;/b&gt;이라고 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2628&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLm2DM/btsQ7R4GheS/OEWzop5evpRolcqUKzJ6d0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLm2DM/btsQ7R4GheS/OEWzop5evpRolcqUKzJ6d0/img.png&quot; data-alt=&quot;인코딩 결과 (https://www.youtube.com/watch?v=yTQIONTE3Y8)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLm2DM/btsQ7R4GheS/OEWzop5evpRolcqUKzJ6d0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLm2DM%2FbtsQ7R4GheS%2FOEWzop5evpRolcqUKzJ6d0%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;667&quot; height=&quot;274&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2628&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인코딩 결과 (https://www.youtube.com/watch?v=yTQIONTE3Y8)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LZ77 은 LZ78 그리고 LZW 등 다양한 변형 알고리즘이 존재하지만 기본적인 프레임워크는 동일합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;&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;h4 data-ke-size=&quot;size20&quot;&gt;허프만 트리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 직관적인 설명이죠? 위의 설명은 Spec 으로 본다면 이제는 구현 세부사항을 알아볼 차례입니다. 빈도 기반 이진코드를 만들기 위해 허프만 트리라는 이진트리를 활용합니다.&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터에서 각 심볼이 나타나는 빈도를 계산하고 각 빈도를 가중치(weight)로 갖는 리프 노드(leaf node)를 생성합니다.&lt;/li&gt;
&lt;li&gt;생성된 모든 리프 노드를 우선순위 큐(priority queue)에 넣습니다. 큐의 head 부분에는 가중치가 낮은 노드가 위치합니다.&lt;/li&gt;
&lt;li&gt;큐에 노드가 하나 남을때까지 두 노드를 꺼내고 이 두 노드를 자식으로 하는 부모 노드를 생성합니다. 이때 부모 노드의 가중치는 두 자식 도으 가중치의 합으로 합니다. 새로 생성된 부모 노드를 다시 우선순위 큐에 넣습니다.&lt;/li&gt;
&lt;li&gt;마지막으로 하나 남은 노드가 바로 허프만 트리의 루트 노드(root node)가 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;this is an example of a huffman tree&quot;. 라는 데이터를 대상으로 허프만 트리를 만들면 아래와 같이 구성됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Huffman_tree_2.svg&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;429&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbYtYD/dJMb9fLKS1H/mWIZb8QYv0We48i809FGcK/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbYtYD/dJMb9fLKS1H/mWIZb8QYv0We48i809FGcK/tfile.svg&quot; data-alt=&quot;(https://en.wikipedia.org/wiki/Huffman_coding)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbYtYD/dJMb9fLKS1H/mWIZb8QYv0We48i809FGcK/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbYtYD%2FdJMb9fLKS1H%2FmWIZb8QYv0We48i809FGcK%2Ftfile.svg&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;667&quot; height=&quot;429&quot; data-filename=&quot;Huffman_tree_2.svg&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;429&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;(https://en.wikipedia.org/wiki/Huffman_coding)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이진코드 부여&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만들어진 허프만 트리를 기반으로 드디어 심볼에 대한 이진코드를 부여합니다. 이진 코드는 루트 노드에서 해당 심볼의 리프 노드까지의 경로로 결정되는데요. 일반적으로 왼쪽 가지로 이동하는 것을 '0'으로&amp;nbsp;오른쪽 가지로 이동하는 것을 '1'로 표현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 통해 빈도가 높은 심볼은 루트에 가까운, 즉 경로가 짧은 위치에 배치되어 짧은 코드를 할당받고, 빈도가 낮은 심볼은 루트에서 멀리 떨어진, 즉 경로가 긴 위치에 배치되어 긴 코드를 할당받게 됩니다. 그리고 이런 과정을 통해 이진코드를 부여하면 놀랍게도 &lt;b&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;어떤 심볼의 코드도 다른 심볼 코드의 접두사(prefix)가 되지 않는다는 특징&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;을 갖게됩니다.&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt; 예를 들어, 'A'의 코드가 01이라면, 다른 어떤 심볼의 코드도 01로 시작할 수 없습니다.&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: #1b1c1d;&quot;&gt;이러한 &lt;b&gt;접두사 없는 코드(Prefix-Free Code)&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;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Python 3.14 신기능:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://docs.python.org/3.14/whatsnew/3.14.html&quot;&gt;https://docs.python.org/3.14/whatsnew/3.14.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python Free-Threded: &lt;a href=&quot;https://docs.python.org/3.14/whatsnew/3.14.html#whatsnew314-free-threaded-cpython&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.python.org/3.14/whatsnew/3.14.html#whatsnew314-free-threaded-cpython&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python zstd: &lt;a href=&quot;https://docs.python.org/3/library/compression.zstd.html&quot;&gt;https://docs.python.org/3/library/compression.zstd.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LZ77 알고리즘 예시: &lt;a href=&quot;https://www.youtube.com/watch?v=yTQIONTE3Y8&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=yTQIONTE3Y8&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;허프만 코딩: &lt;a href=&quot;https://en.wikipedia.org/wiki/Huffman_coding&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://en.wikipedia.org/wiki/Huffman_coding&lt;/a&gt;&lt;/p&gt;</description>
      <category>탐구 생활/Python 과 zstd</category>
      <category>brotli</category>
      <category>gzip</category>
      <category>LZ77</category>
      <category>zstd</category>
      <category>엔트로피 인코딩</category>
      <category>엔트로피 코딩</category>
      <category>허프만 코딩</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/112</guid>
      <comments>https://probehub.tistory.com/112#entry112comment</comments>
      <pubDate>Sun, 12 Oct 2025 18:24:11 +0900</pubDate>
    </item>
    <item>
      <title>하나의 컨테이너에 하나의 프로세스</title>
      <link>https://probehub.tistory.com/111</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;docker_one_process_per_a_container.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GLCgw/btsPaquUc6s/SoNgkYHr49KRkOnRKdiwW1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GLCgw/btsPaquUc6s/SoNgkYHr49KRkOnRKdiwW1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GLCgw/btsPaquUc6s/SoNgkYHr49KRkOnRKdiwW1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGLCgw%2FbtsPaquUc6s%2FSoNgkYHr49KRkOnRKdiwW1%2Fimg.webp&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;520&quot; height=&quot;520&quot; data-filename=&quot;docker_one_process_per_a_container.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종종 하나의 Docker 컨테이너에 2개 이상의 프로세스를 Entrypoint의 Script 를 통해 실행하는 경우를 봅니다. 그리고 이러한 행태가 &quot;전혀 문제 없는 방식&quot; 이라고 말하는 사람도 만납니다. &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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC 와 FastAPI 를 하나의 컨테이너에서 운영하는 경우를 예시로 들겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 운영 리스크를 키운다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 다른 프로세스 두 개를 하나의 컨테이너에서 운영하는 것은 운영 측면에서 리스크를 키웁니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프로세스 생명주기:&lt;/b&gt; Docker는 PID 1 하나만 관리합니다. 추가로 띄운 자식 프로세스가 신호(SIGTERM/SIGINT)를 못 받아 좀비 프로세스가 남거나, graceful-shutdown 없이 강제 종료될 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장애의 전체 전파:&lt;/b&gt; 하나의 컨테이너에서 gRPC 와 FastAPI 를 모두 구동한다면 gRPC 서버만 죽어도 FastAPI까지 함께 재시작하는 현상이 일어납니다. 각각 내부/외부 담당하는 소통 영역이 다르지만 같은 생명주기를 갖게 되므로 부분 장애가 전체 전파로 이어지게 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;헬스체크 불가분성:&lt;/b&gt; 서로 다른 2개 프로세스에 대해 Liveness/Readiness probe 를 설정해야 하므로 설정이 복잡해지고 실제 K8s 에서 probe 를 설정할때 하나의 Pod에 대해서 gRPC HealthCheck 와 HTTP HealthCheck 를 함께 설정할 수 없어서 서비스 품질 관리에 문제가 발생합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로그/모니터링 혼선:&lt;/b&gt; 두 프로세스의 STDOUT/STDERR 스트림이 섞여서 수집되므로 로그스트림이 일관된 정보를 담기 어렵습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리소스 경합:&lt;/b&gt; CPU/메모리 제한을 하나로 묶으므로, 트래픽 패턴이 다른 두 서비스가 서로 스로틀링을 일으킬 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 컨테이너의 철학에 위배된다.&lt;/h3&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;Single Responsibility:&lt;/b&gt; Docker 공식 가이드에 따르면 &lt;b&gt;&amp;ldquo;한 컨테이너에는 한 서비스를 넣어 관심사를 분리하라&amp;rdquo;&lt;/b&gt; 라고 합니다. 이 지침에 따라 컨테이너를 작고 예측 가능하게 유지해야 이식성과 디버깅, 배포 이점이 극대화됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;변경 단위 최소화:&lt;/b&gt; 컨테이너 이미지는 불변(immutable) 아티팩트여야 합니다. 하나의 이미지에 여러 바이너리를 번들하면, 한 컴포넌트 수정이 있을 때마다 전체 이미지를 재빌드, 재배포해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. DevOps 를 복잡하게 만든다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라우드 환경에서 컨테이너를 활용한 CI/CD 그리고 가시성확보가 보편화되었습니다. 그런데 기본적인 컨테이너의 철학을 깨면 암묵적인 룰을 깨트리는 것이죠.&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; HPA(Kubernetes Horizontal Pod Autoscaler)를 프로메트릭스(QPS, CPU) 기준으로 튜닝할 때, FastAPI CPU 스파이크가 gRPC 컨테이너 증설을 강제하여 비용 상승을 초래합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배포 전략 제약:&lt;/b&gt; Blue/Green 배포, Canary 배포와 같이 군집마다 두 서비스가 동시에 교체하는 전략은 절반만 새 버전으로 돌려서 A/B 테스트하기 어렵습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;관측 스택 복잡화:&lt;/b&gt; OpenTelemetry, Prometheus exporter를 둘 다 탑재하면 스크래핑 엔드포인트가 겹치거나 포트 충돌 위험이 있습니다. 이를 회피하기 위해서는 사이드카 컨테이너를 이용할 수 있는데, 굳이 그렇게까지 해야할까요?&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;몇가지 추가적인 이야기들&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 문제점을 이야기를 했을때 들었던 이야기도 공유할까 합니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;그럼 Sidecar Container 로 빼면 되는 건가요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 하나의 컨테이너를 공유하고 있던 여러 프로세스들은 많은 경우 개념적으로 공통적이 많은 프로세스들입니다. 그러다보니 해당 프로세스를 개별 컨테이너로 분리하는것에 대해 이야기할때 &quot;아 그럼 Sidecar 로 분리하면 되는건가&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;이건 경우에 따라 다릅니다. 우선 K8s 의 Sidecar 란 주된 프로세스(Primary App)에 기능을 추가하거나 확장할때 사용되는 기능입니다.&amp;nbsp; 그렇다면 하나의 컨테이너에서 돌아가던 프로세스가 로깅, 보안, 모니터링 등을 위한 장치였다면 Sidecar 로 분리하는게 합리적일 것입니다. 하지만 그냥 다른 서비스라면 Sidecar 로 분리하는것은 설계 철학에 위배되는 행위입니다. 그리고 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;설계 철학을 따르지 않으면 실질적인 문제가 따르게&amp;nbsp;&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;Sidecar 로 별도 서비스(Secondary App)를 배포하면 독립적인 스케일링이 안되며, 주된 프로세스(Primary App)과 Lifecycle 이 연동되어 버리고 보안경계가 흐려진다는 문제가 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;어차피 동일한 로직, DB 접근을 공유하는데 코드는 어떡하죠?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 질문은 아직 모듈화의 개념이 잡히지 않은 개발자에게 주로 듣게되는 것 같습니다. 아직은 Python 진영의 특성상 모듈화를 강조하지 않아서 그런지 Java 진영보다 Python 진영 개발자로부터 더 자주 듣게됩니다.&lt;/p&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 접근을 공유하는데 서비스가 나눠져있다면 로직과 DB 접근을 core 모듈(예를들어)로 분리하고 Application 혹은 Service 계층을 분리하여 2개의 서로다른 컨테이너로 말아올릴 수 있습니다.&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;디렉터리 구조로 보자면 이런 식이죠. 이 코드는 core 에 있는 주요 로직을 apps 의 fastapi_app 과 grpc_app 이 공유하는 형태입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1752023010824&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;├── apps
│   ├── fastapi_app
│   │   └── pyproject.toml
│   └── grpc_app
│       └── pyproject.toml
├── core
│   ├── pyproject.toml
│   └── src
│       └── core
├── Dockerfile.fastapi
├── Dockerfile.grpc
├── pyproject.toml
└── uv.lock&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;현실적인 회색지대&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 문제점에도 불구하고 여러 프로세스를 하나의 컨테이너에서 운영하는 상황이 이해되는 경우가 있습니다. 바로 초기 PoC 단계의 비용 압박인데요. 여러 컨테이너를 돌릴 컴퓨팅 리소스는 제한적인 상황에서 빠르게 아이디어를 검증해봐야 한다면 하나의 컨테이너에서 여러 프로세스를 운용해볼 수 있을 것입니다.&lt;/p&gt;
&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://docs.docker.com/engine/containers/multi-service_container/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Docker 진영에서도 하나의 컨테이너에서 여러개의 프로세스를 구동하는 방법&lt;/a&gt;을 소개하고 있습니다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하지만 해당 문서에서도 하나의 서비스가 하나의 컨테이너에 대응 되어야 한다는 것을 명확히 하고 있습니다. 즉, 가능하다는 거지 &lt;/span&gt;&lt;b&gt;절대 위의 문제가 해소된다는 것이 아닙니다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;It's ok to have multiple processes, but to get the most benefit out of Docker, avoid one container being responsible for multiple aspects of your overall application.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.baeldung.com/ops/one-process-per-container&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;Baeldung, Why Is It Recommended to Run One Process per Container?&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.couchbase.com/blog/docker-container-anti-patterns&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;Couchbase, Docker Container Anti Pattern&lt;/b&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;K8s, Sidecar Containers&lt;/b&gt;&lt;/a&gt;&lt;br /&gt;&lt;b&gt;&lt;a href=&quot;https://docs.docker.com/engine/containers/multi-service_container/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Docker Docs, Run&amp;nbsp;multiple&amp;nbsp;processes&amp;nbsp;in&amp;nbsp;a&amp;nbsp;container&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>탐구 생활/개발 탐구</category>
      <category>docker 컨테이너 best practice</category>
      <category>one process per a container</category>
      <category>one service per a container</category>
      <category>단일 프로세스</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/111</guid>
      <comments>https://probehub.tistory.com/111#entry111comment</comments>
      <pubDate>Wed, 9 Jul 2025 10:04:17 +0900</pubDate>
    </item>
    <item>
      <title>가시성 (1) - 그 개념에 대하여</title>
      <link>https://probehub.tistory.com/109</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;observability.webp&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btvG11/btsO7qH0uIP/q1kGlk0sJVrwuuMGjwFlt1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btvG11/btsO7qH0uIP/q1kGlk0sJVrwuuMGjwFlt1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btvG11/btsO7qH0uIP/q1kGlk0sJVrwuuMGjwFlt1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtvG11%2FbtsO7qH0uIP%2Fq1kGlk0sJVrwuuMGjwFlt1%2Fimg.webp&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;520&quot; height=&quot;347&quot; data-filename=&quot;observability.webp&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 개발자들끼리 이야기하다보면 &lt;b&gt;&quot;어떻게 모니터링하냐&quot;&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가시성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가시성(observability)이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가시성(Observability) 은 &lt;b&gt;시스템이 스스로 내보내는 신호만으로 내부 상태를 재구성하고 특정 현상의 원인을 추론할 수 있는 능력&lt;/b&gt;을 뜻합니다. 처음엔 &lt;a href=&quot;https://namu.wiki/w/%EC%A0%9C%EC%96%B4%EA%B3%B5%ED%95%99&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;제어공학&lt;/a&gt; 용어였지만, 오늘날 소프트웨어, 클라우드 세계에서는 그 아래와 같은 특징이 나타났습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 85px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 32.6744%; height: 17px; text-align: center;&quot;&gt;특징&lt;/td&gt;
&lt;td style=&quot;width: 67.3256%; height: 17px; text-align: center;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 32.6744%; height: 17px;&quot;&gt;&lt;span&gt;출력 기반 진단&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 67.3256%; height: 17px;&quot;&gt;로그, 메트릭, 트레이스, 이벤트, 프로파일링 등 &lt;b&gt;외부 출력&lt;/b&gt; 만 보고 내부를 추적할 수 있음.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 32.6744%; height: 17px;&quot;&gt;&lt;span&gt;액션 가능성 (Actionability)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 67.3256%; height: 17px;&quot;&gt;데이터를 단순히 모아서 리포팅하는게 목적이 아니라 쿼리, 시각화, 알림을 통해 &lt;b&gt;문제를 신속히 파악하고 조치할 수 있도&lt;/b&gt;록 함.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 32.6744%; height: 17px;&quot;&gt;&lt;span&gt;미지의 질문(Unknown-Unknowns) 대응&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 67.3256%; height: 17px;&quot;&gt;미리 정의된 대시보드, 경보만으로는 알 수 없는 &lt;b&gt;새로운 현상을 탐지, 분석&lt;/b&gt;할 수 있어야함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 32.6744%; height: 17px;&quot;&gt;&lt;span&gt;시스템 내재적 품질&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 67.3256%; height: 17px;&quot;&gt;&lt;b&gt;가시성은 시스탬에 내재된 특성&lt;/b&gt;이므로 설계 단계부터 풍부한 외부 출력을 내보내도록 만들어야함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;근본적으로 다릅니다.&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;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.5581%; text-align: center;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;width: 28.3722%; text-align: center;&quot;&gt;모니터링&lt;/td&gt;
&lt;td style=&quot;width: 34.0697%; text-align: center;&quot;&gt;가시성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.5581%;&quot;&gt;원하는 것&lt;/td&gt;
&lt;td style=&quot;width: 28.3722%;&quot;&gt;문제가 발생했는지 파악하는것, 즉 알려진 미지(Known Unkowns)를 기록하고 리포팅하는 것&lt;/td&gt;
&lt;td style=&quot;width: 34.0697%;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;알려지지 않은 미지(Unkown&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Unkowns)에 대응해서 왜 문제가 발생했는지, 다른 이상 징후는 없는지 알아내는 것&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.5581%;&quot;&gt;목표&lt;/td&gt;
&lt;td style=&quot;width: 28.3722%;&quot;&gt;SLA 준수, 빠른 감지&lt;/td&gt;
&lt;td style=&quot;width: 34.0697%;&quot;&gt;근본 원인 분석(RCA), MTTR 단축, 시스템 개선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.5581%;&quot;&gt;데이터 수집 방식&lt;/td&gt;
&lt;td style=&quot;width: 28.3722%;&quot;&gt;사전 정의된 메트릭 위주&lt;/td&gt;
&lt;td style=&quot;width: 34.0697%;&quot;&gt;보다 포괄적인 데이터 스트림, 로그, 트레이스, 이벤트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.5581%;&quot;&gt;세분화&lt;/td&gt;
&lt;td style=&quot;width: 28.3722%;&quot;&gt;일반적으로 호스트와 에이전트를 설치하고 외부 관점에서 메트릭을 캡쳐&lt;/td&gt;
&lt;td style=&quot;width: 34.0697%;&quot;&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;엄밀하게 따지자면 모니터링과 가시성은 다른 개념입니다. 예를 들어 AWS에서 기본적으로 제공하는 EC2, RDS 등의 모니터링 화면은 모니터링의 적절한 예시일 것입니다. 반면 데이터독과 같은 플랫폼은 RCA 까지 제공하는 가시성의 적절한 예시입니다.&lt;/p&gt;
&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; Goole의 SRE 가이드의 &lt;a href=&quot;https://sre.google/sre-book/monitoring-distributed-systems/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;분산 시스템 모니터링 섹터&lt;/a&gt;에서 가시성에 대한 엄밀한 정의 없이&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; color: #424242; text-align: start;&quot;&gt;&lt;b&gt;Root cause&lt;/b&gt; 를 다루고 있는걸 보면 실무자에게 모니터링과 가시성은 굳이 엄밀히 구분지을 필요가 없는 개념일지도 모릅니다.&amp;nbsp;&lt;/span&gt;어쩌면 이미 모니터링툴이라고 사용하고 있는 많은 솔루션들에 가시성의 개념이 녹아있기 때문일지도 모르지요.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정말 가시성이 필요한가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;개발자들과 이야기를 하다보면&lt;span&gt;&amp;nbsp;&lt;b&gt;지금도&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;로그로 오류를 잡고 AWS CloudWath 대시보드로 모두 파악이 되는데 굳이 가시성이라는 분야에 투자를 또 해야하나?&lt;/b&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;b&gt;MTTR(Mean Time To Recovery) x SPT(Sales Per Time) + a&lt;/b&gt; 를 따를 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;a 는 시스템 장애에 실망해서 떠나가는 고객들이 만들어냈을 잠재 매출이며 a MTTR에 대한 함수입니다. 즉, 장애복구에 시간이 길어지면 길어질 수록 이탈 고객도 많아지는 것이지요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모니터링만으로는 장애발생 근본 원인(Root Cause)를 빠르게 밝히기 어렵습니다. 로그를 뒤져서 밝혀내는 것보다 한번에 파악이 가능한 경우가 MTTR을 줄일 수 있겠죠? 가시성을 확보한다면 MTTR을 줄이는 것은 물론이고 잠재적인 오류 발생 가능성도 파악할 수 있습니다.&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;b&gt;마이크로서비스로 구축되었고 심지어 이벤트 기반 아키텍처라면 단일 로그 스트림으로는 오류 원인을 찾기 굉장히 힘들어&lt;/b&gt;집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 하나씩 추가되어 시스템 토폴로지가 계단식으로 복잡해질 때 가시성에 대한 투자는 선형이 아니 지수적인 가치를 만들어낼것입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;조직의 속도와 개발자 경험&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;릴리스 주기가 짧아질수록 신규 코드, 인프라가 무슨 부작용을 일으키는지 빠르게 파악해야합니다. 그래야 배포 속도를 유지하고 개발자들도 용기를 가질 수 있습니다. 또한 가시성 없이 계속 시스템이 복잡해진다면 디버깅 -&amp;gt; 야근 -&amp;gt; 이직률 상승이라는 악순환이 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 가시성이란 장애에 대응하는 속도(MTTR)를 줄임으로써 서비스 품질 유지(매출), 개발 조직의 안정성 향상까지 기여할 수 있는 개념입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가시성을 확보하는 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가시성이 중요하다는 건 알겠는데, 실제로 어떤 신호(signal)를 모아야 MTTR을 줄일 수 있는 걸까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대적인 가시성은 보통 &lt;b&gt;3대 시그널과 +a&lt;/b&gt; 를 많이 이야기합니다. Logs ,Metrics, Traces 그리고 Profiling이 그것입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 87px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 15.3488%; height: 19px; text-align: center;&quot;&gt;시그널&lt;/td&gt;
&lt;td style=&quot;width: 28.7209%; height: 19px; text-align: center;&quot;&gt;From&lt;/td&gt;
&lt;td style=&quot;width: 29.4187%; height: 19px; text-align: center;&quot;&gt;What&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 15.3488%; height: 17px;&quot;&gt;Logs&lt;/td&gt;
&lt;td style=&quot;width: 28.7209%; height: 17px;&quot;&gt;어플리케이션 라이브러리&lt;/td&gt;
&lt;td style=&quot;width: 29.4187%; height: 17px;&quot;&gt;상태 변화, 예외 스택 수집&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 15.3488%; height: 17px;&quot;&gt;Metrics&lt;/td&gt;
&lt;td style=&quot;width: 28.7209%; height: 17px;&quot;&gt;라이브러리 Exprter 가 주기적으로 전송&lt;/td&gt;
&lt;td style=&quot;width: 29.4187%; height: 17px;&quot;&gt;수량, 속도(QPS, CPU, P99 지연) 등, 이를 통해 알림과 대시보드를 만듦&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 15.3488%; height: 17px;&quot;&gt;Traces&lt;/td&gt;
&lt;td style=&quot;width: 28.7209%; height: 17px;&quot;&gt;요청-응답 체인에 따라 자동 삽입된 스캔&lt;/td&gt;
&lt;td style=&quot;width: 29.4187%; height: 17px;&quot;&gt;분산 호출 순서, 지연 구간, 오류 지점 파악&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 15.3488%; height: 17px;&quot;&gt;Profiling&lt;/td&gt;
&lt;td style=&quot;width: 28.7209%; height: 17px;&quot;&gt;커널 이벤트와 애플리케이션 스택&lt;/td&gt;
&lt;td style=&quot;width: 29.4187%; height: 17px;&quot;&gt;함수, 메서드 라인 단위 CPU 메모리, I/O Hotspot 파악&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;b&gt;&amp;ldquo;에러가&amp;nbsp;났다(Logs)&amp;nbsp;&amp;rarr;&amp;nbsp;어느&amp;nbsp;구간이&amp;nbsp;느렸다(Metrics)&amp;nbsp;&amp;rarr;&amp;nbsp;어떤&amp;nbsp;호출&amp;nbsp;체인에서&amp;nbsp;지연이&amp;nbsp;컸다(Traces)&amp;nbsp;&amp;rarr;&amp;nbsp;해당&amp;nbsp;함수가&amp;nbsp;CPU를&amp;nbsp;먹었다(Profiling)&amp;rdquo;&lt;/b&gt;&amp;nbsp;식의&amp;nbsp;원인&amp;nbsp;추적&amp;nbsp;체인이&amp;nbsp;완성됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- CNCF, Observability: &lt;a href=&quot;https://glossary.cncf.io/observability/?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://glossary.cncf.io/observability/?utm_source=chatgpt.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Wikipedia, Observability: &lt;a href=&quot;https://en.wikipedia.org/wiki/Observability_%28software%29?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://en.wikipedia.org/wiki/Observability_%28software%29?utm_source=chatgpt.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Splunk, Observability: &lt;a href=&quot;https://www.splunk.com/en_us/blog/learn/observability.html?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.splunk.com/en_us/blog/learn/observability.html?utm_source=chatgpt.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>탐구 생활/개발 탐구</category>
      <category>observability</category>
      <category>가시성</category>
      <category>가시성 3대 요소</category>
      <category>가시성이란?</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/109</guid>
      <comments>https://probehub.tistory.com/109#entry109comment</comments>
      <pubDate>Sun, 6 Jul 2025 14:28:47 +0900</pubDate>
    </item>
    <item>
      <title>Python Monorepo - uv Docker 다이어트하기</title>
      <link>https://probehub.tistory.com/103</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;b&gt;Docker 이미지가 180MB&lt;/b&gt;에 달하고 &lt;b&gt;Runtime 에는 Idle 상태에서 메모리를 120MB&lt;/b&gt; 를 사용하고 있었습니다. Python alpine 도 아니고 slim 이미지 기반인데 왜 이렇게 메모리를 비효율적으로 쓸까? 호기심이 생겼고 문제를 분석했습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;맥락 요약 (이전 글 안본 분들을 위해)&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;프로젝트 구조&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트의 디렉터리 구조는 아래와 같이 packages 에 공통모듈을 정의하고 services 에서는 해당 공통모듈을 끌어다가 FastAPI 와 gRPC 서버를 돌리는 코드가 위치해 있습니다. 그리고 프로젝트 root 에 pyproject.toml, packges 안의 모든 모듈에 pyproject.toml 이, services 안의 모든 서비스에 pyproject.toml 이 위치해 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Dockerfile&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개별 서비스마다 하나의 Dockerfile 이 존재하며 하나의 Dockerfile 은 아래의 형태를 따르고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv

# 작업 디렉토리
WORKDIR /app

# 필요한 파일들만 복사
ADD pyproject.toml /app/pyproject.toml
ADD uv.lock /app/uv.lock
ADD packages /app/packages
ADD services/a_service /app/services/a_service

RUN uv sync --all-packages

WORKDIR /app/services/a_service

ENV PYTHONPATH=/app:/app/services/a_service

EXPOSE 8000
CMD [&quot;uv&quot;, &quot;run&quot;, &quot;uvicorn&quot;, &quot;app.main:app&quot;, &quot;--workers 2&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;원인 분석&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. Entrypoint 에서 uv run 재호출&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;uv 를 이용해서 sync 한 상태에서 다시 Docker Entrypoint 에서 uv 를 호출하고 있습니다. &lt;a href=&quot;https://docs.astral.sh/uv/reference/cli/#uv-run&quot;&gt;uv run&lt;/a&gt; 명령어는 &lt;b&gt;&quot;어떤 Python 명령이나 스크립트를 실행할 때, 그 실행이 항상 알맞은 Python 가상환경 안에서 이뤄지도록 보장&quot;&lt;/b&gt; 해줍니다. 즉 단순히 Python 코드 실행보다 더 많은 일을 하게 됩니다. 예를들어 필요한 라이브러리 설치와 같은 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Entrypoint 에서 uv run 을 시행하면 기존 package 를 remove 하고 다시 add 하는 동작이 일어납니다. 이러한 행위는 예상치 못한 결과를 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/astral-sh/uv/issues/14013&quot;&gt;게다가 uv sync 에서 --frozen 옵션을 이용한 뒤 다시 uv run 을 호출하면 어떤 이유에선지는 모르지만, Docker 컨테이너 Idel 상태에서의 메모리 사용량이 증가하는 현상이 나타납니다.&lt;/a&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. Build vs Run 경계 모호&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;uv 는 Python 실행 도구가 아니라 패키지 관리 도구&lt;/b&gt; 입니다. 그런데 uv sync 를 통해서 이미 프로젝트 환경에 필요한 의존성을 모두 설치한 뒤에도 uv 를 계속 들고 있습니다. 굳이 그럴 필요가 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Build (Python 에서 Build 는 의존성 설치 과정) 를 끝마치면 uv 를 없애는게 Docker 이미지 크기를 줄이는 방법일 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;해결전략&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. &lt;code&gt;.venv&lt;/code&gt; PATH 주입&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 Docker 내부 SYSTEM PATH 에 &lt;code&gt;.venv&lt;/code&gt; 를 지정함으로써 uv run 을 사용하지 않아도 직접 uvicorn 을 호출할 수 있습니다. 이를 이용하면 Entrypoint 에서 굳이 uv run 을 호출하지 않아도 됩니다.&lt;/p&gt;
&lt;pre class=&quot;julia&quot;&gt;&lt;code&gt;# .venv PATH 주입
ENV PATH=&quot;/app/.venv/bin:$PATH&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. Multi-stage Build&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Builder Stage 와 Runtime Stage 를 나누면 uv 를 없앤 Docker 이미지를 만들 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Builder Stage: uv 로 .venv 완성 후 /app 복사&lt;/li&gt;
&lt;li&gt;Runtime Stage: 슬림 이미지 + /venv 만 사용, uv 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 최종 Dockerfile&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# Builder stage
FROM python:3.12-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv

ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
ENV UV_NO_PYTHON_DOWNLOADS=1

WORKDIR /app

ADD pyproject.toml /app/pyproject.toml
ADD packages /app/packages
ADD services/a_service services/a_service

RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --all-packages --no-install-project --no-dev

# Runtime stage
FROM python:3.12-slim

RUN groupadd -r app &amp;amp;&amp;amp; useradd -r -g app app

COPY --from=builder --chown=app:app /app /app

ENV PATH=&quot;/app/.venv/bin:$PATH&quot;

ENV PYTHONPATH=/app:/app/services/a_service

USER app
WORKDIR /app/services/a_service

EXPOSE 8000 50051
CMD [&quot;uvicorn&quot;, &quot;app.main:app&quot;, &quot;--workers 2&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 Docker 이미지 크기를 20%~30%정도 줄일 수 있었고, Idle 상태의 Docker 컨테이너의 메모리 사용량도 50%가량 줄일 수 있었습니다.&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;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;삽질:&lt;/b&gt; &lt;b&gt;uv sync --all-packages 를 사용한게 문제는 아닌가?&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;가설&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;모든 workspace 를 설치하니 이미지가 커지나?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;`uv sync --all-packages` 를 호출하면 Workspace 의 모든 가상환경에서 해당 프로젝트의 모든 라이브러리를 &lt;b&gt;'프로젝트 환경'&lt;/b&gt;에 설치하고 Monorepo 사이즈가 커짐에 따라 자연스럽게 Docker 이미지와 컨테이너의 메모리가 사용량도 커질 수 있다&quot; 는 가설이 있었습니다.&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;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;증명&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 가설은 틀렸는데, 그 이유는 uv sync 의 동작 방식 때문입니다.&lt;/p&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://docs.astral.sh/uv/concepts/projects/sync/&quot;&gt;공식문서&lt;/a&gt;에는 Syncing은 lockfile(uv.lock) 에 근거하여 packages 일부를 프로젝트 환경에 설치한다고 합니다. (Syncing is the process of installing a subset of packages from the lockfile into the project environment)&lt;/li&gt;
&lt;li&gt;그리고 &lt;a href=&quot;https://docs.astral.sh/uv/concepts/projects/layout/#the-lockfile&quot;&gt;또 다른 공식문서&lt;/a&gt;를 통해는 uv sync 명령어를 실행하는 동안 lockfile 이 만들어진다는 것을 알 수 있습니다. (The lockfile is automatically created and updated during uv invocations that use the project environment, i.e., uv sync and uv run.)&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;즉 uv sync --all-packages 를 통해 프로젝트 환경에 라이브러리를 만든다면 아래의 동작이 일어나는 것입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;uv sync --all-packages 를 입력한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모든 Workspace의 pyproject.toml을 돌아다니며 uv.lock 파일과 비교하여 uv.lock 파일을 업데이트.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;만약 uv.lock 파일이 없다면 현재 소스코드에 근거하여 새롭게 uv.lock 파일을 만든다.&lt;/li&gt;
&lt;li&gt;uv.lock 파일에 따라서 프로젝트 환경에 필요한 라이브러리를 설치한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 실행한 테스트에서 uv.lock 을 포함한 상태에서 Dockerfile 을 빌드했을때와 uv.lock을 포함하지 않고 Dockerfile 을 빌드했을때의 사이즈가 동일했기 때문에 2번의 소스코드와 비교하여 uv.lock 파일을 &lt;b&gt;&quot;업데이트&quot;&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;p data-ke-size=&quot;size16&quot;&gt;이러한 과정 때문에 uv.lock 파일이 있는 경우와 없는 경우에 최종 Dockerfile 의 크기에 유의미한 차이가 없으며 결과적으로 Runtime(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;차차리 다른 관점에서 uv.lock 파일이 없을때 비교, 업데이트하는 과정이 없어서 이미지 빌드 속도가 조금 더 빠를 수 있겠다는 생각은 듭니다.&lt;/p&gt;</description>
      <category>탐구 생활/Python Monorepo</category>
      <category>uv docker bestpractice</category>
      <category>uv dockerfile</category>
      <category>uv dockerfile bestpractice</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/103</guid>
      <comments>https://probehub.tistory.com/103#entry103comment</comments>
      <pubDate>Sun, 29 Jun 2025 18:13:32 +0900</pubDate>
    </item>
    <item>
      <title>Python Monorepo - namespace 활용하기</title>
      <link>https://probehub.tistory.com/102</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제점: 잘못된 이름으로 만들어진 공통모듈&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://probehub.tistory.com/97&quot;&gt;직전 글&lt;/a&gt;에서 uv 를 이용해서 Python Monorepo 를 구성하였습니다. 대략 이런 모습이었죠. shared 에 있는 모듈을 services 에서 끌어다 쓰는 구조입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;현재상황.png&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/roa7f/btsOAWIIm1r/yC7I47yaJkOf0ZlKTkauX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/roa7f/btsOAWIIm1r/yC7I47yaJkOf0ZlKTkauX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/roa7f/btsOAWIIm1r/yC7I47yaJkOf0ZlKTkauX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Froa7f%2FbtsOAWIIm1r%2FyC7I47yaJkOf0ZlKTkauX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;620&quot; height=&quot;339&quot; data-filename=&quot;현재상황.png&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;362&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조 자체는 괜찮은데, 저의 미숙함때문에&amp;nbsp;&lt;b&gt;치명적인 문제&lt;/b&gt;가 생겼습니다. 바로 &lt;b&gt;너무 일반적인 이름을 사용&lt;/b&gt;했다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;services 하위의 app에서도 언제든지 exception, logger 와 같은 파일을 정의해서 사용할 수 있기에 충돌 가능성이 있습니다. 즉, 공통모듈을 사용하는 개발자 개개인이 조심해야하는 문제점이 생깁니다. 실무에 집중해야하는 개발자에게 큰 스트레스겠죠?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결했던 과정을 공유하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;가장 일반적으로 문제를 해결하는 방식은 shared 밑에 각각의 모듈을 두는 것입니다. 현재 디렉터리 구조상으로만 그런게 아니라 실제 Python 가상환경 안에 설치된 패키지의 구조가 그렇게 인식되어야 하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 된다면 IDE가 아래와 같은 가이드라인을 제공해줄 수 있겠죠.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;원하는 결과물.png&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYM88F/btsOBYMhdSp/eUfDBXAAMNITgw64AGTXM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYM88F/btsOBYMhdSp/eUfDBXAAMNITgw64AGTXM0/img.png&quot; data-alt=&quot;shared 밑에 공통 모듈들이 있습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYM88F/btsOBYMhdSp/eUfDBXAAMNITgw64AGTXM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYM88F%2FbtsOBYMhdSp%2FeUfDBXAAMNITgw64AGTXM0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;620&quot; height=&quot;171&quot; data-filename=&quot;원하는 결과물.png&quot; data-origin-width=&quot;1134&quot; data-origin-height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;shared 밑에 공통 모듈들이 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;패키지 자체를 계층화하는 방법&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 이런 결과를 얻을 수 있을까요? 아래와 같이 shared 라는 디렉터리 자체를 패키지로 만들고 exception, logger 를 내부 패키지로 만들어야 하는 걸까요? 물론 유효한 접근입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749820567242&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.shared
├── __init__.py
│   ├── config
│   └── proto
├── exception
│   ├── __init__.py
│   └── ...
├── logger
│   ├── __init__.py
│   └── ...
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 된다면 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;실제로는 모든 서비스가 공통으로 사용하지 않는 기능이&lt;/span&gt; 공통 모듈에 포함되어 공통 모듈이 불필요하게 커질 우려가 있습니다. 그리고 개별 단위 모듈을 분리해서 관리하고자하는 monorepo 의 철학과도 맞지 않습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Build Target을 이용한 패키지 빌드&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://probehub.tistory.com/97&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글&lt;/a&gt;에서 Python 패키지 관리에서 사용되는 &lt;b&gt;빌드&lt;/b&gt;라는 용어를 아래와 같이 설명했습니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Python 코드를 가진 소스 디렉터리를 다른 환경에서 설치가 가능한 아카이브(.whl, .tar.gz)로 변환하는 과정을 말합니다.&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 코드는 따로따로 작성하지만 그들을 빌드하는 과정에서 하나의 아카이브로 모을 수 있다면, 그리고 그 아카이브의 최상위 모듈이 shared 라면 우리가 원하는 형태의 결과물을 만들 수 있지 않을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히도 Python build-backend 는 &lt;a href=&quot;https://hatch.pypa.io/1.9/config/build/#build-targets&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;build target&lt;/a&gt; 이라는 spec으로 이러한 기능을 지원해야함을 명시하였고, hatchling은 이 spec을 따르기 때문에 &lt;b&gt;&lt;span style=&quot;color: #6791e0; text-align: start;&quot;&gt;[tool.hatch.build.targets.&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color: #6791e0; text-align: start;&quot;&gt;TARGET_NAME&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color: #6791e0; text-align: start;&quot;&gt;]&lt;/span&gt;&lt;/b&gt; 테이블을 통해 이러한 기능을 제공하고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Target Wheel 을 이용하기 위한 디렉터리 구조&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hatcgling 자체가 src 디렉터리를 요구하는 등 디렉터리 구조에 영향을 받습니다. 그러다보니까 사용하고자 하는 namespace 를 &lt;b&gt;{module_name}/src/{namespace}/{module_name}&lt;/b&gt; 식으로 구성해 줘야합니다. 그 예시는 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-13 오후 10.35.51.png&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cks2Kn/btsOBQ1Y4TB/KWMty58qOsGyi3e1nUkKyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cks2Kn/btsOBQ1Y4TB/KWMty58qOsGyi3e1nUkKyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cks2Kn/btsOBQ1Y4TB/KWMty58qOsGyi3e1nUkKyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcks2Kn%2FbtsOBQ1Y4TB%2FKWMty58qOsGyi3e1nUkKyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;620&quot; height=&quot;507&quot; data-filename=&quot;스크린샷 2025-06-13 오후 10.35.51.png&quot; data-origin-width=&quot;656&quot; data-origin-height=&quot;536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;shared 가 namespace 로 빠지면서 packages라는 이름을 최상단으로 만들었습니다. 이러한 이름은 프론트엔드 진영의 예시를 따른 것으로 백엔드에서는 최적의 네이밍이 아닐 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 사실이 있습니다. 바로 각 shared namespace 를 공유할 모듈들이 /src/shared/__init__.py 를 만들지 않는다는 것입니다. 그 이유는 &lt;a href=&quot;https://peps.python.org/pep-0420/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;PEP 420, Implicit Namespace&lt;/b&gt;&lt;/a&gt;&amp;nbsp;와 깊은 관계가 있습니다. 암묵적 네임스페이스(Implicit Namespace)로 인해 디렉터리만 있으면 패키지로 인식되게 되었습니다. 따라서 일반 패키지는 하나의 디렉터리와 하나의 __init__.py 만 가질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 만약 모든 shared namespace 를 공유하는 상황이 온다면 N 개의 wheel 안에 동일 경로 + 동일 파일명이 중복되고, 실제 서비스 코드에서는 shared.logger, shared.middleware 를 통한 import 가 ModuleNotFoundError 를 발생시키게 됩니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Target Wheel 을 이용하기 위한 pyproject.toml&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 디렉터리 구성을 맞췄다면 해당 공통 모듈의 pyproject.toml 이 아래와 같이 세팅되어야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1749869592549&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[project]
name = &quot;shared.middleware&quot;
version = &quot;0.1.0&quot;
description = &quot;Add your description here&quot;
requires-python = &quot;&amp;gt;=3.12&quot;
dependencies = [
    &quot;asgiref&amp;gt;=3.8.1&quot;,
    &quot;shared.logger&quot;,
]

[tool.uv.sources]
&quot;shared.logger&quot; = { workspace = true }


[build-system]
requires = [&quot;hatchling&quot;]
build-backend = &quot;hatchling.build&quot;

[tool.hatch.build.targets.wheel]
packages = [&quot;src/shared&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;다소 낯설어졌습니다. middleware 라는 공통 모듈은 앞에 &lt;b&gt;shared.&lt;/b&gt; prefix 가 붙었으며 심지어 동일한 workspace 에 있는 다른 모듈을 참고할때도 해당 모듈의 prefix 인 &lt;b&gt;shared &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;이러한 세팅을 통해 이제 동일한 packages 안에 있는 모듈이라고 할지라도 어떤 모듈은 shared namespace 를 공유하며, 어떤 모듈은 grpc_client 라는 모듈을 공유할 수 있게 되었습니다. 덕분에 packages 를 끌어다 쓰는 다른 개발자들도 더 명시적으로 자신이 끌어다 쓰는 모듈이 어떤것인지 파악할 수 있게되어 불편이 해소되었습니다.&lt;/p&gt;</description>
      <category>탐구 생활/Python Monorepo</category>
      <category>python hatcling namespace</category>
      <category>python monorepo namespace</category>
      <category>python namespace</category>
      <category>python shared module</category>
      <category>python 공통모듈</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/102</guid>
      <comments>https://probehub.tistory.com/102#entry102comment</comments>
      <pubDate>Sat, 14 Jun 2025 12:47:27 +0900</pubDate>
    </item>
    <item>
      <title>Python MSA를 위한 Monorepo 구성기 &amp;mdash; 왜 uv를 선택했는가</title>
      <link>https://probehub.tistory.com/97</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Python&amp;amp;FastAPI 위주로 MSA 를 구성하면서 codebase 는 monorepo 형태를 차용했습니다. 이 글에서는 Python으로 MSA를 만들 때 &lt;code&gt;uv&lt;/code&gt;를 통해 어떻게 monorepo 형태로 구성할 수 있는지 경험을 공유하고자 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;monorepo 란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;monorepo 는 &lt;i&gt;polyrepos&lt;/i&gt;(혹은&amp;nbsp;multi-project)에 대응되는 개념으로, &lt;b&gt;하나의 Git 저장소&lt;/b&gt;에서 여러 모듈(서비스‧라이브러리)을 함께 관리하는 전략을 말합니다. 동일한 논리적 변경을 하나의 Commit 또는 PR(Review)로 묶을 수 있고, 저장소별 권한&amp;middot;CI/CD 파이프라인을 중복 정의할 필요가 없다는 점이 가장 큰 장점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 &amp;ldquo;거대한 모놀리식 시스템(모놀리식 코드베이스)과 같다&amp;rdquo;는 오해도 있지만, &lt;b&gt;코드 관리 방식&lt;/b&gt;과 &lt;b&gt;런타임 아키텍처&lt;/b&gt;는 분리해서 봐야 합니다. 모놀리스는 배포 단위가 하나지만, monorepo는 &lt;u&gt;여러 독립 실행 단위(컨테이너&amp;middot;람다&amp;middot;패키지)를 한 저장소에 둘 뿐&lt;/u&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;m_vs_m.png&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;553&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NeZDl/btsOkG5Zeaf/aZzqfcNglM9seyGorgcit0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NeZDl/btsOkG5Zeaf/aZzqfcNglM9seyGorgcit0/img.png&quot; data-alt=&quot;https://monorepo.tools/#what-is-a-monorepo&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NeZDl/btsOkG5Zeaf/aZzqfcNglM9seyGorgcit0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNeZDl%2FbtsOkG5Zeaf%2FaZzqfcNglM9seyGorgcit0%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;650&quot; height=&quot;316&quot; data-filename=&quot;m_vs_m.png&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;553&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://monorepo.tools/#what-is-a-monorepo&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://circleci.com/blog/monorepo-dev-practices/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CircleCI의 글&lt;/a&gt;에서는 monorepo 채택에 따른 이점을 다음과 같이 제시합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-ke-size=&quot;size16&quot;&gt;
&lt;li&gt;&lt;b&gt;원자적 변경(Atomic change)&lt;/b&gt;: API 스펙 변경과 각 서비스 클라이언트 수정이 한 커밋에 들어가므로 깨지는 중간 단계가 사라짐&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관된 개발 경험&lt;/b&gt;: 공통 린터, 테스트, 릴리스 스크립트를 한곳에 유지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쉬운 코드 리팩터링&lt;/b&gt;: 대규모 네임스페이스 변경&amp;middot;공통 모듈 추출을 한 PR에서 끝낼 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 우리 팀은 monorepo 를 도입했을때 위의 이유로 개발 편의성이 향상되고 이로인해 human error 발생 빈도를 줄일 수 있었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;monorepo 와 multi-module&lt;/b&gt;&lt;br /&gt;Gradle 기반 SpringBoot를 쓰던 저에게는 &lt;i&gt;multi-module&lt;/i&gt;이 더 익숙한 개념이었고 monorepo 라는 개념을 받아들일때 도대체 &lt;i&gt;multi-module&lt;/i&gt;과 다를게 뭔가 궁금했습니다. 결국 저는 &amp;ldquo;&lt;i&gt;단일 코드베이스에 여러 모듈&lt;/i&gt;&amp;rdquo;이라는 점은 동일하지만, &amp;nbsp;&amp;bull; multi-module은 &lt;u&gt;빌드 시스템(Gradle) 관점&lt;/u&gt;, &amp;nbsp;&amp;bull; monorepo는 &lt;u&gt;저장소‧협업 관점&lt;/u&gt;의 용어라는 차이가 있을 뿐이라고 결론을 내렸습니다. 이 글에서는 통일하여 &lt;b&gt;monorepo&lt;/b&gt;라 부르겠습니다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;패키지,&amp;nbsp;프로젝트&amp;nbsp;관리의&amp;nbsp;필요성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;monorepo 구조까지는 좋았습니다. 하지만 우리 팀은 { polyrepo, Monolithic, Java&amp;amp;SpringBoot }에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;{ monorepo, MSA, Python&amp;amp;FastAPI }&lt;/b&gt;로 전환을 하였고 대대적인 패러다임&amp;middot;언어&amp;middot;아키텍처 변화를 겪다보니 제대로된&lt;span&gt;&amp;nbsp;&lt;/span&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;처음에는 pip-tools 를 이용해 개별 서비스마다 별도의 requirements.in 과 requirement.txt 파일을 두고 local 개발환경에서 개별 서비스마다 가상환경(venv)을 별도로 지정하게 되었습니다. 처음 두세번의 배포까지는 아무런 문제가 없었습니다. &lt;b&gt;그런데 점점 서비스간 패키지 버전이 불일치하는 일&lt;/b&gt;이 발생했습니다. 서비스간 패키지 버전이 불일치한다는 것은 A 서비스에서는 제공되는 기능이 B 서비스에서는 제대로 동작하지 않는문제가 발생할 수 있다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개별 서비스마다 별도의 가상환경을 두는 선택이 문제를 더욱 심각하게 만들었습니다. local 개발환경에서 A 서비스의 가상환경을 환경을 활성화해놓고 B 서비스의 코드를 수정, 테스트하면 분명 문제없이 동작하던 기능이 (A 가상환경이었기 때문에) 컨테이너화하여 Pod 에 올라가면 K8s의 Liveness probe 에서 막히는 일이 발생했으며, 더 심각하게는 전혀 예상치 못한 RuntimeException 이 발생하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론&amp;nbsp;컨테이너화해서&amp;nbsp;테스트하는&amp;nbsp;절차를&amp;nbsp;철저하게&amp;nbsp;지킨다면&amp;nbsp;위의&amp;nbsp;문제가&amp;nbsp;발생하기&amp;nbsp;전에&amp;nbsp;알아차릴&amp;nbsp;수&amp;nbsp;있었겠지만&amp;nbsp;Test&amp;nbsp;Coverage&amp;nbsp;를&amp;nbsp;적정&amp;nbsp;수준으로&amp;nbsp;유지해야하므로&amp;nbsp;현실적으로&amp;nbsp;이런&amp;nbsp;상황에서&amp;nbsp;모든&amp;nbsp;RuntimeException을&amp;nbsp;막기는&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;따라서 패키지간 버전 불일치 문제를 해결하고 local 환경에서 하나의 가상환경에서 여러 서비스를 개발할 수 있는 패키지, 프로젝트관리 체계가 필요해졌습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;uv 로 monorepo 만들기&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 uv인가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 우리 팀은 uv 와 workspace, build-system을 활용하여 이러한 문제를 해결했습니다. 그런데 Python 생태계에는 많은 패키지 관리 툴이 존재합니다. 그 중에서 왜 uv 를 선택했을까요? 저희는 제대로된 패키지 관리 체계를 도입하기 위해 아래의 요구사항을 세웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우리가 세운 최소 요구사항&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항을 만들다보니 &lt;b&gt;&quot;공통 모듈&quot;&lt;/b&gt; 이라는 개념이 추가 되었는데 이것 역시 MSA를 운용하다보면 필요성이 절실해지는 부분입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-ke-size=&quot;size16&quot;&gt;
&lt;li&gt;아키텍처(arm64/amd64)&amp;middot;OS&amp;middot;Python 버전별 &lt;b&gt;재현 가능한(Immutable) Lockfile&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;모든 마이크로서비스가 &lt;u&gt;단일 Lockfile&lt;/u&gt;을 공유해 &lt;b&gt;버전 불일치&lt;/b&gt;를 막을 것&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공통 모듈&lt;/b&gt;을 &lt;code&gt;shared/&lt;/code&gt; 하위 패키지로 두고 필요한 서비스가 &amp;ldquo;로컬(Editable) 의존성&amp;rdquo;으로 끌어다 쓸 것&lt;/li&gt;
&lt;li&gt;의존성 해석과 설치 속도가 빠를 것(로컬 캐시&amp;middot;병렬 빌드 지원)&lt;/li&gt;
&lt;li&gt;Docker나 CI 환경에서 &lt;b&gt;pip만큼 가볍게&lt;/b&gt; 쓸 수 있을 것(&lt;code&gt;curl
    /uv&lt;/code&gt; 한 줄 설치)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;패키지 관리툴 간단 비교&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;툴&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;Lockfile&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;Workspace/Monorepo&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;패키지 설치속도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;pipenv&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;pipfile.lock&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;미지원&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;느림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;Poetry&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;poetry.lock&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;Partial(패키지 경로)&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;보통&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;PDM&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;pdm.lock&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;PEP 582 기반 (글로벌 Cache)&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;Hatch&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;hatch.lock&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;플로그인으로 지원&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;보통&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;uv&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;uv.lock&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;Cargo Workspace 개념 지원&lt;/td&gt;
&lt;td style=&quot;width: 20%;&quot;&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;code&gt;uv&lt;/code&gt;는 Rust로 작성되어 pip 대비 8~10배, Poetry 대비 4~5배 빠른 의존성 해석&amp;middot;설치 속도를 보여줍니다. 또한 Cargo와 같은 &lt;b&gt;workspace&lt;/b&gt; 기능을 제공해, 여러 패키지가 &lt;u&gt;하나의 &lt;code&gt;uv.lock&lt;/code&gt;&lt;/u&gt;을 공유하도록 강제합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reddit 등 커뮤니티 피드백에서도 &amp;ldquo;Poetry와 기능 동등성(parity)을 달성했고, monorepo 이동 시 버전 지옥을 해소했다&amp;rdquo;는 후기가 많았으며, 한국의 Python 사용자들도 uv 를 적극권장한다는 이야기를 전했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 원하는 요구사항을 모두 충족하면서, 속도도 빠르고, 국내외 사용자들의 후기도 좋았기에 uv를 제외한 다른 툴을 선택할 이유가 없었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;uv 의 주요 개념&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. pyproject.toml, uv.lock 파일&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 pyproejct.toml 파일과 lock 파일은 uv 만의 개념은 아닙니다. 하지만 Python 패키지 관리 툴을 처음 접하는 사람에게는 둘다 생소한 개념이므로 이 글에서 설명합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pyproject.toml 파일은 개념적으로 이해하자면 &lt;b&gt;개발자가 원하는 명세를 작성하는 파일&lt;/b&gt;입니다. 이 프로젝트의 이름과 설명, 상황별로 원하는 라이브러리와 그 버전을 명시합니다. 필요하다면 Lint 설정도 가능합니다. 그리고 이러한 개발자가 작성한 명세에 따라 실제 원하는 것들을 모아서&lt;b&gt; 그 상태를 저장한 파일이 uv.lock 파일&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;실제 Python 어플리케이션이 구동하기 위한 가상환경의 구성은 1차적으로 uv.lock 파일에 의존합니다. 만약 uv.lock 파일이 없다면 pyproject.toml 파일을 통해 바로 uv.lock 파일을 생성할 수 있습니다.&lt;/p&gt;
&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://packaging.python.org/en/latest/guides/writing-pyproject-toml/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;pyproject.toml 공식문서&lt;/a&gt;, &lt;a href=&quot;https://docs.astral.sh/uv/concepts/projects/sync/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;uv.lock 공식문서&lt;/a&gt;를 참조하시는걸 추천드립니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. workspace&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;uv에는&amp;nbsp;&lt;a href=&quot;https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Rust Cargo Workspace&lt;/a&gt; 개념이 적용되었습니다. Rust의 패키지관리 툴인 Cargo에서 Workspace란 크기가 커진 Rust 라이브러리를 잘게 쪼개서 관리할 수 있도록, 즉 크기가 작아진 개별 라이브러리간 상호 참조를 손쉽게 할 수 있도록 도와주는 장치입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;uv에서 동일한 workspace에 '공통 모듈'과 '마이크로서비스'들이 등록된다면 동일한 uv.lock 파일을 공유하게되고 마이크로서비스들이 공통 모듈을 손쉽게 참조할 수 있는 관계를 만들 수 있게 됩니다. 이러한 참조 관계는 path 참조를 통해서도 이뤄질 수 있으며 혹은 가상환경안에 해당 공통모듈을 설치하여 참조할 수도 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. build-backend&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build-backend 역시 대부분의 Python 패키지 관리 툴이 공유하는 개념으로 &lt;b&gt;어떤 방식으로 해당 패키지를 빌드&lt;/b&gt;해서 가상환경에 추가할지를 지정하는 것입니다. 그런데 설명이 좀 이상합니다. &lt;b&gt;Python은 인터프리터 언어이므로 빌드라는 과정이 없는데&lt;/b&gt;&amp;nbsp;도대체 뭘 빌드한다는 걸까요?&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;733&quot; data-start=&quot;579&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 엄밀하게 설명하자면 build-backend 들은 Python 코드를 가진 &lt;b&gt;소스 디렉터리를 다른 환경에서 설치가 가능한 아카이브(.whl, .tar.gz)로 변환하는 과정을 빌드&lt;/b&gt;라고 말합니다. 이렇게 아카이브로 변환하는 과정을 거쳐야 패키지 설치 속도와 운영체제 호환성이 확보됩니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;uv 를 이용한 MSA monorepo 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 코드는 &lt;a href=&quot;https://github.com/timothy-jeong/monorepo-example/tree/non-target-whell-version&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;깃헙&lt;/a&gt;에서 확인하실 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 공통모듈은 모두 build-backend 를 활용하여 아카이브 파일 형태로 가상환경에 포함된다는 것입니다. 이러한 조치를 통해 각 서비스가 배포되는 환경이 변화되어도 안정한 동작을 보장할 수 있고, 무엇보다 IDE가 해당 패키지의 존재 여부를 명확하게 파악할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요하다면 마이크로서비스로 아카이브파일로 바꿔서 사용하는 방법을 구현할 수도 있겠으나, 현재는 Dockerfile 에서 해당 파일을 직접 호출하는 형태이므로 불필요하다고 판단했습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://monorepo.tools/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://monorepo.tools&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.nrwl.io/misconceptions-about-monorepos-monorepo-monolith-df1250d4b03c&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://blog.nrwl.io/misconceptions-about-monorepos-monorepo-monolith-df1250d4b03c&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://engineering.linecorp.com/ko/blog/mono-repo-multi-project-gradle-plugin&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://engineering.linecorp.com/ko/blog/mono-repo-multi-project-gradle-plugin&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://techblog.lycorp.co.jp/ko/python-multi-project-application-with-poetry&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://techblog.lycorp.co.jp/ko/python-multi-project-application-with-poetry&lt;/a&gt;&lt;/p&gt;</description>
      <category>탐구 생활/Python Monorepo</category>
      <category>monorepo</category>
      <category>msa monorepo</category>
      <category>python monorepo</category>
      <category>python monorepo using uv</category>
      <category>python msa monorepo</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/97</guid>
      <comments>https://probehub.tistory.com/97#entry97comment</comments>
      <pubDate>Sun, 1 Jun 2025 12:01:00 +0900</pubDate>
    </item>
    <item>
      <title>Python gRPC (4) - gRPC gateway 에 대한 탐구</title>
      <link>https://probehub.tistory.com/91</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 하나에 대해서 REST 서버와 gRPC 서버를 따로 운영하면서 재밌지만 다소 버겁다고 생각하고 있었습니다. 그러던중 GraphQL까지 추가되었지요. 내부 비지니스 로직은 통합했더라도 여러 프로토콜을 운영하는건 공부할 내용도 많고 어떤일이 발생할지 불명확하여 두려운 일입니다.&lt;/p&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, gRPC, GraphQL 이 셋을 하나로 혹은 둘로만 줄일 수 있어도 참 좋겠다고 생각하고 찾아보던중 gRPC Gateway 라는 개념을 발견했고 이를 공부해보았습니다. 과연 현실적으로 개발 공수를 줄일 수 있을지, 그리고 운영하면서 추가적인 이슈를 만들어내지는 않을지 알아보았습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;gRPC gateway&lt;/h2&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;gRPC Gateway&lt;/b&gt;는 &lt;b&gt;gRPC 서버를 HTTP/JSON API처럼 사용할 수 있게 중간에서 변환해주는 서버&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;REST API&lt;/b&gt;처럼 HTTP 요청 (GET, POST 등)을 보내면 gRPC-Gateway가 이 요청을 받아서 내부적으로 &lt;b&gt;gRPC 호출&lt;/b&gt;로 변환하고 최종적으로 &lt;b&gt;gRPC 서버&lt;/b&gt;에 요청을 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;347&quot; data-start=&quot;290&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉, gRPC 서버를 REST API처럼 외부에 노출할 수 있게 해주는 '다리(리버스 프록시 서버)' 역할&lt;/b&gt;을 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구성&amp;nbsp;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;architecture_introduction_diagram.svg&quot; data-origin-width=&quot;1120&quot; data-origin-height=&quot;760&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uimTT/btsNoxjjezc/A7fuUrO6FFAvJeJly6pElk/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uimTT/btsNoxjjezc/A7fuUrO6FFAvJeJly6pElk/tfile.svg&quot; data-alt=&quot;https://grpc-ecosystem.github.io/grpc-gateway/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uimTT/btsNoxjjezc/A7fuUrO6FFAvJeJly6pElk/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuimTT%2FbtsNoxjjezc%2FA7fuUrO6FFAvJeJly6pElk%2Ftfile.svg&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;520&quot; height=&quot;353&quot; data-filename=&quot;architecture_introduction_diagram.svg&quot; data-origin-width=&quot;1120&quot; data-origin-height=&quot;760&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://grpc-ecosystem.github.io/grpc-gateway/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 개념에서도 이야기했지만 gRPC gateway는 리버스 프록시 서버를 만들어주는게 핵심입니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;구현&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;준비사항&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적인 gRPC 구성을 위한 지식이 필요합니다.&lt;/li&gt;
&lt;li&gt;gRPC gateway의 핵심인 reverse proxy는 go언어로만 구현이 됩니다. 따라서 해당 머신에 &lt;a href=&quot;https://go.dev/doc/install&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;go를 설치&lt;/a&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;기본적인 gRPC 구성에 대한 지식과 go 튜토리얼 정도를 맞췄다면 github readme 에 있는 &lt;a href=&quot;https://github.com/grpc-ecosystem/grpc-gateway?tab=readme-ov-file#installation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;설치 방법&lt;/a&gt;을 통해 gRPC gateway 구성을 위한 go 패키지를 설치하고, &lt;a href=&quot;https://github.com/grpc-ecosystem/grpc-gateway?tab=readme-ov-file#usage&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;사용 방법&lt;/a&gt;을 통해 직접 테스트해볼 수 있을 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 이러한 작업을 직접하기 귀찮다면 &lt;a href=&quot;https://github.com/akoserwal/gen-grpc-gateway-api&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 모든것을 shell script 로 만들어둔 프로젝트&lt;/a&gt;를 이용할 수 있습니다. 간단하게 한번 해보려는거라면 개인적으로 shell script를 이용하시는걸 추천드립니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;gRPC gateway에 대한 의문점&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;HTTP/2 에 따른 성능상 이점을 포기하는 걸까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://probehub.tistory.com/81&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글&lt;/a&gt;에서도 다뤘지만, gRPC는 HTTP/2에 기반하기 때문에 &lt;b&gt;멀티플렉싱&lt;/b&gt;과 &lt;b&gt;헤더압축&lt;/b&gt;를 활용할 수 있고, 일반적으로 HTTP/1.x 에 기반한 REST 보다 성능상 이득이 있다고 하였습니다. 물론 proto buffer를 사용함으로써 얻는 더 작은 Payload 사이즈에 따른 이득도 있지만 gRPC gateway를 이용하면 순수 gRPC를 썼을 때보다 성능상 불이익이 예상되는 것도 사실입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;JSON &amp;lt;-&amp;gt; Binary 변화에 따른 오버헤드가 존재하진 않을까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC Stub 과 gRPC Server 가 통신할때는 이진화된 데이터를 주고 받기 때문에 이진화된 데이터를 처리하는데 따른 오버헤드를 크게 걱정하지 않았습니다. 하지만 gRPC gateway를 적용하면 조금 달라집니다. 최초 JSON 요청이 넘어오고 그 이진화 한 다음, 다시 이진화된 데이터는 JSON 으로 바꿔야합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도표화 하자면 아래와 같습니다:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;REST.webp&quot; data-origin-width=&quot;2613&quot; data-origin-height=&quot;1612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNrnKo/btsNrkXz92Q/LZpukGbnXLishRAoPyfRJK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNrnKo/btsNrkXz92Q/LZpukGbnXLishRAoPyfRJK/img.webp&quot; data-alt=&quot;REST, 클라이언트 서버 처리과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNrnKo/btsNrkXz92Q/LZpukGbnXLishRAoPyfRJK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNrnKo%2FbtsNrkXz92Q%2FLZpukGbnXLishRAoPyfRJK%2Fimg.webp&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;720&quot; height=&quot;444&quot; data-filename=&quot;REST.webp&quot; data-origin-width=&quot;2613&quot; data-origin-height=&quot;1612&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;REST, 클라이언트 서버 처리과정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;gRPC-gateway.webp&quot; data-origin-width=&quot;2613&quot; data-origin-height=&quot;1612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d7DFld/btsNptm0hyX/05f3160W3RpcHJsq6UTyFk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d7DFld/btsNptm0hyX/05f3160W3RpcHJsq6UTyFk/img.webp&quot; data-alt=&quot;gRPC-gateway, 클라이언트 서버 처리과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d7DFld/btsNptm0hyX/05f3160W3RpcHJsq6UTyFk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd7DFld%2FbtsNptm0hyX%2F05f3160W3RpcHJsq6UTyFk%2Fimg.webp&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;720&quot; height=&quot;444&quot; data-filename=&quot;gRPC-gateway.webp&quot; data-origin-width=&quot;2613&quot; data-origin-height=&quot;1612&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;gRPC-gateway, 클라이언트 서버 처리과정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 과정에서 오버헤드가 존재한다면 역시 순수 gRPC 를 쓰는것보다도 성능상 이점을 찾기 어려울 것이며, 심지어 순수 REST 보다 성능이 떨어지는게 아닐까? 하는 생각이 듭니다. 이상의 두 의문점을 해소하기 위해 벤치마크 테스트를 시행했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;gRPC 러닝커브 * google.api 러닝커브를 줄일만한 방법이 있을까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능과는 다른 관점에서의 의문이 들기도 했습니다. SpringBoot, Django, FastAPI 등 Web Server Framework 들은 웹 표준에 맞춰서 서버를 개발할 수 있는 많은 편의 기능을 제공해주고 있습니다. 덕분에 RESTful API 등을 개발하는 백엔드 개발자 입장에서는 생산성이 많이 올라갔지요, 하지만 지금의 protoc를 통해 코드들이 이러한 편의 기능을 잘 제공해주는지는 의문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AIP 스펙문서를 보면 gRPC를 RESTful하게 제공하기 위해 proto 파일을 작성하는 순간부터 지켜야할 표준들이 엄청 많아지는것이 보입니다. gRPC 자체도 러닝커브가 존재하는데 googe.api.* 의 proto 를 어떻게 사용해야하는지를 익히는 것도 개발자 입장에서는 부담으로 작용합니다.&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;아직은 Web Server Framework 수준으로 편하게 개발할 수 있는 환경이 갖춰지지 않은 것으로 보입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;벤치마크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 얼마나 성능을 희생하는건지 벤치마크 테스트를 통해 확인할 수 있을 것입니다. 순수 gRPC에 비해서 gRPC gateway는 성능을 얼마나 희생하는 걸까요? &lt;a href=&quot;https://probehub.tistory.com/81#%EC%A7%81%EC%A0%91%20%EC%8B%A4%ED%97%98%ED%95%98%EA%B8%B0-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;기존에 벤치마크를 진행했던 히스토리&lt;/a&gt;가 있기 때문에 동일한 조건을 유지하기 위해 &lt;b&gt;gRPC server 자체는 Python으로 구성하고, go를 이용해 gRPC gateway를 만들어서 진행&lt;/b&gt;했습니다. (&lt;a href=&quot;https://github.com/timothy-jeong/grpc_vs_rest&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github 에서 코드 확인 가능&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소에 사용하는 Macbook Air 환경에서 실험하기 때문에 완벽히 통제된 결과를 얻기는 힘듭니다. 따라서 이전의 테스트 결과를 그대로 사용하지 않았고 벤치마크 테스트 스크립트는 다시 시행해서 새로운 결과를 얻어냈습니다. 이러한 이유로 이전 히스토리와는 세부적인 숫자가 다를 수 있습니다.&lt;/p&gt;
&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;table style=&quot;border-collapse: collapse; width: 100%; height: 122px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-sheets-root=&quot;1&quot; data-sheets-baot=&quot;1&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 16.1628%; height: 19px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 15.814%; height: 19px;&quot;&gt;평균 지연시간(ms)&lt;/td&gt;
&lt;td style=&quot;width: 13.7209%; height: 19px;&quot;&gt;Requests/sec&lt;/td&gt;
&lt;td style=&quot;width: 13.3721%; height: 19px;&quot;&gt;가장느린 응답&lt;/td&gt;
&lt;td style=&quot;width: 12.4418%; height: 19px;&quot;&gt;가장 빠른 응답&lt;/td&gt;
&lt;td style=&quot;width: 14.186%; height: 19px;&quot;&gt;지연시간 중앙값&lt;/td&gt;
&lt;td style=&quot;width: 14.186%; height: 19px;&quot;&gt;99% 지연시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 16.1628%; height: 17px;&quot;&gt;gRPC aio&lt;/td&gt;
&lt;td style=&quot;width: 15.814%; height: 17px;&quot;&gt;13.02&lt;/td&gt;
&lt;td style=&quot;width: 13.7209%; height: 17px;&quot;&gt;7600.1&lt;/td&gt;
&lt;td style=&quot;width: 13.3721%; height: 17px;&quot;&gt;21.09&lt;/td&gt;
&lt;td style=&quot;width: 12.4418%; height: 17px;&quot;&gt;0.32&lt;/td&gt;
&lt;td style=&quot;width: 14.186%; height: 17px;&quot;&gt;12.8&lt;/td&gt;
&lt;td style=&quot;width: 14.186%; height: 17px;&quot;&gt;19.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 16.1628%; height: 17px;&quot;&gt;gRPC&lt;/td&gt;
&lt;td style=&quot;width: 15.814%; height: 17px;&quot;&gt;15.16&lt;/td&gt;
&lt;td style=&quot;width: 13.7209%; height: 17px;&quot;&gt;6510.8&lt;/td&gt;
&lt;td style=&quot;width: 13.3721%; height: 17px;&quot;&gt;42.38&lt;/td&gt;
&lt;td style=&quot;width: 12.4418%; height: 17px;&quot;&gt;0.75&lt;/td&gt;
&lt;td style=&quot;width: 14.186%; height: 17px;&quot;&gt;14.6&lt;/td&gt;
&lt;td style=&quot;width: 14.186%; height: 17px;&quot;&gt;23.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 16.1628%; height: 17px;&quot;&gt;REST&lt;/td&gt;
&lt;td style=&quot;width: 15.814%; height: 17px;&quot;&gt;20.8&lt;/td&gt;
&lt;td style=&quot;width: 13.7209%; height: 17px;&quot;&gt;4241.8&lt;/td&gt;
&lt;td style=&quot;width: 13.3721%; height: 17px;&quot;&gt;567.3&lt;/td&gt;
&lt;td style=&quot;width: 12.4418%; height: 17px;&quot;&gt;0.5&lt;/td&gt;
&lt;td style=&quot;width: 14.186%; height: 17px;&quot;&gt;20.4&lt;/td&gt;
&lt;td style=&quot;width: 14.186%; height: 17px;&quot;&gt;39.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 16.1628%; height: 17px;&quot;&gt;gRPC gateway&lt;/td&gt;
&lt;td style=&quot;width: 15.814%; height: 17px;&quot;&gt;24.5&lt;/td&gt;
&lt;td style=&quot;width: 13.7209%; height: 17px;&quot;&gt;3991.1&lt;/td&gt;
&lt;td style=&quot;width: 13.3721%; height: 17px;&quot;&gt;113&lt;/td&gt;
&lt;td style=&quot;width: 12.4418%; height: 17px;&quot;&gt;0.7&lt;/td&gt;
&lt;td style=&quot;width: 14.186%; height: 17px;&quot;&gt;23.5&lt;/td&gt;
&lt;td style=&quot;width: 14.186%; height: 17px;&quot;&gt;42.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 35px;&quot;&gt;
&lt;td style=&quot;width: 16.1628%; height: 35px;&quot;&gt;gRPC gateway aio&lt;/td&gt;
&lt;td style=&quot;width: 15.814%; height: 35px;&quot;&gt;29.3&lt;/td&gt;
&lt;td style=&quot;width: 13.7209%; height: 35px;&quot;&gt;3378.8&lt;/td&gt;
&lt;td style=&quot;width: 13.3721%; height: 35px;&quot;&gt;77.4&lt;/td&gt;
&lt;td style=&quot;width: 12.4418%; height: 35px;&quot;&gt;1.4&lt;/td&gt;
&lt;td style=&quot;width: 14.186%; height: 35px;&quot;&gt;28.3&lt;/td&gt;
&lt;td style=&quot;width: 14.186%; height: 35px;&quot;&gt;49.2&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;예상한 대로, gRPC Gateway를 이용한 HTTP 요청은 worker 2개를 사용한 Starlette 기반 REST 서버에 비해 성능 면에서 다소 부족한 모습을 보였습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히&amp;nbsp;이번&amp;nbsp;테스트는&amp;nbsp;localhost&amp;nbsp;환경에서&amp;nbsp;진행되었기&amp;nbsp;때문에,&amp;nbsp;HTTP/1.1과&amp;nbsp;HTTP/2&amp;nbsp;프로토콜&amp;nbsp;차이보다는,&amp;nbsp;&lt;b&gt;이진&amp;nbsp;데이터(Protobuf)를&amp;nbsp;JSON으로&amp;nbsp;변환하는&amp;nbsp;과정에서&amp;nbsp;발생하는&amp;nbsp;오버헤드&lt;/b&gt;가&amp;nbsp;추가&amp;nbsp;지연시간의&amp;nbsp;주요&amp;nbsp;원인으로&amp;nbsp;작용했을&amp;nbsp;것으로&amp;nbsp;추정됩니다.&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;예상에 부합하는 결과가 나왔다는 것과는 별개로 2가지 흥미로운 점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째는 REST 와 gRPC gateway의 &lt;b&gt;Tail Case&lt;/b&gt;를 비교하면 REST 가 가장 안좋은 모습을 보였으며, 여기서도 gRPC aio 가 가장 안정적인 결과를 보여였다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나는 순수 gRPC 서버에서는 grpc.aio를 이용해 asyncio 기반 비동기 처리를 적극 적용할 경우 가장 뛰어난 성능을 보였지만, &lt;b&gt;gRPC Gateway 환경에서는 오히려 grpc.aio를 사용한 서버가 성능 저하&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;Tail Case 관리 측면에서 Python의 asyncio 를 사용하는 것이 유리한 이유와 gRPC Gateway에서 grpc.aio 가 더 느린 이유를 알아보는 것도 흥미로운 주제가 될 것 같습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;gRPC gateway 활용 사례&lt;/h2&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;etcd&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;유명한 분산 Key-Value 저장소중 하나인 etcd는 &lt;a href=&quot;https://etcd.io/docs/v3.4/dev-guide/api_grpc_gateway/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt;에서 gRPC gateway를 사용중임을 밝히고 있습니다. 특히 etcd 는 K8s환경에서 클러스터 상태 저장 (Pod, Service, ConfigMap, Secret 등)에서 활용중이기 때문에 백엔드 개발자라면 생각지도 못한 순간에 gRPC gateway를 간접적으로 이용중이었던 셈입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;동일한 문서에서 etcd는 gRPC gateway를 도입한 이유에 대해서 밝히고 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;etcd v3는 클러스터 내부 통신을 gRPC 프로토콜로 처리합니다. Go 언어용 gRPC 클라이언트와 etcdctl CLI를 기본 제공하며,&lt;br /&gt;gRPC를 직접 사용할 수 없는 환경을 위해 HTTP/JSON 요청을 gRPC 메시지로 변환하는 RESTful 프록시(gRPC Gateway) 도 함께 제공합니다.&lt;/blockquote&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 gateway를 사용할 수 있는 셈입니다. 이런 경우 gRPC gateway로 인한 성능 저하는 일부 클라이언트들에게 나타나는 문제이지, 정상적으로 etcdctl 을 이용하는 클라이언트들은 별도 성능 저하를 경험하지 않을 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;gRPC Gateway 활용에 대한 중간 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA 환경에서 특정 서비스가 RESTful API와 gRPC 를 모두 제공하고 있기 때문에 이를 통합하기 위한 방법으로 gRPC gateway에 대해서 알아봤습니다. 그리고 지금까지 알아본 결론은 etcd 처럼 특정 사용자의 행위를 정확히 규정하는 솔루션의 경우 서비스 제공 범위를 넓히기 위해 gRPC Gateway는 유용하게 사용될 수 있겠지만 일반 &lt;b&gt;유저가 직접 사용하는 프로덕트 서버의 경우 섣불리 gRPC gateway를 도입하기는 어렵다는 것입니다.&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;아직까지는 일반 유적 직접 사용하는 프로덕트는 HTTP/1.1 웹 기반인 경우가 많으며 이런 경우에는 gRPC Gateway도입이 오히려 성능 저하를 불러일으킬 것이며 또한 대고객 서비스의 업데이트가 잦다고 할때 아직까지는 gRPC gateway를 구성하는 도구가 제공하는 생산성이 Web Server Framework 가 제공하는 생산성보다 떨어지기 때문입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- gRPC gateway: &lt;a href=&quot;https://grpc-ecosystem.github.io/grpc-gateway/&quot;&gt;https://grpc-ecosystem.github.io/grpc-gateway/&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- HTTP and gRPC Transcoding Spec: &lt;a href=&quot;https://google.aip.dev/127&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://google.aip.dev/127&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- etch gRPC gateway: &lt;a href=&quot;https://etcd.io/docs/v3.5/dev-guide/api_grpc_gateway/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://etcd.io/docs/v3.5/dev-guide/api_grpc_gateway/&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>탐구 생활/gRPC&amp;amp;Python</category>
      <category>grpc gateway</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/91</guid>
      <comments>https://probehub.tistory.com/91#entry91comment</comments>
      <pubDate>Sun, 27 Apr 2025 14:53:38 +0900</pubDate>
    </item>
    <item>
      <title>RDB에서 계층형 데이터를 관리하는 방법</title>
      <link>https://probehub.tistory.com/88</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;모놀리식으로 개발했던 시스템들을 MSA로 전환하면서 실수를 보완하고 변경된 요구사항에 맞춰서 많은 것들을 바꾸고 있습니다. 특히 부동산 프로젝트 관리 도메인을 마이크로 서비스로 만들때는 다양한 프로젝트 관련 메타데이터들을 RDB에서 어떻게 관리할지 고민이 되었습니다. 어떤 문제를 인지했는지, 어떻게 해결했는지 공유하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;프로젝트 관리를 위해서는 프로젝트에 적절히 메타데이터를 부여해야했습니다. 이러한 메타데이터는 N 개로 늘어날수도 있으며, 심지어 하나의 메타데이터 카테고리 안에서 위계가 생겨나는 중이었죠.&lt;/p&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;000 이라는 카테고리에 하위 개념이 생기면 테이블 하나 더 만들지 뭐!&quot; 하지만 얼마 지나지않아 그렇게 풀면 안되는 문제라는 것을 깨달았습니다.&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;FrontEnd 개발자와 약속했던 API Interface가 바뀌어야 했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠팡 등 커머스 서비스를 보면 다양한 카테고리가 있는데 그들이 이렇게 비효율적으로 일할것같지 않을것 같다는 생각을 쭉 해오고 해결법을 찾았다가 MSA로 변경하는 김에 같이 바꿨습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;당연하게도 이러한 문제 의식을 가진건 제가 처음이 아니었고, 2010년대 초반을 기점으로 다양한 레퍼런스를 찾을 수 있었습니다. RDB는 데이터 자체를 Flat하게 다루는데 최적화 되어 있기 때문에 위계가 있는 데이터를 어떻게 Flat하게 다룰지가 포인트였습니다. 그리고 선배 개발자분들이 고안한 해결책은 다음과 같습니다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Adjacency List Model&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 행이 자신의 부모 노드 식별자(parent id)를 저장하는 구조입니다. 예를 들어, 아래와 같이 간단한 테이블을 구성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;+-------------+----------------------+--------+
| category_id | name                 | parent |
+-------------+----------------------+--------+
|           1 | ELECTRONICS          |   NULL |
|           2 | TELEVISIONS          |      1 |
|           3 | TUBE                 |      2 |
|           4 | LCD                  |      2 |
|           5 | PLASMA               |      2 |
|           6 | PORTABLE ELECTRONICS |      1 |
|           7 | MP3 PLAYERS          |      6 |
|           8 | FLASH                |      7 |
|           9 | CD PLAYERS           |      6 |
|          10 | 2 WAY RADIOS         |      6 |
+-------------+----------------------+--------+&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;전체 Hierarchy 조회:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 노드가 단일 부모를 가리키므로, 전체 트리를 조회하기 위해 재귀적 탐색(예: WITH RECURSIVE 쿼리)을 사용해야 합니다, 시간복잡도: 최악 O(n)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단일 Hierarchy Path 조회:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;단일 경로는 부모를 재귀적으로 따라가므로 데이터 양이 적으면 문제가 없음, 시간복잡도: O(depth)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;종말 Node(Leaf) 조회:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설명: LEFT JOIN이나 서브쿼리로 자식이 없는 노드를 찾을 수 있으나, 전체 데이터 스캔 필요, &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;시간복잡도: O(n)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Hierarchy Node 추가/삭제:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 INSERT 또는 DELETE 쿼리로 노드 추가/삭제 가능 (부모 ID만 처리하면 됨), 시간복잡도: O(1)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Nested Set Model&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 해법은 테이블로 보면 다소 난해해집니다. 그래서 아래의 그림을 보는게 가장 직관적입니다:&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;nested_categories.webp&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;242&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zI7xE/btsNiPR0jmS/Cm4k3rbxWpFDEW2BpcMii0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zI7xE/btsNiPR0jmS/Cm4k3rbxWpFDEW2BpcMii0/img.webp&quot; data-alt=&quot;https://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zI7xE/btsNiPR0jmS/Cm4k3rbxWpFDEW2BpcMii0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzI7xE%2FbtsNiPR0jmS%2FCm4k3rbxWpFDEW2BpcMii0%2Fimg.webp&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;780&quot; height=&quot;242&quot; data-filename=&quot;nested_categories.webp&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;242&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ELECTRONICS 라는 대분류 안에 TELEVISIONS, PORTABLE ELECTROFICS 가 나뉘고 그 이하에 다른 카테고리들이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이 그림을 X 좌표계에 올리고 좌우 양끝점을 통해 범주를 계산한다고 생각해보겠습니다. 그렇다면 ELECTRONICS의 시작 x 값이 가장 작고 종료 x 값이 제일 커야겠죠? 반대로 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;TELEVISIONS, PORTABLE ELECTROFICS&lt;span&gt; 의 범위 값은 ELECTRONICS 보다 작아야하고 서로 겹치지 않아야합니다. 이렇한 개념을 RDB 테이블에서 표현하면 다음과 같습니다:&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;+-------------+----------------------+-----+-----+
| category_id | name                 | lft | rgt |
+-------------+----------------------+-----+-----+
|           1 | ELECTRONICS          |   1 |  20 |
|           2 | TELEVISIONS          |   2 |   9 |
|           3 | TUBE                 |   3 |   4 |
|           4 | LCD                  |   5 |   6 |
|           5 | PLASMA               |   7 |   8 |
|           6 | PORTABLE ELECTRONICS |  10 |  19 |
|           7 | MP3 PLAYERS          |  11 |  14 |
|           8 | FLASH                |  12 |  13 |
|           9 | CD PLAYERS           |  15 |  16 |
|          10 | 2 WAY RADIOS         |  17 |  18 |
+-------------+----------------------+-----+-----+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 공통 기준에서 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전체 Hierarchy 조회:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 노드에 미리 계산된 lft와 rgt 값을 이용하여, 단일 범위(BETWEEN) 쿼리로 전체 서브트리를 빠르게 조회할 수 있습니다, 시간복잡도: O(1) ~ O(log n)&lt;/p&gt;
&lt;p data-end=&quot;812&quot; data-start=&quot;785&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단일 Hierarchy Path 조회:&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;812&quot; data-start=&quot;785&quot; data-ke-size=&quot;size16&quot;&gt;특정 노드에 대한 전체 경로는 해당 노드의 lft/rgt 값 범위를 활용하여 즉시 조회할 수 있습니다, 시간복잡도: O(1) ~ O(log n)&lt;/p&gt;
&lt;p data-end=&quot;928&quot; data-start=&quot;905&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;종말 Node(Leaf) 조회:&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;928&quot; data-start=&quot;905&quot; data-ke-size=&quot;size16&quot;&gt;설명: Leaf 노드는 일반적으로 rgt = lft + 1 조건을 만족하므로, 이를 통해 빠르게 조회할 수 있습니다, 시간복잡도: O(1)&lt;/p&gt;
&lt;p data-end=&quot;1041&quot; data-start=&quot;1014&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Hierarchy Node 추가/삭제:&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1041&quot; data-start=&quot;1014&quot; data-ke-size=&quot;size16&quot;&gt;노드 삽입이나 삭제 시, 전체 트리에서 영향을 받는 left/right 값들을 재계산하고 업데이트해야 하므로, 작업 비용이 많이 듭니다, 시간복잡도: O(n)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Closure Table&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 RDB에서 계층형 데이터를 다룰때 가장 RDB스럽게 풀어낸 방법이라고 생각합니다. 바로 데이터 테이블과 계층정보를 다루는 테이블을 분리하는 것입니다. 위의 해법들과는 다르게 테이블이 2개 생긴다는(따라서 공간복잡도가 커지는) 특징이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;closure_table_ko.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mJnVd/btsNi5tsP88/KM33v0jKjKbT99lZXK4RKK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mJnVd/btsNi5tsP88/KM33v0jKjKbT99lZXK4RKK/img.webp&quot; data-alt=&quot;https://kntmr.hatenablog.com/entry/2020/08/14/080000&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mJnVd/btsNi5tsP88/KM33v0jKjKbT99lZXK4RKK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmJnVd%2FbtsNi5tsP88%2FKM33v0jKjKbT99lZXK4RKK%2Fimg.webp&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;520&quot; height=&quot;520&quot; data-filename=&quot;closure_table_ko.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://kntmr.hatenablog.com/entry/2020/08/14/080000&lt;/figcaption&gt;
&lt;/figure&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;전체 Hierarchy 조회:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 조상&amp;ndash;자손 관계가 별도의 테이블에 저장되어 있으므로, 단일 JOIN으로 전체 서브트리를 빠르게 조회할 수 있습니다, &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;시간복잡도: O(1) ~ O(log n)&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1337&quot; data-start=&quot;1310&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단일 Hierarchy Path 조회:&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1337&quot; data-start=&quot;1310&quot; data-ke-size=&quot;size16&quot;&gt;Closure Table에 미리 계산된 경로 정보를 통해 특정 노드의 전체 경로를 단순 JOIN이나 WHERE 조건으로 조회할 수 있습니다, &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;시간복잡도: O(1) ~ O(log n)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1474&quot; data-start=&quot;1451&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;종말 Node(Leaf) 조회:&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1474&quot; data-start=&quot;1451&quot; data-ke-size=&quot;size16&quot;&gt;자식이 없는 노드의 경우, Closure Table에서 관련 조상&amp;ndash;자손 관계가 없음을 조건으로 빠르게 필터링할 수 있습니다, &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;시간복잡도: O(1) ~ O(log n)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1606&quot; data-start=&quot;1579&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Hierarchy Node 추가/삭제:&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1606&quot; data-start=&quot;1579&quot; data-ke-size=&quot;size16&quot;&gt;노드 추가 시, 부모의 모든 조상 관계를 새로운 노드와 연결하는 다수의 INSERT가 필요하며, 삭제 시에도 관련된 모든 관계를 제거해야 합니다, 시간복잡도: O(a) 또는 O(r) (여기서 a는 추가할 조상 수, r은 삭제할 관련 관계 수; 일반적으로 트리 깊이에 비례)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;postgresql의 ltree&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ltree&lt;/b&gt;는 PostgreSQL의 확장(extension) 기능 중 하나로, 계층적(트리형) 데이터 구조를 문자열(경로)의 형태로 저장하고, 이 경로를 기준으로 효율적으로 검색할 수 있는 &lt;b&gt;데이터 타입&lt;/b&gt;을 제공합니다. 각 노드의 위치는 루트에서 해당 노드까지의 &amp;ldquo;경로&amp;rdquo;를 나타내는 문자열로 저장됩니다. 예를 들어, 파일 시스템의 경로처럼 'Top.Middle.Bottom'으로 표현할 수 있습니다. ltree 만을 위한 @&amp;gt; 연산자, &amp;lt;@ 연산자와&amp;nbsp;기타 함수(예: nlevel(), subpath()) 들이 제공됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_postgresql_ltree.png&quot; data-origin-width=&quot;730&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vDavs/btsNkmghTHP/CpF9OwlRj19goIeIiASOKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vDavs/btsNkmghTHP/CpF9OwlRj19goIeIiASOKk/img.png&quot; data-alt=&quot;https://dba.stackexchange.com/questions/90797/how-to-update-the-parent-child-of-all-rows-in-the-tree-ltree&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vDavs/btsNkmghTHP/CpF9OwlRj19goIeIiASOKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvDavs%2FbtsNkmghTHP%2FCpF9OwlRj19goIeIiASOKk%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;730&quot; height=&quot;504&quot; data-filename=&quot;edited_postgresql_ltree.png&quot; data-origin-width=&quot;730&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://dba.stackexchange.com/questions/90797/how-to-update-the-parent-child-of-all-rows-in-the-tree-ltree&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전체 Hierarchy 조회:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ltree 전용 인덱스와 &amp;lt;@ 연산자 등으로 경로 기반 조회를 수행할 수 있어, 전체 서브트리를 매우 빠르게 조회할 수 있습니다, 시간복잡도: O(1) ~ O(log n)&lt;/p&gt;
&lt;p data-end=&quot;1965&quot; data-start=&quot;1938&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단일 Hierarchy Path 조회:&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1965&quot; data-start=&quot;1938&quot; data-ke-size=&quot;size16&quot;&gt;경로 문자열을 기반으로 접두사 매칭 연산을 사용하여, 특정 노드까지의 경로를 즉시 추출할 수 있습니다, 시간복잡도: O(1) ~ O(log n)&lt;/p&gt;
&lt;p data-end=&quot;2081&quot; data-start=&quot;2058&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;종말 Node(Leaf) 조회:&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2081&quot; data-start=&quot;2058&quot; data-ke-size=&quot;size16&quot;&gt;ltree는 기본적으로 경로 정보만 저장하므로, Leaf 여부 판별을 위해 경로 길이 등의 추가 계산이 필요할 수 있지만, 적절히 최적화하면 빠르게 조회할 수 있습니다, 시간복잡도: 보통 (약간의 추가 계산 필요)&lt;/p&gt;
&lt;p data-end=&quot;2240&quot; data-start=&quot;2213&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Hierarchy Node 추가/삭제:&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2240&quot; data-start=&quot;2213&quot; data-ke-size=&quot;size16&quot;&gt;노드 추가나 이동 시, 해당 노드의 경로 문자열을 계산하여 업데이트해야 합니다. 인덱스 재구성이 필요하므로 비용이 들 수 있으나, 데이터 규모가 작다면 큰 부담은 없습니다, 시간복잡도: O(1) ~ O(log n) (경로 재계산 비용 포함)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;효율성 종합&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;3324&quot; data-start=&quot;187&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.4651%;&quot;&gt;모델&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.1163%;&quot;&gt;노드 추가&lt;/td&gt;
&lt;td style=&quot;width: 15.6977%;&quot;&gt;노드 삭제&lt;/td&gt;
&lt;td style=&quot;width: 20.814%;&quot;&gt;전체 조회&lt;/td&gt;
&lt;td style=&quot;width: 19.7674%;&quot;&gt;단일 Path 조회&lt;/td&gt;
&lt;td style=&quot;width: 18.0233%;&quot;&gt;종말 Node(Leaf) 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1868&quot; data-start=&quot;1382&quot;&gt;
&lt;td style=&quot;width: 10.4651%;&quot; data-end=&quot;1409&quot; data-start=&quot;1382&quot;&gt;&lt;b&gt;Adjacency List&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.1163%;&quot; data-end=&quot;1513&quot; data-start=&quot;1409&quot;&gt;시간복잡도: O(1)&lt;/td&gt;
&lt;td style=&quot;width: 15.6977%;&quot; data-end=&quot;1617&quot; data-start=&quot;1513&quot;&gt;시간복잡도: O(1)&lt;/td&gt;
&lt;td style=&quot;width: 20.814%;&quot; data-end=&quot;1715&quot; data-start=&quot;1617&quot;&gt;시간복잡도: 최악 O(n)&lt;/td&gt;
&lt;td style=&quot;width: 19.7674%;&quot; data-end=&quot;1795&quot; data-start=&quot;1715&quot;&gt;시간복잡도: O(depth) (depth는 보통 작음)&lt;/td&gt;
&lt;td style=&quot;width: 18.0233%;&quot; data-end=&quot;1868&quot; data-start=&quot;1795&quot;&gt;시간복잡도: O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2337&quot; data-start=&quot;1869&quot;&gt;
&lt;td style=&quot;width: 10.4651%;&quot; data-end=&quot;1896&quot; data-start=&quot;1869&quot;&gt;&lt;b&gt;Nested Set&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.1163%;&quot; data-end=&quot;1996&quot; data-start=&quot;1896&quot;&gt;시간복잡도: O(n)&lt;/td&gt;
&lt;td style=&quot;width: 15.6977%;&quot; data-end=&quot;2092&quot; data-start=&quot;1996&quot;&gt;시간복잡도: O(n)&lt;/td&gt;
&lt;td style=&quot;width: 20.814%;&quot; data-end=&quot;2189&quot; data-start=&quot;2092&quot;&gt;시간복잡도: O(1) ~ O(log n)&lt;/td&gt;
&lt;td style=&quot;width: 19.7674%;&quot; data-end=&quot;2271&quot; data-start=&quot;2189&quot;&gt;시간복잡도: O(1) ~ O(log n)&lt;/td&gt;
&lt;td style=&quot;width: 18.0233%;&quot; data-end=&quot;2337&quot; data-start=&quot;2271&quot;&gt;시간복잡도: O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2829&quot; data-start=&quot;2338&quot;&gt;
&lt;td style=&quot;width: 10.4651%;&quot; data-end=&quot;2365&quot; data-start=&quot;2338&quot;&gt;&lt;b&gt;Closure Table&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.1163%;&quot; data-end=&quot;2470&quot; data-start=&quot;2365&quot;&gt;시간복잡도: O(a), a는 조상 수 (최악 O(n))&lt;/td&gt;
&lt;td style=&quot;width: 15.6977%;&quot; data-end=&quot;2560&quot; data-start=&quot;2470&quot;&gt;시간복잡도: O(r), r은 관련 관계 수 (최악 O(n))&lt;/td&gt;
&lt;td style=&quot;width: 20.814%;&quot; data-end=&quot;2661&quot; data-start=&quot;2560&quot;&gt;시간복잡도: O(1) ~ O(log n)&lt;/td&gt;
&lt;td style=&quot;width: 19.7674%;&quot; data-end=&quot;2749&quot; data-start=&quot;2661&quot;&gt;시간복잡도: O(1) ~ O(log n)&lt;/td&gt;
&lt;td style=&quot;width: 18.0233%;&quot; data-end=&quot;2829&quot; data-start=&quot;2749&quot;&gt;시간복잡도: O(1) ~ O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3324&quot; data-start=&quot;2830&quot;&gt;
&lt;td style=&quot;width: 10.4651%;&quot; data-end=&quot;2857&quot; data-start=&quot;2830&quot;&gt;&lt;b&gt;PostgreSQL ltree&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.1163%;&quot; data-end=&quot;2959&quot; data-start=&quot;2857&quot;&gt;시간복잡도: O(1) ~ O(log n) (데이터량이 작으면 거의 O(1))&lt;/td&gt;
&lt;td style=&quot;width: 15.6977%;&quot; data-end=&quot;3053&quot; data-start=&quot;2959&quot;&gt;시간복잡도: O(1) ~ O(log n)&lt;/td&gt;
&lt;td style=&quot;width: 20.814%;&quot; data-end=&quot;3156&quot; data-start=&quot;3053&quot;&gt;시간복잡도: O(1) ~ O(log n)&lt;/td&gt;
&lt;td style=&quot;width: 19.7674%;&quot; data-end=&quot;3237&quot; data-start=&quot;3156&quot;&gt;시간복잡도: O(1) ~ O(log n)&lt;/td&gt;
&lt;td style=&quot;width: 18.0233%;&quot; data-end=&quot;3324&quot; data-start=&quot;3237&quot;&gt;시간복잡도: O(1) ~ O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;b&gt;&lt;/b&gt;&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;데이터의 전체 건수(N)가 많을수록, 그리고 위계 깊이가 깊을수록 각각의 알고리즘이 가진 시간복잡도와 공간 복잡도가 중요한 고려 대상이 됩니다. 또한 &lt;b&gt;읽기(READ) 연산이 압도적으로 많은 경우,&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Insert/Update 비용보다는 조회 성능이 우선되기 때문에 전체 Hierarchy 조회 및 특정 경로 조회의 효율성이 결정적인 요소가 됩니다. 이러한 기준에서 주관적으로 평가를 해보자면:&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-end=&quot;346&quot; data-start=&quot;321&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Adjacency List Model&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;346&quot; data-start=&quot;321&quot; data-ke-size=&quot;size16&quot;&gt;구조가 간단하여 구현이 쉽고, 단일 노드의 추가/삭제는 매우 빠르지만, 읽기 성능이 좋지 않기 때문에 소규모 프로젝트에 적합하다고 판단됩니다.&lt;/p&gt;
&lt;h3 data-end=&quot;346&quot; data-start=&quot;321&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Nested Set Model&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;346&quot; data-start=&quot;321&quot; data-ke-size=&quot;size16&quot;&gt;lft/rgt 값을 이용한 범위 쿼리로 전체 서브트리 조회 및 특정 경로 조회가 매우 빠르며, 인덱스가 잘 구성되면 조회 성능이 뛰어납니다. 다만&amp;nbsp; 노드 추가/삭제시 전체 트리의 lft/rgt 를 계산해야하므로 업데이트 비용이 매우 높아집니다.&lt;/p&gt;
&lt;p data-end=&quot;346&quot; data-start=&quot;321&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;346&quot; data-start=&quot;321&quot; data-ke-size=&quot;size16&quot;&gt;다양한 위계를 가진 다수의 데이터를 다루며, 읽기 연산이 압도적으로 많은 경우에 적합합니다.&lt;/p&gt;
&lt;h3 data-end=&quot;346&quot; data-start=&quot;321&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Closure Table&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;346&quot; data-start=&quot;321&quot; data-ke-size=&quot;size16&quot;&gt;각 노드의 모든 조상&amp;ndash;자손 관계를 별도 테이블에 미리 저장하기 때문에, 전체 조회나 특정 경로 조회가 단순 JOIN이나 WHERE 조건으로 빠르게 수행됩니다. 노드 추가/삭제시 처리해야 할 작업은 있지만, 구현 복잡도 측면에서는 비교적 간단합니다. 다만 대량의 데이터에 대해서 공간복잡도가 크게 증가되는 문제가 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;346&quot; data-start=&quot;321&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;346&quot; data-start=&quot;321&quot; data-ke-size=&quot;size16&quot;&gt;하지만 일반적으로 계층형 데이터가 대량이기는 어렵기에 공간복잡도는 큰 문제가 되지 않을 것으로 보이며, 오히려 가장 RDB다운 해법이라고 판단됩니다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;PostgreSQL ltree&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능측면에서 나무랄데가 없습니다. 다만 특정 RDBMS에 종속성이 생긴다는 점, 그리고 특정 러닝커브가 생긴다는 점에서 부정적입니다.&lt;/p&gt;
&lt;h3 data-end=&quot;1805&quot; data-start=&quot;1796&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;최종 결론&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1820&quot; data-start=&quot;1807&quot; data-ke-size=&quot;size16&quot;&gt;저의 주관을 종합해보면 읽기 연산이 압도적으로 많은 환경에서는:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2095&quot; data-start=&quot;1854&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1913&quot; data-start=&quot;1854&quot;&gt;&lt;b&gt;Nested Set Model&lt;/b&gt;과 &lt;b&gt;Closure Table&lt;/b&gt;이 조회 성능 면에서 우수합니다.&lt;/li&gt;
&lt;li data-end=&quot;1993&quot; data-start=&quot;1916&quot;&gt;&lt;b&gt;Nested Set Model&lt;/b&gt;은 조회 쿼리가 매우 빠르지만, 노드 추가/삭제 시 전체 트리 재계산 등 업데이트 비용이 높습니다.&lt;/li&gt;
&lt;li data-end=&quot;2092&quot; data-start=&quot;1996&quot;&gt;&lt;b&gt;Closure Table&lt;/b&gt;은 조회 성능도 뛰어나면서, 노드 추가/삭제 작업에 있어서도 구현이 비교적 간단하며, RDB를 이용한 해결책이라는 점이 더 잘 와닿습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2291&quot; data-start=&quot;1822&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2193&quot; data-start=&quot;2096&quot;&gt;&lt;b&gt;PostgreSQL ltree&lt;/b&gt;는 기능적으로 매우 매력적이지만, 특정 RDBMS(즉, PostgreSQL)에 종속성이 생긴다는 점에서 범용성이 떨어진다고 생각합니다.&lt;/li&gt;
&lt;li data-end=&quot;2291&quot; data-start=&quot;2195&quot;&gt;&lt;b&gt;Adjacency List Model&lt;/b&gt;은 구현이 간단하고 단순한 수정 작업에는 효과적이지만, 조회 연산이 많은 경우에는 재귀 탐색 비용 때문에 효율성이 떨어집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2477&quot; data-start=&quot;2293&quot; data-ke-size=&quot;size16&quot;&gt;따라서, &lt;b&gt;전체 Record 수(N)가 상대적으로 적고, 위계 깊이(Depth)가 2~5 정도인 환경에서, READ 연산이 주를 이루는 상황이라면, 공간 복잡도만 감수할 수 있다면 Closure Table 방식이 Nested Set 방식보다 구현 복잡도 면에서 우수하며, RDB 기반 해법으로서도 더 직관적이라고 판단됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Response Interface는?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 개발자로서 또 하나의 고민은 DB는 저렇게 해결한다고 하는데 &quot;프론트에게 내려주는 데이터 구조는 어떻게 가야하는걸까?&quot; 입니다. 이러한 고민은 백엔드 뿐만 아니라 프론트도 많이 해왔고, React 진영에는 관련 라이브러리가 있는것을 확인했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 다음과 같이 데이터를 내려주는 것으로 합의가 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744527984504&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;id&quot;: 1,
    &quot;name&quot;: &quot;item1&quot;,
    &quot;parentId&quot;: null,
    &quot;depth&quot;: 0
},
{
    &quot;id&quot;: 2,
    &quot;name&quot;: &quot;item1.1&quot;,
    &quot;parentId&quot;: 1,
    &quot;depth&quot;: 1
},
{
    &quot;id&quot;: 3,
    &quot;name&quot;: &quot;item1.2&quot;,
    &quot;parentId&quot;: 1,
    &quot;depth&quot;: 1
},
{
    &quot;id&quot;: 4,
    &quot;name&quot;: &quot;item2&quot;,
    &quot;parentId&quot;: null,
    &quot;depth&quot;: 0
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;&lt;b&gt;managing-hierarchical-data-in-mysql:&amp;nbsp; &lt;/b&gt;&lt;a href=&quot;https://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;closure table:&lt;/b&gt; &lt;a href=&quot;https://fueled.com/the-cache/posts/backend/closure-table/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://fueled.com/the-cache/posts/backend/closure-table/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;closure talbe&lt;/b&gt;: &lt;a href=&quot;https://kntmr.hatenablog.com/entry/2020/08/14/080000&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kntmr.hatenablog.com/entry/2020/08/14/080000&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;postgresql ltree:&lt;/b&gt; &lt;a href=&quot;https://www.postgresql.org/docs/16/ltree.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.postgresql.org/docs/16/ltree.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;postgresql ltree: &lt;a href=&quot;https://patshaughnessy.net/2017/12/13/saving-a-tree-in-postgres-using-ltree&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://patshaughnessy.net/2017/12/13/saving-a-tree-in-postgres-using-ltree&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>탐구 생활/데이터베이스</category>
      <category>db 계층형</category>
      <category>rdb hierarchy</category>
      <category>rdb 계층형</category>
      <category>계층형 데이터</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/88</guid>
      <comments>https://probehub.tistory.com/88#entry88comment</comments>
      <pubDate>Sun, 13 Apr 2025 16:07:14 +0900</pubDate>
    </item>
    <item>
      <title>Python gRPC (3) - gRPC가 빠른 이유와 벤치마크</title>
      <link>https://probehub.tistory.com/81</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;thumb.webp&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baXUvJ/btsNjjx2Qnm/R8g5K7Cty5USknKdx1szx0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baXUvJ/btsNjjx2Qnm/R8g5K7Cty5USknKdx1szx0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baXUvJ/btsNjjx2Qnm/R8g5K7Cty5USknKdx1szx0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaXUvJ%2FbtsNjjx2Qnm%2FR8g5K7Cty5USknKdx1szx0%2Fimg.webp&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;360&quot; height=&quot;360&quot; data-filename=&quot;thumb.webp&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC를 왜 써야했는지, 그리고 어떻게 구현해야하는지 알아봤습니다. 이제는 그래서 도대체 왜 gRPC가 빠른건지 이론적인 배경을 정리하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;gRPC 성능의 핵심 이론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC가 빠른 가장 큰 이유는 &lt;b&gt;통신 계층의 개선(HTTP/2)&lt;/b&gt;과 &lt;b&gt;데이터 표현 방식의 개선(Protocol Buffers)&lt;/b&gt;에 있습니다. HTTP/2가 제공하는 멀티플렉싱, 헤더 압축 등의 기능과, 프로토콜 버퍼를 통한 바이너리 데이터 직렬화가 결합되어 지연을 줄이고 처리량을 높입니다. 주요 이론적 요소를 정리하면 다음과 같습니다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;HTTP/2 멀티플렉싱&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;오늘날에 일반적인 웹 통신은 HTTP/1.1 버전을 따릅니다. 하지만 gRPC는 HTTP/2를 따르도록 되어 있으며, HTTP/2는 하나의 TCP 연결에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;동시에 여러 요청과 응답을 주고받을 수 있는 멀티플렉싱&lt;/b&gt;을 지원합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;http-2-multiplexing.webp&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;473&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfBUnM/btsNjC5bh2R/Ajn1Fck99WD361ZcwdZ3pK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfBUnM/btsNjC5bh2R/Ajn1Fck99WD361ZcwdZ3pK/img.webp&quot; data-alt=&quot;https://blog.cloudflare.com/http-2-for-web-developers&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfBUnM/btsNjC5bh2R/Ajn1Fck99WD361ZcwdZ3pK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfBUnM%2FbtsNjC5bh2R%2FAjn1Fck99WD361ZcwdZ3pK%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;620&quot; height=&quot;367&quot; data-filename=&quot;http-2-multiplexing.webp&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;473&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://blog.cloudflare.com/http-2-for-web-developers&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 따라 이전 HTTP/1.1처럼 요청-응답을 &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;p data-ke-size=&quot;size16&quot;&gt;물론 HTTP/1.1 에서도 요청-응답을 비동기적으로 처리할 수 있도록, 즉 한번에 여러 요청을 처리할 수 있도록 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Connection_management_in_HTTP_1.x#persistent_connections&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Keep-Alive,&lt;/a&gt;&amp;nbsp;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Connection_management_in_HTTP_1.x#http_pipelining&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;pipelining&lt;/a&gt; 을 지원했지만 근본적은 해결책은 아니었기 때문에 &lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Head_of_line_blocking&quot;&gt;HOL 블로킹(Head of line blocking)&lt;/a&gt;문제가 발생할 수 있으며, 다수의 proxy 서버들은 이 spec에 대응하지 못했습니다. 그에비해 HTTP/2 의 멀티플렉싱은 이러한 문제를 더 본질적으로 해결한 접근입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;HTTP/2 헤더 압축(HPACK)&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;connection-use-streams.webp&quot; data-origin-width=&quot;819&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L8yar/btsNi42bXZD/B7rciczBePLPQ0wVUHqIt0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L8yar/btsNi42bXZD/B7rciczBePLPQ0wVUHqIt0/img.webp&quot; data-alt=&quot;https://cheapsslsecurity.com/p/http-2-header-compression/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L8yar/btsNi42bXZD/B7rciczBePLPQ0wVUHqIt0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL8yar%2FbtsNi42bXZD%2FB7rciczBePLPQ0wVUHqIt0%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;620&quot; height=&quot;308&quot; data-filename=&quot;connection-use-streams.webp&quot; data-origin-width=&quot;819&quot; data-origin-height=&quot;407&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://cheapsslsecurity.com/p/http-2-header-compression/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/1.1에서는 매 요청마다 쿠키 등 &lt;b&gt;반복적인 헤더&lt;/b&gt;들이 전송되어 &lt;b&gt;불필요한 오버헤드&lt;/b&gt;가 많았습니다.&amp;nbsp;&lt;a style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot; href=&quot;https://blog.cloudflare.com/hpack-the-silent-killer-feature-of-http-2/#hpack&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;HTTP/2에서는 &lt;b&gt;헤더를 압축&lt;/b&gt;하고 &lt;b&gt;중복을 제거&lt;/b&gt;&lt;/a&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;하여 전송하기 때문에 대역폭 사용이 효율적이고 응답 속도가 개선됩니다​. 예를 들어 gRPC는 HTTP/2 기반이므로 &lt;/span&gt;&lt;b&gt;content-type, 권한 토큰 등의 헤더를 한 번만 보내거나 압축하여&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 반복 전송하지 않으므로, REST API 대비 &lt;/span&gt;&lt;b&gt;패킷 크기가 줄고&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 처리 효율이 높아집니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Protocol Buffers 바이너리 직렬화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지의 특성은 사실 HTTP/2를 이용하기 때문에 얻을 수 있는 이득이지 gRPC를 사용하기 때문에 얻는 이득이라고 보기 어렵습니다. 위의 성능상 이점은 클라이언트와 서버가 HTTP/2 위에서 REST로 통신을 해도 똑같으니까요. 하지만 gRPC는 proto buffer를 이용하여 바이너리 직렬화/역직렬화라는 추가적인 성능상 이득을 줍니다.&lt;/p&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;b&gt;JSON 대신 Protocol Buffers&lt;/b&gt;(이하 &lt;b&gt;ProtoBuf&lt;/b&gt;)를 사용합니다. ProtoBuf는 &lt;b&gt;이진(binary)&lt;/b&gt; 포맷으로 데이터를 직렬화하기 때문에 JSON처럼 사람이 읽을 수 있는 텍스트 표현에 비해 &lt;b&gt;데이터 크기가 작고 파싱 속도가 빠릅니다&lt;/b&gt;​. ProtoBuf에서는 각 필드를 번호(Tag)로 식별하고 타입을 미리 정의하여 변환하므로, 키 이름과 구조를 일일이 문자열로 보내는 JSON에 비해 &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;a href=&quot;https://martin.kleppmann.com/2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Martin Kleppmann는 자신의 블로그&lt;/a&gt;에서 이 부분을 정확히 설명했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1744451121140&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# json 으로 이 데이터를 표현하면 82byte 지만
{
    &quot;userName&quot;: &quot;Martin&quot;,
    &quot;favouriteNumber&quot;: 1337,
    &quot;interests&quot;: [&quot;daydreaming&quot;, &quot;hacking&quot;]
}

# proto 파일을 에서 메시지 포맷을 이렇게 정의하면 33byte 가 됩니다.
message Person {
    required string user_name        = 1;
    optional int64  favourite_number = 2;
    repeated string interests        = 3;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;python code로 보자면, message/person.proto 파일을 기반으로 만들어진 pb2.py 파일에서 이진 포맷 데이터를 확인할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1744451957085&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# pb/message/person.pb2.py
from google.protobuf import descriptor_pool as _descriptor_pool
... 
# DESCRIPTOR 에 할당된 이진 데이터를 확인할 수 있습니다.
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14message/person.proto\x12\x06member\&quot;H\n\x06Person\x12\x11\n\tuser_name\x18\x01 \x01(\t\x12\x18\n\x10\x66\x61vourite_number\x18\x02 \x01(\x03\x12\x11\n\tinterests\x18\x03 \x03(\tb\x06proto3')

...&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-end=&quot;5123&quot; data-start=&quot;5095&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실험 결과: REST vs gRPC 성능 비교&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;공식 벤치마크&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://grpc.io/docs/guides/benchmarking/#overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;gRPC 공식문서에서는 Benchmark&lt;/a&gt; 에 대한 정보를 제공하고 있습니다. 심지어 Grafana로 만든 &lt;a href=&quot;https://grafana-dot-grpc-testing.appspot.com/?orgId=1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;대시보드&lt;/a&gt;도 제공하고 있어서 정보를 얻기 정말 편합니다. 다만, REST 와 gRPC의 비교가 아니라 각 언어간 gRPC의 Latency 와 throughput 을 비교하는 자료여서 지금 알아보고자 하는 내용과는 달라서 깊이 알아보진 않겠습니다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-end=&quot;5390&quot; data-start=&quot;5125&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Payload 에 따른 비교&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;5390&quot; data-start=&quot;5125&quot; data-ke-size=&quot;size16&quot;&gt;gRPC 특성상 다양한 클라이언트(언어)가 고려되어야 할것입니다. 다양한 벤치마크들 중 &lt;a href=&quot;https://kth.diva-portal.org/smash/record.jsf?pid=diva2%3A1792957&amp;amp;dswid=6703&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;KTH의 학사학위 프로젝트&lt;/a&gt;에서 적절한 레퍼런스를 찾은듯 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;datasize_table4.2.webp&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bI1ywn/btsNmBFvysv/GQw8au1tlSoIuL1K0KHzm1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bI1ywn/btsNmBFvysv/GQw8au1tlSoIuL1K0KHzm1/img.webp&quot; data-alt=&quot;table 4.2, data size&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI1ywn/btsNmBFvysv/GQw8au1tlSoIuL1K0KHzm1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbI1ywn%2FbtsNmBFvysv%2FGQw8au1tlSoIuL1K0KHzm1%2Fimg.webp&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;420&quot; height=&quot;155&quot; data-filename=&quot;datasize_table4.2.webp&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;table 4.2, data size&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;5390&quot; data-start=&quot;5125&quot; data-ke-size=&quot;size16&quot;&gt;Java, Python, Rust 언어에 대해서 요청횟수는 고정하고, payload 크기를 XS, S, M, L 으로 바꿔가며 실험한 결과를 발견했습니다. 초록부터 Appendix 이전가지 44 페이지 정도 되는 짧은 보고서이므로 다 읽어보는게 좋겠지만, 실험결과만 요역하자면 다음과 같습니다:&lt;/p&gt;
&lt;p data-end=&quot;5390&quot; data-start=&quot;5125&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnREKK/btsNjD37Ffe/8aaB25OLa7TKYjJC21Ne7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnREKK/btsNjD37Ffe/8aaB25OLa7TKYjJC21Ne7k/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1416&quot; data-origin-height=&quot;734&quot; data-filename=&quot;java.png&quot; width=&quot;520&quot; height=&quot;270&quot; style=&quot;width: 33.2782%; margin-right: 10px;&quot; data-widthpercent=&quot;34.07&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnREKK/btsNjD37Ffe/8aaB25OLa7TKYjJC21Ne7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnREKK%2FbtsNjD37Ffe%2F8aaB25OLa7TKYjJC21Ne7k%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;1416&quot; height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bV8FVJ/btsNi7dCUyt/O42iiqE9KbolZyLSoDt8W1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bV8FVJ/btsNi7dCUyt/O42iiqE9KbolZyLSoDt8W1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;772&quot; data-filename=&quot;python.png&quot; width=&quot;520&quot; height=&quot;282&quot; style=&quot;width: 31.8636%; margin-right: 10px;&quot; data-widthpercent=&quot;32.62&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bV8FVJ/btsNi7dCUyt/O42iiqE9KbolZyLSoDt8W1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbV8FVJ%2FbtsNi7dCUyt%2FO42iiqE9KbolZyLSoDt8W1%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;1426&quot; height=&quot;772&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ArPNR/btsNkkppZga/1kYcukxXWDihS9oHda1Ln0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ArPNR/btsNkkppZga/1kYcukxXWDihS9oHda1Ln0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1422&quot; data-origin-height=&quot;754&quot; data-filename=&quot;rust.png&quot; width=&quot;520&quot; height=&quot;276&quot; style=&quot;width: 32.5327%;&quot; data-widthpercent=&quot;33.31&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ArPNR/btsNkkppZga/1kYcukxXWDihS9oHda1Ln0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FArPNR%2FbtsNkkppZga%2F1kYcukxXWDihS9oHda1Ln0%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;1422&quot; height=&quot;754&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;java, Python, Rust 결과 그래프&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실험 결과에 따르면, &lt;b&gt;Payload 크기가 클수록 gRPC의 성능이 REST 대비 일관되게 우수&lt;/b&gt;한 것으로 나타났습니다. Java, Python, Rust 세 언어 모두에서 동일한 경향이 확인되었으며, 특히 &lt;b&gt;중간(M) 이상 크기의 메시지에서 gRPC가 확실한 우위를 보였습니다.&lt;/b&gt;&amp;nbsp;예를 들어 &lt;b&gt;Java 환경에서는 protobuf로 인코딩된 메시지는 JSON 대비 56% 더 작은 크기를 가졌고, gRPC 서버는 REST보다 52% 더 높은 throughput 을 달성&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;프로그래밍 언어에 따라 gRPC의 효율성은 다소 차이가 있었습니다.&lt;/b&gt; 실험에서는 Payload가 아주 커지는 경우(예: L)에는 Python의 gRPC 성능 향상폭이 다른 두 언어의 성능 향상 폭보다 굉장히 컸습니다. 반면 Payload가 작은 경우 RUST는 명확하게 REST 를 이용하는게 gRPC 보다 340% 더 높은 throughput을 보여주었습니다.&lt;/p&gt;
&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;항상 gRPC가 REST보다 나은것은 아니다. 전달하는 데이터 payload가 작다면 REST를 고려할수도 있다.&lt;/li&gt;
&lt;li&gt;언어에따라 REST 대비 gRPC의 성능이 차이가 있다. 만약 성능상 차이가 크지 않다면 운영이 더 복잡해지는 gRPC를 굳이 도입하지 않을 수도 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-end=&quot;5390&quot; data-start=&quot;5125&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;직접 실험하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;5390&quot; data-start=&quot;5125&quot; data-ke-size=&quot;size16&quot;&gt;위의 실험도 충분히 재미있고 유용한 결과를 주지만 요청횟수에 따른 비교가 아쉽습니다. 더 오피셜하고 명확한 레퍼런스로는 &quot;&lt;a href=&quot;https://ieeexplore.ieee.org/document/9555425&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021년에 작성된 관련 보고서&quot;&lt;/a&gt; 가 있지만, 저는 접근할 수 없어서 직접 테스트하게 되었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5390&quot; data-start=&quot;5125&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5390&quot; data-start=&quot;5125&quot; data-ke-size=&quot;size16&quot;&gt;원래는 더 현실적인 테스트를 위해 EC2 에 gRPC, REST 서버를 올리고 부하 테스트를 진행했지만,&amp;nbsp; 제가 이용하는 네트워크망이 일반 가정용이어서 너무 느린 문제로 결국 localhost 테스트로 대체되었습니다.&lt;/p&gt;
&lt;p data-end=&quot;5390&quot; data-start=&quot;5125&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5390&quot; data-start=&quot;5125&quot; data-ke-size=&quot;size16&quot;&gt;이하의 내용은 &lt;a href=&quot;https://github.com/timothy-jeong/grpc_vs_rest&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github&lt;/a&gt;에서 코드를 내려받으셔서 직접 재현할 수 있습니다.&lt;/p&gt;
&lt;h4 data-end=&quot;5390&quot; data-start=&quot;5125&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;테스트 환경&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;서버 환경&lt;/b&gt;: MacBook Air 15, M3, 2024, RAM 24GB&lt;/li&gt;
&lt;li&gt;&lt;b&gt;REST 서버&lt;/b&gt;: Starlette + Uvicorn (&lt;b&gt;--workers=2&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;gRPC 서버&lt;/b&gt;: Python grpc.server(ThreadPoolExecutor(max_workers=10)), grpc.aio.server(...)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클라이언트&lt;/b&gt;: go ghz, hey&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Response Payload 크기&lt;/b&gt;: 14KB, 256KB (REST는 JSON, gRPC는 Protobuf)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부하 조건&lt;/b&gt;: 클라이언트: 50, 100개, 요청: 2000, 5000, 10,000&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;5390&quot; data-start=&quot;5125&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;테스트 결과&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부하조건을 다르게 줬지만, 결과적으로 저정도 부&lt;b&gt;하 조건에는 각 프로토콜별로 유의미한 차이를 찾을 수 없습니다&lt;/b&gt;. raw 데이터는 구글 스프레드시트에 열어두었습니다.(&lt;a title=&quot;raw data&quot; href=&quot;https://docs.google.com/spreadsheets/d/1OiJd_DSkLQIPNSbAS_0XWDivMwIGMkxsEk2oIVuyDuU/edit?usp=sharing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;raw data&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Payload 크기를 14KB, 256KB 로 고정했을때 클라이언트 100개가가 총 10,000개의 요청을 전달하는 케이스만 요약하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;14KB 조건:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 &quot;Payload에 다른 비교&quot;와 같은 결과가 나왔습니다. REST 가 전반적으로 gRPC, gRPC_aio 보다 적은 Latency 를 보여주 고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_14kb_c100_n10000_box_plot.webp&quot; data-origin-width=&quot;1979&quot; data-origin-height=&quot;1180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/thGuA/btsNosoZZ9k/3rzZd2K8WY04K0Tim5pTv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/thGuA/btsNosoZZ9k/3rzZd2K8WY04K0Tim5pTv0/img.png&quot; data-alt=&quot;14KB 조건에서 Latency Distribution&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/thGuA/btsNosoZZ9k/3rzZd2K8WY04K0Tim5pTv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FthGuA%2FbtsNosoZZ9k%2F3rzZd2K8WY04K0Tim5pTv0%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;520&quot; height=&quot;310&quot; data-filename=&quot;edited_14kb_c100_n10000_box_plot.webp&quot; data-origin-width=&quot;1979&quot; data-origin-height=&quot;1180&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;14KB 조건에서 Latency Distribution&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 median latency 로 비교를 해보자면 REST(3.4ms), gRPC(5.66ms), gRPC_aio(4.09ms) 로,&lt;b&gt; REST 가 gRPC 보다 40%, gRPC_aio 보다는 16% 더 나은 성능&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;다만 99% percentile 에서의 안정성은 gRPC 가 REST 보다 뛰어났으며, 특히 aio 를 이용한 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;256KB 조건:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Payload 크기가 커지자 gRPC의 성능이 REST보다 나아지기 시작했습니다. 역시 aio를 이용한 gRPC 가 gRPC 보다 더 안정적이고 더 나은 성능을 보여주는 것이 확인되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;256kb_c100_n10000_box_plot.webp&quot; data-origin-width=&quot;1979&quot; data-origin-height=&quot;1180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcA9C7/btsNpWbO2eR/qaDXqq9kK7PQtTKZ4ruIKk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcA9C7/btsNpWbO2eR/qaDXqq9kK7PQtTKZ4ruIKk/img.webp&quot; data-alt=&quot;256KB 조건에서 Latency Distribution&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcA9C7/btsNpWbO2eR/qaDXqq9kK7PQtTKZ4ruIKk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcA9C7%2FbtsNpWbO2eR%2FqaDXqq9kK7PQtTKZ4ruIKk%2Fimg.webp&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;520&quot; height=&quot;310&quot; data-filename=&quot;256kb_c100_n10000_box_plot.webp&quot; data-origin-width=&quot;1979&quot; data-origin-height=&quot;1180&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;256KB 조건에서 Latency Distribution&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 median latency 로 비교를 해보자면 REST(22ms), gRPC(14.19ms), gRPC_aio(12.33ms) 로, &lt;b&gt;REST 가 gRPC 보다 55%, gRPC_aio 보다는 78% 더 안좋은 성능&lt;/b&gt;을 보여주고 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;gRPC vs gRPC_aio&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 벤치마크 테스트를 해보고는 스웨덴 학부생들이 했던 실험결과를 재확인하였습니다. 그런데 거기서 그치지 않고 추가로 흥미로운 결과과를 하나 확인했습니다. &lt;b&gt;aio 를 이용한 gRPC가 일반 gRPC보다 단순 I/O 작업에서 더 좋은 성능(낮은 Latency)를 보여주고 있다는 것&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;gRPC는 Python Thread에 기반하여 동시성을 구현하고 있는것으로 추측됩니다. 그렇다면 &lt;a title=&quot;Python Asyncio&quot; href=&quot;https://probehub.tistory.com/82&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Python Asyncio&lt;/a&gt;를 추가로 이용하는 grpc.aio 가 단순 I/O 작업에서는 더 나은 성능을 보여주는것은 당연할 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 일반적으로 I/O 가 중요한 작업이라면 gpc 보다는 grpc.aio 를 이용하는게 더 나은 선택일 것입니다. 다만 아직은 Python GIL을 이용하는게 기본 사양이므로 gRPC를 통해 일어나는 작업이 CPU Bound 하다면 무작정 grpc.aio를 이용하는건 지양해야할것입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;종합 결론&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 Python에 대한 이야기입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;localhost 환경에서 REST의 병렬처리 정도를 (warker)를 높인다면 요청 처리량에 있어서는 gRPC 와 유의미한 차이가 없음&lt;/li&gt;
&lt;li&gt;Payload가 일정수준 커질경우 gRPC가 REST 보다 더 나은 성능을 보여줌&lt;/li&gt;
&lt;li&gt;Payload 크기와 관계없이 항상 gRPC가 REST보다 더 안정적인 성능(저 낮은 Latency 편차)을 보여줌&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;실험 한계&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트는 모두 localhost에서 진행되어 &lt;b&gt;HTTP/2의 장점(멀티플렉싱, RTT 감소 등)은 반영되지 않음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;CPU 사용률, 메모리 프로파일링은 포함하지 않음 (추가 실험 대상)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://medium.com/naver-cloud-platform/nbp-%EA%B8%B0%EC%88%A0-%EA%B2%BD%ED%97%98-%EC%8B%9C%EB%8C%80%EC%9D%98-%ED%9D%90%EB%A6%84-grpc-%EA%B9%8A%EA%B2%8C-%ED%8C%8C%EA%B3%A0%EB%93%A4%EA%B8%B0-1-39e97cb3460&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;gRPC 깊게 파고들기 #1, 네이버클라우드&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://medium.com/naver-cloud-platform/nbp-%EA%B8%B0%EC%88%A0-%EA%B2%BD%ED%97%98-%EC%8B%9C%EB%8C%80%EC%9D%98-%ED%9D%90%EB%A6%84-grpc-%EA%B9%8A%EA%B2%8C-%ED%8C%8C%EA%B3%A0%EB%93%A4%EA%B8%B0-2-b01d390a7190&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;gRPC 깊게 파고들기 #2, 네이버클라우드&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://grpc.io/docs/what-is-grpc/introduction/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;gRPC Introduction, google&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://stackoverflow.com/questions/36517829/what-does-multiplexing-mean-in-http-2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;What does multiplexing mean in HTTP/2, stackoverflow&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Connection_management_in_HTTP_1.x&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;HTTP/1.x connection management, MDN&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://blog.cloudflare.com/http-2-for-web-developers/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;http2 for web developers, cloudflare&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://blog.cloudflare.com/hpack-the-silent-killer-feature-of-http-2/#hpack&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;HPACK, cloudflare&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://grpc.io/docs/guides/benchmarking/#overview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;gRPC Benchmark&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://medium.com/@EmperorRXF/evaluating-performance-of-rest-vs-grpc-1b8bdf0b22da&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;gRPC vs REST, .NET Benchmakr&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://cla9.tistory.com/177&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;gRPC vs REST, SpringBoot&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://kth.diva-portal.org/smash/record.jsf?pid=diva2%3A1792957&amp;amp;dswid=6703&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;gRPC vs REST, A&amp;nbsp;Comparative&amp;nbsp;Study&amp;nbsp;of&amp;nbsp;REST&amp;nbsp;and&amp;nbsp;gRPC&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://zerohertz.github.io/grpc-init/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;gRPC vs REST, Using Locust&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>탐구 생활/gRPC&amp;amp;Python</category>
      <category>grpc 벤치마크</category>
      <category>grpc 왜 빠른가</category>
      <category>grpc 이론적 배경</category>
      <category>rest vs grpc</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/81</guid>
      <comments>https://probehub.tistory.com/81#entry81comment</comments>
      <pubDate>Sun, 13 Apr 2025 14:28:53 +0900</pubDate>
    </item>
    <item>
      <title>Strawberry 선택 이유와 타 라이브러리와의 비교 분석</title>
      <link>https://probehub.tistory.com/85</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;68747470733a2f2f6769746875622e636f6d2f737472617762657272792d6772617068716c2f737472617762657272792f7261772f6d61696e2f2e6769746875622f6c6f676f2e706e67.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SSrgb/btsM1gPoh3z/TsH8zOCmDGwGAUnskunK8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SSrgb/btsM1gPoh3z/TsH8zOCmDGwGAUnskunK8K/img.png&quot; data-alt=&quot;https://pypi.org/project/strawberry-graphql/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SSrgb/btsM1gPoh3z/TsH8zOCmDGwGAUnskunK8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSSrgb%2FbtsM1gPoh3z%2FTsH8zOCmDGwGAUnskunK8K%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;220&quot; height=&quot;265&quot; data-filename=&quot;68747470733a2f2f6769746875622e636f6d2f737472617762657272792d6772617068716c2f737472617762657272792f7261772f6d61696e2f2e6769746875622f6c6f676f2e706e67.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;362&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://pypi.org/project/strawberry-graphql/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전글에서 REST, gRPC 그리고 GraphQL를 비교해봤습니다. 이번에는 Python의 GraphQL 사용을 돕는 라이브러리를 비교해보려고 합니다. 솔직히 말하자면 FastAPI 유저인 저에게는 FastAPI가 공식문서에서 추천한 &lt;b&gt;Strawberry&lt;/b&gt;가 정답으로 자리잡고 있고, 이미 Strawberry를 이용해서 구현했지만 공부하는 차원에서 각 라이브러리의&amp;nbsp;&lt;b&gt;사용성&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;문서화와 개발자 경험(DX)&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;타입 지원&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;웹 프레임워크 통합성&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;프로젝트 유지보수 및 커뮤니티 현황&lt;/b&gt;을 살펴보겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;왜 라이브러리 비교기준에서 성능이 빠졌을지 의문을 가질 수 있습니다. 하지만 물론 저도 처음에는 알아봤지만 GraphQL 서버 성능은 주로 쿼리 실행 엔진의 효율과 리졸버에서의 I/O 처리 방식에 따라 좌우되며, 세 라이브러리 모두 Python용 GraphQL 코어 라이브러리를 기반으로 하고 있어, 기본적인 쿼리 파싱/실행 성능은 큰 차이가 없다고 판단되어 비교기준에서 제외했습니다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Python &amp;amp; GraphQL 라이브러리들&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python에서 GraphQL API를 구현하기 위해 많이 사용되는 라이브러리로 &lt;b&gt;Strawberry&lt;/b&gt;, &lt;b&gt;Graphene&lt;/b&gt;, &lt;b&gt;Ariadne&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;우선 Graphene 프로젝트는 가장 역사가 오래되었지만 최근에 유지보수나 업데이트가 활발하지 않은것으로 확인되었습니다.&lt;/li&gt;
&lt;li&gt;Strawberry는 보다는 최근에 만들어졌으며 github에서 활발히 논의가 이뤄지는게 확인이 되었습니다.&lt;/li&gt;
&lt;li&gt;Ariadne 는 코드우선방식을 채택한 Graphene나 Strawberry와 다르게&amp;nbsp;스키마 우선 방식을 채택하여 비교할 가치가 있다고 생각했습니다.&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;style5&quot; /&gt;
&lt;h2 data-end=&quot;590&quot; data-start=&quot;562&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;사용성: Pythonic한 코드 작성의 간결함&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;698&quot; data-start=&quot;592&quot; data-ke-size=&quot;size16&quot;&gt;GraphQL 라이브러리의 &lt;b&gt;사용성&lt;/b&gt;은 얼마나 간결하고 Pythonic하게 스키마와 리졸버를 작성할 수 있는지를 의미합니다. 각 라이브러리는 &lt;b&gt;스키마 정의 방식&lt;/b&gt;부터 차이가 있습니다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-end=&quot;698&quot; data-start=&quot;592&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Graphene&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;698&quot; data-start=&quot;592&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코드 우선(code-first)&lt;/b&gt; 접근 방식의 대표격입니다. Django ORM 스타일과 비슷하게 Python 클래스를 정의하여 GraphQL 스키마를 만듭니다. Django 개발자에게 친숙할 수 있지만, 작은 스키마에도 많은 &lt;b&gt;보일러플레이트&lt;/b&gt;(boilerplate) 코드와 규칙이 필요할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743233513597&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import graphene

class User(graphene.ObjectType):
    name = graphene.String()
    age = graphene.Int()

class Query(graphene.ObjectType):
    user = graphene.Field(User)

    def resolve_user(self, info):
        return User(name=&quot;Patrick&quot;, age=100)

schema = graphene.Schema(query=Query)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Strawberry&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Graphene과 마찬가지로 &lt;b&gt;코드 우선&lt;/b&gt; 접근이지만, 최신 Python 기능인 &lt;b&gt;타입 힌트&lt;/b&gt;와 &lt;b&gt;데이터클래스&lt;/b&gt;를 적극 활용하여 보다 간결하고 &lt;b&gt;Pythonic&lt;/b&gt;한 인터페이스를 제공합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743233754484&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import strawberry


@strawberry.type
class User:
    name: str
    age: int

@strawberry.type
class Query:
    @strawberry.field
    def user(self) -&amp;gt; User:
        return User(name=&quot;Patrick&quot;, age=100)


schema = strawberry.Schema(query=Query)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Ariadne&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스키마 우선(schema-first)&lt;/b&gt; 접근 방식을 취합니다. 즉, GraphQL의 &lt;b&gt;SDL(schema definition language)&lt;/b&gt; 문법으로 스키마를 문자열이나 파일로 작성하고, 해당 스키마의 각 타입과 필드에 대응하는 &lt;b&gt;리졸버 함수&lt;/b&gt;를 Python 코드로 연결합니다​.&lt;/p&gt;
&lt;pre id=&quot;code_1743233976009&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from ariadne import gql, QueryType, make_executable_schema

# GraphQL 스키마 정의 (SDL)
type_defs = gql(&quot;&quot;&quot;
    type User {
        name: String!
        age: Int!
    }

    type Query {
        user: User!
    }
&quot;&quot;&quot;)

# 리졸버 객체 생성
query = QueryType()

@query.field(&quot;user&quot;)
def resolve_user(_, info):
    return {&quot;name&quot;: &quot;Patrick&quot;, &quot;age&quot;: 100}

schema = make_executable_schema(type_defs, query)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;​Ariadne을 쓰면 GraphQL 자체의 표준 문법으로 API를 명세하고, 비즈니스 로직 연결은 @query.field(&quot;fieldName&quot;) 같은 데코레이터나 ObjectType(&quot;TypeName&quot;) 객체를 통해 매핑합니다​. 이 방식은 GraphQL을 잘 아는 개발자에게는 &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;Graphene처럼 복잡한 Python 클래스 계층을 만들 필요 없이 &lt;b&gt;스키마는 스키마대로, 코드 로직은 코드대로&lt;/b&gt; 분리할 수 있어 단순합니다. 반면 Python 타입 힌트를 이용해 자동으로 스키마를 생성해주진 않기에, GraphQL SDL 문법을 별도로 작성해야 하는 부담이 있습니다. &lt;span&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;제 개인적인 견해로는 Strawberry의 방식이 가장 Pythonic 한것 같습니다. Django처럼 python type과 별도 호환이 필요한것도 아니고, Ariadne처럼 type_defs SDL을 별도로 관리해줄 필요도 없습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-end=&quot;3040&quot; data-start=&quot;3018&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문서화 수준과 개발자 경험 (DX)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;3128&quot; data-start=&quot;3042&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개발자 경험(DX)&lt;/b&gt;은 학습 곡선, 문서의 친절함, 사용 시 장애 요소 등을 포함합니다. 세 라이브러리는 이 부분에서 다음과 같은 장단점을 보입니다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-end=&quot;3128&quot; data-start=&quot;3042&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Graphene&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오랜 기간 사용되었지만 공식 &lt;b&gt;문서화가 다소 빈약&lt;/b&gt;하고 최신 업데이트를 충분히 반영하지 못한 면이 있습니다​. (개인적으로 ASGI의 공식문서를 보는 느낌이 들었을 정도로 재미없는 공식문서입니다.) 특히 Graphene v3로 업그레이드되는 동안 문서와 주변 도구들이 늦게 따라와 혼란이 있었고, 예를 들어 &lt;a href=&quot;https://blog.muehlemann-popp.ch/graphene-vs-strawberry-which-is-better-for-providing-a-graphql-api-382633f6cd50&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;DataLoader&lt;/b&gt; 문서가 깨져 한동안 제대로 안내되지 않았다는 지적&lt;/a&gt;도 있었습니다​.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 Graphene은 &lt;b&gt;성숙한 생태계&lt;/b&gt;를 갖추고 있어, Stack Overflow 등 Q&amp;amp;A나 블로그 게시물이 풍부한 편입니다. 다만 2020~2021년 경 프로젝트가 잠시 정체되면서 유지보수 부재 논란이 있었고, 실제로 2021년에는 &lt;a href=&quot;https://github.com/graphql-python/graphene/issues/1312&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;maintainer를 모집하는 공지&lt;/a&gt;가 올라오기도 했습니다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이후 다행히 새로운 기여자들이 참여하여 현재는 릴리스가 이어지고 있지만, 이러한 과거 때문에 &lt;/span&gt;&lt;b&gt;미래 지원에 대한 우려&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;가 있는것도 사실입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Strawberry&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교적 &lt;b&gt;신생 프로젝트&lt;/b&gt;지만 문서가 잘 정비되어 있고, &lt;b&gt;예제와 API 레퍼런스&lt;/b&gt;가 명확합니다. FastAPI 등 최신 프레임워크 철학과 맞닿아 있어 학습 개념도 쉽게 와닿으며, 공식 문서도 타입 힌트의 활용, Pydantic 통합 등 실용적인 내용을 담고 있습니다. 실제로 Graphene 사용 중 겪었던 서드파티 라이브러리 호환성 문제(예: 구독 프로토콜, 오래된 의존성 등)들이 Strawberry에서는 비교적 원활하게 해결되었고, &lt;a href=&quot;https://blog.muehlemann-popp.ch/graphene-vs-strawberry-which-is-better-for-providing-a-graphql-api-382633f6cd50#:~:text=We%20have%20projects%20with%20Graphene,ws%60&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Async 지원이나 최신 GraphQL 기능(예: &lt;b&gt;서브스크립션&lt;/b&gt;, &lt;b&gt;데이터로더&lt;/b&gt;)도 &lt;b&gt;기본 제공&lt;/b&gt; 또는 쉽게 사용할 수 있도록 되어 있습니다​.&lt;/a&gt;&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;​&lt;span&gt;&lt;/span&gt;다만 Strawberry는 아직 버전 1.0 이전(현재 0.x대)이라 &lt;b&gt;API가 완전히 안정된 상태는 아니며&lt;/b&gt; 종종 breaking change가 발생할 수 있습니다. 그럼에도 &lt;b&gt;활발한 개발과 빈번한 릴리스&lt;/b&gt;로 이슈 해결이 빠르고, Discord 등지에서 활발한 커뮤니티 지원을 받을 수 있습니다​.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Ariadne&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서와 예제가 잘 제공되어 있고, &lt;b&gt;개념이 단순&lt;/b&gt;하여 진입 장벽이 높지 않습니다. 공식 사이트의 튜토리얼이나 블로그 글을 통해 &lt;b&gt;SDL&amp;nbsp;작성부터 ASGI 앱 구동까지&lt;/b&gt; 간단한 예제로 쉽게 따라할 수 있습니다. Ariadne의 철학이 &quot;작고 기억하기 쉬운 API&quot;를 지향하여, 실제 사용해보면 필요한 기능만 최소한의 API로 제공해 &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;예를 들어 make_executable_schema로 스키마 문자열과 리졸버를 연결하고, GraphQL ASGI 앱으로 서비스하는 흐름이 명쾌합니다:&lt;/p&gt;
&lt;pre id=&quot;code_1743234596581&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import os

from ariadne import make_executable_schema
from ariadne.asgi import GraphQL
from mygraphql import type_defs, resolvers

schema = make_executable_schema(type_defs, resolvers)
application = GraphQL(schema)&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;개발자 경험 측면에서 GraphQL 스키마 설계와 Python 코드를 분리하는 것을 선호하는 경우 Ariadne이 만족도를 줄것으로 보입니다. 반면, 대규모 프로젝트에서 &lt;b&gt;Python 타입 시스템과 연계된 도구(Mypy, Pydantic 등)를 활용한 개발 편의&lt;/b&gt;는 Ariadne에서 자동으로 얻기 어렵습니다. 모든 타입 정의를 GraphQL SDL로 수동 관리해야 하므로, 코드 상에서 타입참조 추적이나 에디터 자동완성 같은 부분은 Strawberry보다 부족할 수 있습니다. 요약하면 Ariadne은 &lt;b&gt;심플함&lt;/b&gt;이 강점이지만, &lt;b&gt;Python 도구들과의 통합된 DX&lt;/b&gt;는 상대적으로 단순한 편입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-end=&quot;8387&quot; data-start=&quot;8351&quot; data-ke-size=&quot;size26&quot;&gt;타입 지원: Python 타입 힌트 및 Pydantic 통합&lt;/h2&gt;
&lt;p data-end=&quot;8513&quot; data-start=&quot;8389&quot; data-ke-size=&quot;size16&quot;&gt;Python 백엔드 개발자에게 &lt;b&gt;타입 시스템과의 통합&lt;/b&gt;은 매우 중요한 요소입니다. 각 라이브러리가 &lt;b&gt;Python의 타입 힌트&lt;/b&gt;나 &lt;b&gt;데이터 검증 라이브러리(Pydantic 등)&lt;/b&gt;와 어떻게 호환되는지 살펴보겠습니다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Graphene&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등장 시기가 Python 타입 힌트가 도입되던 무렵이라, Graphene 자체는 &lt;b&gt;타입 힌트를 활용한 설계는 아닙니다.&lt;/b&gt; 스키마 정의에 별도의 graphene 타입 클래스를 사용하기 때문에, IDE 상에서 Python 타입 체크를 해주진 않습니다. 예를 들어 age = graphene.Int()로 필드를 정의해도 해당 클래스의 age 속성은 단순히 GraphQL 필드이지, Python 정수형이라는 힌트를 갖지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Graphene은 &lt;b&gt;정적 타입 체크 도구(mypy)&lt;/b&gt;와의 연계는 부족합니다. 한편 Graphene은 Django 모델이나 SQLAlchemy 모델과의 통합을 통해 &lt;b&gt;모델 필드로부터 GraphQL 필드를 생성&lt;/b&gt;하는 기능을 제공하지만, &lt;b&gt;Pydantic&lt;/b&gt; 같은 독립적인 검증 모델과의 공식 통합은 없습니다. Pydantic 모델을 GraphQL 스키마로 이용하려면 수동으로 Graphene 타입을 만들어 매핑해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Strawberry&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;타입 힌트 우선 디자인&lt;/b&gt;을 내세운 라이브러리답게, 모든 GraphQL 타입 정의와 필드에 Python 타입 힌트를 사용합니다​. 덕분에 개발자는 &lt;b&gt;자연스러운 Python 데이터클래스&lt;/b&gt;를 작성하듯이 GraphQL 타입을 정의할 수 있고, IDE 자동완성이나 mypy 같은 도구의 혜택을 누릴 수 있습니다. 또한 Strawberry는 &lt;b&gt;Pydantic&lt;/b&gt;과의 통합을 실험적으로 지원하여, 기존 Pydantic 모델을 재사용해 GraphQL 타입을 만들 수 있습니다. &lt;a href=&quot;https://strawberry.rocks/docs/integrations/pydantic&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;에 따르면 &lt;b&gt;&quot;Strawberry는 Pydantic을 지원하며, Pydantic 모델로부터 Strawberry 타입을 생성해 코드를 두 번 작성할 필요를 없애준다&quot;&lt;/b&gt;고 합니다​. (하지만 저는 아직 experimental 단계의 기능이어서 실제 회사에서는 사용하지 못했습니다.)&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;strawberry.experimental.pydantic.type 데코레이터를 활용하여... Pydantic의 필드 정의를 읽어들여 &lt;b&gt;자동으로 해당 GraphQL 타입&lt;/b&gt;을 생성합니다​. 예를 들어, Pydantic 모델 User(BaseModel)이 있을 때:&lt;/p&gt;
&lt;pre id=&quot;code_1743232774687&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@strawberry.experimental.pydantic.type(model=User)
class UserType:
    id: strawberry.auto
    name: strawberry.auto&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 UserType GraphQL 객체 타입이 Pydantic User의 id, name 필드를 그대로 갖게 됩니다. 이 기능 덕분에 &lt;b&gt;데이터 검증(Pydantic)과 GraphQL 스키마 정의를 한 번에&lt;/b&gt; 관리할 수 있으며, 중복 모델 정의를 줄일 수 있습니다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Ariadne&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ariadne은 &lt;b&gt;GraphQL 스키마를 SDL로 정의&lt;/b&gt;하므로 Python의 타입 힌트 시스템과 직접적인 연계는 없습니다. GraphQL 스키마 자체의 타입(예: String, Int, 커스텀 타입 등)을 사용하고, 리졸버 함수의 시그니처에는 자유롭게 Python 타입 힌트를 달 수 있지만 그것이 GraphQL 스키마에 반영되진 않습니다. 따라서 &lt;b&gt;정적 타입 체크&lt;/b&gt;나 &lt;b&gt;에디터 지원&lt;/b&gt; 측면에서는 Ariadne이 특별한 도움을 주지 않습니다. Pydantic과의 통합도 별도로 제공되지 않습니다.&lt;/p&gt;
&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;리졸버 구현 내에서 Pydantic 모델을 활용&lt;/b&gt;할 수는 있습니다. 예를 들어, 뮤테이션 인자로 받은 딕셔너리를 Pydantic 모델로 파싱하여 검증하거나, 반대로 반환할 객체를 Pydantic 모델로 생성한 뒤 .dict()로 직렬화해 돌려주는 식으로 수작업 통합은 가능합니다. 이것은 순전히 개발자 몫이며, 프레임워크 차원의 지원은 아니죠. 정리하면 Ariadne은 &lt;b&gt;타입 힌트/모델 통합보다는 GraphQL 스키마 자체에 집중&lt;/b&gt;한 라이브러리입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-end=&quot;11170&quot; data-start=&quot;11151&quot; data-ke-size=&quot;size26&quot;&gt;주요 웹 프레임워크와의 통합성&lt;/h2&gt;
&lt;p data-end=&quot;11277&quot; data-start=&quot;11172&quot; data-ke-size=&quot;size16&quot;&gt;GraphQL 라이브러리 선택 시, &lt;b&gt;사용 중인 웹 프레임워크와 쉽게 통합&lt;/b&gt;되는지도 고려해야 합니다. FastAPI, Django 같은 인기 프레임워크와의 통합 현황을 비교해보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;FastAPI와 ASGI 프레임워크&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI는 공식 문서에서 Strawberry를 권장할 정도로 &lt;b&gt;Strawberry와의 궁합이 뛰어납니다&lt;/b&gt;​.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Strawberry는 strawberry.fastapi.GraphQLRouter를 제공하여 FastAPI 애플리케이션에 &lt;b&gt;손쉽게 GraphQL 엔드포인트를 추가&lt;/b&gt;할 수 있습니다​. app.include_router(GraphQLRouter(schema), prefix=&quot;/graphql&quot;) 한 줄로 통합이 가능합니다&lt;/p&gt;
&lt;pre id=&quot;code_1743235368528&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import strawberry
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter


@strawberry.type
class User:
	pass

@strawberry.type
class Query:
	pass

schema = strawberry.Schema(query=Query)

graphql_app = GraphQLRouter(schema)

app = FastAPI()
app.include_router(graphql_app, prefix=&quot;/graphql&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;Graphene&lt;/b&gt;의 경우, Starlette(및 FastAPI)에서 GraphQL을 쓰려면 &lt;b&gt;별도의 어댑터&lt;/b&gt;가 필요했습니다. 예전에는 Starlette의 GraphQLApp 클래스를 사용했으나 deprecated 되었고, 현재는 타사 라이브러리인 &lt;b&gt;starlette-graphene3&lt;/b&gt;를 이용해 FastAPI에 Graphene을 붙일 수 있습니다​.&amp;nbsp;이는 동작은 하지만 &lt;b&gt;공식 지원이 아닙니다.&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Ariadne&lt;/b&gt;은 &lt;b&gt;ASGI 앱&lt;/b&gt; 자체로 동작하므로 FastAPI에 &lt;a href=&quot;https://ariadnegraphql.org/docs/fastapi-integration#:~:text=Ariadne%20is%20an%20ASGI%20application,WebSocket%20traffic%20used%20by%20subscriptions&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;손쉽게 mount&lt;/b&gt;&lt;/a&gt;할 수 있습니다​. app.mount(&quot;/graphql&quot;, GraphQL(schema)) 형태로 서브 애플리케이션으로 붙이거나, FastAPI 라우터 내부에서 GraphQL(schema).handle_http 등을 직접 endpoint로 사용하는 방식도 가능합니다. 실제로 &lt;b&gt;Starlette, FastAPI, Flask&lt;/b&gt; 등과의 통합 예제가 문서에 제공됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743235562786&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from ariadne import QueryType, make_executable_schema
from ariadne.asgi import GraphQL
from fastapi import FastAPI

type_defs = &quot;&quot;&quot;
    type Query {
        hello: String!
    }
&quot;&quot;&quot;

query = QueryType()


@query.field(&quot;hello&quot;)
def resolve_hello(*_):
    return &quot;Hello world!&quot;


# Create executable schema instance
schema = make_executable_schema(type_defs, query)

# Mount Ariadne GraphQL as sub-application for FastAPI
app = FastAPI()

app.mount(&quot;/graphql/&quot;, GraphQL(schema, debug=True))&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Django&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Graphene&lt;/b&gt;은 Django 환경에서 가장 오래 사용된 GraphQL 솔루션입니다. &lt;a href=&quot;https://docs.graphene-python.org/projects/django/en/latest/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;graphene-django&lt;/b&gt;&lt;/a&gt; 패키지를 통해 Django의 ORM 모델을 Graphene 타입으로 쉽게 변환하고, Django view로 GraphQL 엔드포인트(/graphql)를 제공할 수 있습니다. Graphene-Django는 Django의 권한 체계나 필터 셋과 연동하는 등 &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;b&gt;Strawberry&lt;/b&gt;도 최근 &lt;a href=&quot;https://strawberry.rocks/docs/integrations/django&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;strawberry-graphql-django&lt;/b&gt;&lt;/a&gt;패키지를 통해 Django 통합을 제공하고 있습니다. Django 모델로부터 Strawberry 타입을 생성하거나, Django의 urlpatterns에 GraphQL view를 추가하는 기능을 갖추고 있지만, &lt;b&gt;성숙도는 이제 올라오는 단계&lt;/b&gt;입니다​. Graphene-Django가 수년간 다듬어져 온 것에 비해 Strawberry-Django는 비교적 새롭지만, 활발한 개발이 이루어지고 있rh 점차 안정화되고 있습니다​.&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Ariadne&lt;/b&gt;도 Django 통합을 지원하는 별도 패키지(&lt;a href=&quot;https://github.com/mirumee/ariadne-django&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;ariadne_django&lt;/b&gt;&lt;/a&gt;)가 있어서, GraphQLView.as_view()를 통해 Django의 URL 패턴에 GraphQL 엔드포인트를 등록할 수 있습니다.&lt;br /&gt;Django에서 Ariadne를 사용하면 GraphQL 스키마는 SDL로 정의하고, 각 앱별로 SDL 조각과 resolver 함수를 모듈화하여 구성하는 패턴을 취할 수 있습니다. Django 환경에서는 Graphene과 Ariadne 둘 다 많이 사용되고 있고, Strawberry는 신흥 대안으로 자리잡는 추세입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-end=&quot;14641&quot; data-start=&quot;14616&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;프로젝트 유지보수 상태 및 커뮤니티 활동&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;14748&quot; data-start=&quot;14643&quot; data-ke-size=&quot;size16&quot;&gt;오픈소스 라이브러리의 &lt;b&gt;유지보수 현황&lt;/b&gt;과 &lt;b&gt;커뮤니티 규모&lt;/b&gt;도 무시할 수 없는 요소입니다. 장기적으로 사용할 기술이라면 업데이트 주기나 이슈 대응 속도, 사용자 풀 등을 봐야 합니다.&lt;/p&gt;
&lt;h3 data-end=&quot;14748&quot; data-start=&quot;14643&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Graphene&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;14748&quot; data-start=&quot;14643&quot; data-ke-size=&quot;size16&quot;&gt;2015년경부터 개발되어 &lt;b&gt;가장 역사가 오래된 Python GraphQL 라이브러리&lt;/b&gt; 중 하나입니다. 스타 수, 다운로드 수 등 &lt;b&gt;인지도나 사용자 기반이 가장 크다&lt;/b&gt;고 볼 수 있습니다. 다만 앞서 언급했듯 한때 주요 유지관리자들의 손이 떠나 &lt;b&gt;프로젝트가 정체&lt;/b&gt;되었던 시기가 있었습니다​.&lt;/p&gt;
&lt;p data-end=&quot;14748&quot; data-start=&quot;14643&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;14748&quot; data-start=&quot;14643&quot; data-ke-size=&quot;size16&quot;&gt;현재는 여러 기여자의 참여로 다시 활기를 찾았고, 2022년 Graphene 3.x 정식 버전이 나오면서 꾸준한 릴리스가 이뤄지고 있습니다. &lt;b&gt;Graphene-Django&lt;/b&gt;나 &lt;b&gt;Graphene-SQLAlchemy&lt;/b&gt; 같은 서브 프로젝트들도 커뮤니티 주도로 관리되고 있습니다. 커뮤니티 Q&amp;amp;A는 많지만, 오래된 이슈나 v2 기준의 답변도 많아 정보를 걸러볼 필요는 있습니다.&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Strawberry&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2019년경 등장하여 &lt;b&gt;빠르게 성장&lt;/b&gt;한 프로젝트입니다. 아직 버전 1.0은 아니지만 0.x 버전대에서 &lt;b&gt;거의 매주 단위의 빈번한 업데이트&lt;/b&gt;가 진행되고 있습니다. Graphene 핵심 개발자 출신 등이 참여해 만들었기 때문에 Graphene의 단점을 보완하려 노력하고 있으며, &lt;b&gt;커뮤니티도 Discord, GitHub Discussions 등을 통해 활발&lt;/b&gt;합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Ariadne&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2018년경부터 시작되어 현재까지 &lt;b&gt;꾸준히 유지보수&lt;/b&gt;되고 있습니다. 주로 Django 기반 커머스 플랫폼 Saleor를 만든 Mirumee사가 중심이 되어 개발한 것으로 알려져 있으며, 해당 회사 프로젝트에서 활용하면서 안정성을 높여온 케이스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 버전도 0.x이지만 (최근 2025년 2월에 0.26 버전 릴리스​), 이는 Semantic versioning을 엄격히 따르는 것일 뿐 기능은 충분히 안정적입니다. 커뮤니티 규모는 Graphene이나 Strawberry보다 작아 보이지만, GraphQL을 선호하는 Python 개발자들 사이에서는 &lt;b&gt;평판이 좋은 편&lt;/b&gt;입니다. Slack/Discord 같은 공식 커뮤니티와 GitHub 이슈를 통해 질문을 해결할 수 있고, GraphQL 표준에 맞춰 돌아가는 만큼 일반 GraphQL 자료들도 참고 가능합니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-end=&quot;17398&quot; data-start=&quot;17377&quot; data-ke-size=&quot;size26&quot;&gt;Strawberry를 선택할 만한 이유&lt;/h2&gt;
&lt;p data-end=&quot;17734&quot; data-start=&quot;17400&quot; data-ke-size=&quot;size16&quot;&gt;위의 비교 요소들을 종합해보면, &lt;b&gt;Strawberry&lt;/b&gt;를 선택하는것은 타입힌트를 선호하는 Python 개발자에게는 좋은 선택일 것이며, 특히 FastAPI를 이용중인 팀에게는 가장 나은 대안이 될 것으로 보입니다.&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 몇 줄의 코드만으로 GraphQL 라우트를 추가할 수 있었고, FastAPI의 &lt;b&gt;의존성 주입&lt;/b&gt;이나 &lt;b&gt;보안 처리&lt;/b&gt;도 Strawberry의 컨텍스트 기능과 잘 연계되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 Strawberry로 GraphQL을 구현할 때 Python 코드와 GraphQL 스키마 사이의 &lt;b&gt;갭이 느껴지지 않았고&lt;/b&gt; 자연스러웠습니다. 반면 Graphene은 Django 스타일이라 호불호가 있었고 (우리 팀에는 Django 경험이 적은 인원도 있어 학습이 필요해 보였습니다.), Ariadne은 SDL 작성에 익숙하지 않은 몇몇에게 초기 진입장벽이 존재했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;18725&quot; data-start=&quot;18488&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;유지보수와 향후 전망&lt;/b&gt;도 선택에 영향을 줄것입니다. 물론 미래일은 알 수 없지만, 최근의 모멘텀만 보자면 Strawberry는 적극적으로 개선되고 있어 새로운 GraphQL 기능이나 버그 수정이 빠른것으로 파악되었스빈다. Graphene은 현재도 사용자가 많지만 만약 메인테이너 이탈이 다시 생긴다면 대응이 느려질 수 있다는 걱정이 있었죠. Ariadne도 안정적이지만 개발 커뮤니티의 규모가 Strawberry만큼 크진 않아 정보 구하기 측면에서는 Strawberry 쪽이 나았습니다.&lt;/p&gt;
&lt;p data-end=&quot;18725&quot; data-start=&quot;18488&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;19014&quot; data-start=&quot;18727&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로, &lt;b&gt;기능성&lt;/b&gt; 측면도 Strawberry는 부족함이 없었습니다. GraphQL 서브스크립션, 파일 업로드, 권한 제어 등 필요한 기능을 플러그인이나 내장 기능으로 지원했고, Graphene에서 쓰던 방식을 거의 대등하거나 더 편리한 방법으로 대체할 수 있었습니다. 예컨대 Graphene-Django에서 쓰던 JWT 인증 미들웨어를 Strawberry의 dependencies로 손쉽게 재구현하거나, DataLoader를 활용해 N+1 문제를 해결하는 것도 Strawberry가 잘 지원했습니다.&lt;/p&gt;
&lt;p data-end=&quot;19014&quot; data-start=&quot;18727&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;19169&quot; data-start=&quot;19016&quot; data-ke-size=&quot;size16&quot;&gt;이러한 이유들로, Python을 이용하는 팀이 Strawberry를 GraphQL 개발에 사용하는 것은 좋은 선택이 될 것입니다. 개발 속도 뿐만 아니라 개발자 경험 측면에서도 좋은 선택으로 보입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;</description>
      <category>탐구 생활/GraphQL</category>
      <category>ariadne</category>
      <category>fastapi graphql</category>
      <category>Graphene</category>
      <category>strawberry</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/85</guid>
      <comments>https://probehub.tistory.com/85#entry85comment</comments>
      <pubDate>Sat, 29 Mar 2025 13:52:56 +0900</pubDate>
    </item>
    <item>
      <title>GraphQL vs REST vs gRPC: 개념과 설계 철학, 성능 특성 비교</title>
      <link>https://probehub.tistory.com/84</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;graphql-rest-grpc.png&quot; data-origin-width=&quot;1013&quot; data-origin-height=&quot;429&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1ImSn/btsM04Pd79h/gen9dZmY64aoreSVfDHzyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1ImSn/btsM04Pd79h/gen9dZmY64aoreSVfDHzyk/img.png&quot; data-alt=&quot;https://pradeepl.com/blog/api/rest-vs-graphql-vs-grpc/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1ImSn/btsM04Pd79h/gen9dZmY64aoreSVfDHzyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1ImSn%2FbtsM04Pd79h%2Fgen9dZmY64aoreSVfDHzyk%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;550&quot; height=&quot;233&quot; data-filename=&quot;graphql-rest-grpc.png&quot; data-origin-width=&quot;1013&quot; data-origin-height=&quot;429&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://pradeepl.com/blog/api/rest-vs-graphql-vs-grpc/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 REST 와 gRPC로 데이터를 서빙하던 FastAPI 서버에 &lt;a href=&quot;https://graphql.org/learn/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GraphQL&lt;/a&gt;을 도입하게 되었습니다. 필드가 많은 데이터를 서빙하는 서버였는데, 다수의 클라이언트에 서로 다른 인터페이스의 API를 서빙해야 했습니다. 그렇다면 REST 보다는 데이터 질의가 유연한 GraphQL을 도입하는게 좋겠다는 결론을 내렸고, 이미 잘 만들어진 라이브러리와 공식문서 덕분에 3일만에 배포할 수 있었습니다. 일단 구현은 했고, 이제는 블로그에 글을 연재하면서 관련 이론과 케이스 스터디를 진행해보려고 합니다.&lt;/p&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와 gRPC 그리고 GraphQL의 개념, 설계철학을 알아보고 성능 특성을 비교하여 향후 프로젝트를 진행할때 적절한 기술을 선택할 수 있는 기반을 다지고자합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-end=&quot;468&quot; data-start=&quot;443&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;REST: 리소스 중심의 API 아키텍처&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_rest-diagram.png&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;848&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YRQCE/btsM13PjaxQ/Ui57aKFkqNsDhKTH0beXE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YRQCE/btsM13PjaxQ/Ui57aKFkqNsDhKTH0beXE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YRQCE/btsM13PjaxQ/Ui57aKFkqNsDhKTH0beXE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYRQCE%2FbtsM13PjaxQ%2FUi57aKFkqNsDhKTH0beXE0%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;315&quot; data-filename=&quot;edited_rest-diagram.png&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;848&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;967&quot; data-start=&quot;470&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;REST(Representational State Transfer)는 Roy Fielding&lt;/a&gt;의 논문에서 정립된 아키텍처 스타일로, &lt;b&gt;HTTP&lt;/b&gt; &lt;b&gt;프로토콜을 기반으로 클라이언트와 서버가 상호 작용하는 가장 전통적인 방식&lt;/b&gt;입니다​. RESTful API에서는 &lt;b&gt;리소스&lt;/b&gt;(자원)를 중심으로 URL 엔드포인트를 설계하고, HTTP의 표준 메서드를 통해 해당 자원에 대한 생성, 조회, 갱신, 삭제(CRUD) 작업을 수행합니다. REST의 중요한 특징은 &lt;b&gt;무상태성(stateless)&lt;/b&gt;으로, 각 요청이 이전 요청의 상태에 영향받지 않고 독립적으로 처리됩니다. 또한 &lt;a href=&quot;https://docs.aws.amazon.com/appsync/latest/devguide/what-is-rest.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;균일 인터페이스(uniform interface)&lt;/b&gt;&lt;/a&gt;를 지향하여, 일관된 방식으로 자원에 접근하고 표현하도록 규약합니다​.&lt;span data-state=&quot;closed&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-end=&quot;1466&quot; data-start=&quot;969&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;REST의 장점과 단점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대다수의 사람들에게 익숙한 개념이기 때문에 추가로 정리할 내용은 없는것 같습니다. 다른 기술들과 비교를 위해 장점, 단점만 정리하겠습니다.&lt;/p&gt;
&lt;h4 data-end=&quot;2123&quot; data-start=&quot;2108&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;REST의 장점:&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2686&quot; data-start=&quot;2124&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2229&quot; data-start=&quot;2124&quot;&gt;&lt;b&gt;단순성:&lt;/b&gt; HTTP 기반으로 동작하므로 이해하기 쉽고 구현이 용이합니다. 클라이언트에서 curl이나 브라우저로도 테스트할 수 있고, 기존 웹 기술 스택을 그대로 활용합니다.&lt;/li&gt;
&lt;li data-end=&quot;2417&quot; data-start=&quot;2230&quot;&gt;&lt;b&gt;광범위한 호환성:&lt;/b&gt; JSON, XML 등 &lt;b&gt;여러 포맷&lt;/b&gt;을 주고받을 수 있고​, 언어나 플랫폼과 무관하게 모든 HTTP 클라이언트가 이용 가능하여 범용성이 높습니다​&lt;/li&gt;
&lt;li data-end=&quot;2558&quot; data-start=&quot;2418&quot;&gt;&lt;b&gt;캐싱 등 웹 인프라 활용:&lt;/b&gt; HTTP 프로토콜의 캐시 제어 등을 활용하여 성능 개선이 가능하고, 로드 밸런서나 게이트웨이 등 기존 인프라와 쉽게 연동됩니다​.&lt;/li&gt;
&lt;li data-end=&quot;2686&quot; data-start=&quot;2559&quot;&gt;&lt;b&gt;성숙한 생태계:&lt;/b&gt; 오랜 기간 사용되어 온 만큼 문서화, 모니터링, 테스트를 위한 &lt;b&gt;풍부한 도구&lt;/b&gt;와 커뮤니티 지식이 축적되어 있습니다​.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;2703&quot; data-start=&quot;2688&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;REST의 단점:&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3230&quot; data-start=&quot;2704&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2837&quot; data-start=&quot;2704&quot;&gt;&lt;b&gt;과/미페치 문제:&lt;/b&gt; 고정된 응답 구조로 인해 필요 이상의 데이터를 받거나 필요한 데이터를 한 번에 못 받아 여러 호출이 필요한 문제가 흔합니다​.&lt;/li&gt;
&lt;li data-end=&quot;2989&quot; data-start=&quot;2838&quot;&gt;&lt;b&gt;성능 한계:&lt;/b&gt; 텍스트 기반 통신으로 이진 프로토콜에 비해 메시지 크기, 처리에서 불이익이 있으며 HTTP/1.x 오버헤드로 인해 고성능이 요구되는 환경에서는 비효율적일 수 있습니다​. 대용량 데이터 전송이나 실시간 상호작용에는 부적합합니다.&lt;/li&gt;
&lt;li data-end=&quot;3102&quot; data-start=&quot;2990&quot;&gt;&lt;b&gt;버전 관리 부담:&lt;/b&gt; 클라이언트 호환성을 유지하면서 API를 발전시키기 어려워, 새로운 기능 추가 시 종종 API 버전을 올려야 합니다. 이는 API 소비자와 제공자 모두에게 부담이 됩니다.&lt;/li&gt;
&lt;li data-end=&quot;3230&quot; data-start=&quot;3103&quot;&gt;&lt;b&gt;실시간 기능의 부재:&lt;/b&gt; 표준 REST에는 서버-&amp;gt;클라이언트 &lt;b&gt;푸시&lt;/b&gt;나 실시간 스트리밍 기능이 없어, 실시간 업데이트가 필요할 경우 WebSocket이나 Server-Sent Events 등을 별도로 구현해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot; data-start=&quot;7816&quot; data-end=&quot;7838&quot;&gt;&lt;b&gt;gRPC: 고성능 RPC 프레임워크&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;7840&quot; data-end=&quot;8338&quot; data-ke-size=&quot;size16&quot;&gt;gRPC는 Google이 개발하여 2015년에 오픈 소스로 공개한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;고성능 RPC&lt;/b&gt;(Remote Procedure Call) 프레임워크입니다​. gRPC에서는 서비스와 메서드를 정의한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;프로토콜 버퍼(Protocol Buffers)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기반의 인터페이스 정의서(IDL)를 토대로, 클라이언트와 서버가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;강력한 타입의 Stub 코드&lt;/b&gt;를 생성하여 사용합니다. 쉽게 말해, 원격 서버의 함수를 로컬에서 호출하듯이 사용할 수 있게 해주는 기술입니다. HTTP/2 프로토콜을 전송 계층으로 사용하며, 요청과 응답 데이터는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;프로토콜 버퍼&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;형식으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;바이너리 직렬화&lt;/b&gt;되어 교환됩니다​. 이러한 구조 덕분에 gRPC는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;빠르고 효율적인 통신&lt;/b&gt;을 지원하며, 다양한 프로그래밍 언어에서 생성된 코드로 상호 운용이 가능합니다.&lt;/p&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;8340&quot; data-end=&quot;8852&quot; data-ke-size=&quot;size16&quot;&gt;gRPC의 설계 철학은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;계약 우선(contract-first)&lt;/b&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;엄격한 스키마&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;및&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;효율성&lt;/b&gt;으로 요약됩니다​. API의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;인터페이스&lt;/b&gt;를 먼저 .proto 파일로 정의하고, 이 계약에 맞춰 양측 코드가 생성되므로 개발자간 명세 불일치가 적고 안정성이 높습니다​. 또한 메시지 형식을 바이너리로 엄격히 정의하기 때문에, REST나 GraphQL처럼 각 요청마다 &lt;b&gt;데이터를 해석하고 검증하는 오버헤드가 줄어듭니다.&lt;/b&gt; gRPC는 데이터보다는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;동작&lt;/b&gt;(원격 함수 호출)에 초점을 맞추고 있어서, 마치 객체의 메서드를 호출하듯 서비스를 사용하게 해줍니다. 그 대신 REST처럼 리소스 경로만 알면 되는 것이 아니라, 사전에 컴파일된 클라이언트 코드(Stub)를 사용해야 한다는 차이가 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;10202&quot; data-end=&quot;10437&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;gRPC를 구성하는 요소&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 python gRPC 시리즈에서 다룬 내용이지만, 이 글만 읽어서 gRPC가 낯선 사람들을 위해 간단히 gRPC를 구성하는 요소들을 정리해보자면 다음과 같습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;landing-2.svg&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;327&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nOCxh/btsM0qkq7tD/BbdoOyZCkwokYk9lkQhxrk/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nOCxh/btsM0qkq7tD/BbdoOyZCkwokYk9lkQhxrk/tfile.svg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nOCxh/btsM0qkq7tD/BbdoOyZCkwokYk9lkQhxrk/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnOCxh%2FbtsM0qkq7tD%2FBbdoOyZCkwokYk9lkQhxrk%2Ftfile.svg&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;552&quot; height=&quot;327&quot; data-filename=&quot;landing-2.svg&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;327&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;10202&quot; data-end=&quot;10437&quot;&gt;.proto 파일에 의해 정의된 Proto Request, Proto Response가 각 서비스에 정의된 gRPC Stub, gRPC Server에 의해 통신되는 구조입니다. 이때 각 연결된 서비스들은 엄격히 정의된 .proto 파일의 제한을 받는것으로 안정성을 향상시킵니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-start=&quot;10202&quot; data-end=&quot;10437&quot;&gt;&lt;b&gt;gRPC의 활용&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;10202&quot; data-end=&quot;10437&quot;&gt;gRPC는 웹 브라우저 환경에서 바로 사용하기 어려운데, 브라우저가 HTTP/2 상의 임의의 바이너리 프로토콜 호출을 직접 수행할 수 없기 때문입니다​. 이를 해결하기 위해 &lt;a href=&quot;https://github.com/grpc/grpc-web&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;gRPC-Web&lt;/a&gt;이라는 별도 기술이 존재하지만, 순수 웹 클라이언트만 대상으로 하는 공개 API라면 REST나 GraphQL 대비 구현이 복잡해질 수 있습니다. 그럼에도 불구하고 서버-서버 통신이나 모바일 앱과 백엔드 간 통신 등&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;통제된 환경&lt;/b&gt;에서는 gRPC의 이점이 커서, 강력한 타입 안전성과 성능 때문에 선호됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-start=&quot;10202&quot; data-end=&quot;10437&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-end=&quot;10437&quot; data-start=&quot;10202&quot;&gt;gRPC는 주로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;내부 시스템&lt;/b&gt;이나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;마이크로서비스&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;아키텍처에서 서비스 간 통신에 많이 쓰이며, 외부 개발자에게 공개하는 API보다는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;사내 API&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;용도에 적합하다는 평가를 받습니다. 대표적인 도입 사례로는 Google, Netflix 외에도 Uber가 대규모 실시간 플랫폼에 gRPC를 활용하고 있는것으로 알려져있으며, &lt;a href=&quot;https://www.redhat.com/en/blog/grpc-use-cases&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Red Hat Blog&lt;/a&gt;에서는 gRPC의 활용에 대한 사례를 다루고 있어서 참고할만 합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-start=&quot;10202&quot; data-end=&quot;10437&quot;&gt;&lt;b&gt;gRPC의 장점과 단점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC의 가장 큰 강점은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;성능과 네트워크 효율&lt;/b&gt;입니다. HTTP/2의 멀티플렉싱과 헤더 압축, 프로토콜 버퍼의 바이너리 직렬화를 결합하여 매우 낮은 대기 시간(latency)과 높은 처리량(throughput)을 보여줍니다. 실제로 동일한 API를 gRPC로 구현하면 REST 대비 응답 속도가 크게 향상될 수 있는데, 이는 HTTP/2의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;스트림&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기능으로 다중 요청을 단일 연결에서 병렬 처리하고, 이진 데이터로 데이터 크기를 최소화하기 때문입니다​&lt;span&gt;&lt;/span&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 gRPC는&lt;span&gt; &lt;/span&gt;서버-&amp;gt;클라이언트 스트림, 클라이언트-&amp;gt;서버 스트림, 양방향&lt;b&gt; 스트림 등의 통신 패턴&lt;/b&gt;을 제공하기 때문에 실시간 양방향 통신이나 대용량 데이터 스트림 전송에 매우 유리합니다​&lt;span&gt;&lt;/span&gt;. 예를 들어 채팅, 화상통화, 실시간 데이터 피드 등은 gRPC의 스트리밍을 활용해 구현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-end=&quot;10200&quot; data-start=&quot;9546&quot; data-ke-size=&quot;size16&quot;&gt;한편, gRPC를 사용하려면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;초기 설정과 학습 부담&lt;/b&gt;이 다소 있습니다. 프로토콜 버퍼&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;IDL&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;문법을 익히고 .proto 파일을 작성해야 하며, 이를 기반으로 서버와 클라이언트 코드를 생성하는 빌드 과정을 거쳐야 합니다. REST나 GraphQL처럼 인간이 읽을 수 있는 JSON을 주고받는 것이 아니므로, 디버깅이나 테스트 시 일반적인 HTTP 툴로는 내부 내용을 직접 확인하기 어렵습니다​. &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;대신&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;grpcurl&lt;/b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;과 같은 전용 도구나 로깅을 통해 디버깅을 해야 합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-end=&quot;10200&quot; data-start=&quot;9546&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;10439&quot; data-end=&quot;10454&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;gRPC의 장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;10455&quot; data-end=&quot;11237&quot;&gt;
&lt;li data-start=&quot;10455&quot; data-end=&quot;10613&quot;&gt;&lt;b&gt;우수한 성능:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이진 프로토콜과 HTTP/2 기반으로 매우 빠른 응답 시간과 높은 처리량을 보입니다. 동일 환경에서 REST 대비 지연 시간이 현저히 낮고 효율적이라는 것이 입증되었습니다​.&lt;/li&gt;
&lt;li data-start=&quot;10614&quot; data-end=&quot;10776&quot;&gt;&lt;b&gt;스트리밍 지원:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;요청-응답 외에 스트리밍 방식 통신을 기본으로 지원하여, 실시간 양방향 데이터 송수신이나 스트림 전송에 용이합니다​. 별도의 프로토콜 없이도 스트리밍 API를 구현할 수 있습니다.&lt;/li&gt;
&lt;li data-start=&quot;10777&quot; data-end=&quot;10948&quot;&gt;&lt;b&gt;엄격한 스키마와 타입:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;프로토콜 버퍼 IDL로 메시지 구조와 서비스를 명시적으로 정의하므로, 클라이언트-서버 간 인터페이스가 명확하고 타입 안정성이 높습니다​. 컴파일 타임에 검증되므로 런타임 오류가 줄어듭니다.&lt;/li&gt;
&lt;li data-start=&quot;10949&quot; data-end=&quot;11113&quot;&gt;&lt;b&gt;다중 언어 및 플랫폼 지원:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;공식 툴체인이 다양한 언어를 지원하여(예: C++, Java, Python, Go 등), 서로 다른 기술 스택 사이의 통신도 통합된 방식으로 처리할 수 있습니다. 한 번 IDL을 작성하면 여러 언어의 클라이언트/서버 코드를 생성해 사용할 수 있습니다.&lt;/li&gt;
&lt;li data-start=&quot;11114&quot; data-end=&quot;11237&quot;&gt;&lt;b&gt;효율적인 데이터 전송:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;프로토콜 버퍼는 JSON 대비 데이터 크기가 작고 파싱 속도가 빠르며, 필드가 정의된 것만 전송하므로 불필요한 데이터가 오가지 않습니다. 네트워크 대역폭이 제한적인 환경에서도 유리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;11239&quot; data-end=&quot;11254&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;gRPC의 단점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;11255&quot; data-end=&quot;12081&quot;&gt;
&lt;li data-start=&quot;11255&quot; data-end=&quot;11418&quot;&gt;&lt;b&gt;브라우저 미지원:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;웹 브라우저에서는 gRPC 호출을 직접 할 수 없어 별도의 프록시(gRPC-Web) 또는 폴백이 필요합니다​. 따라서 웹 프런트엔드 대상의 공개 API에는 추가 작업이 요구됩니다.&lt;/li&gt;
&lt;li data-start=&quot;11419&quot; data-end=&quot;11577&quot;&gt;&lt;b&gt;학습 곡선:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;새로운 IDL 문법과 RPC 개념에 익숙해져야 하고, 빌드/코드생성 파이프라인을 구축해야 하므로 초기 진입 장벽이 있습니다​. 작은 팀이나 단순 서비스에는 설정 부담이 큽니다.&lt;/li&gt;
&lt;li data-start=&quot;11578&quot; data-end=&quot;11745&quot;&gt;&lt;b&gt;디버깅 어려움:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메시지가 바이너리라 사람이 읽기 어려워, 문제 발생 시 HTTP 기반 API만큼 직관적으로 디버깅하기 힘듭니다​. 전용 도구가 필요하며, 로그 분석 등으로 추상화된 호출을 추적해야 합니다.&lt;/li&gt;
&lt;li data-start=&quot;11746&quot; data-end=&quot;11896&quot;&gt;&lt;b&gt;유연성 감소:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;클라이언트가 고정된 stub와 메시지 형식에 의존하므로, GraphQL처럼 요청마다 필드를 가감할 유연성은 없습니다. 계약을 변경하려면 양측 코드를 재생성 및 배포해야 하므로, 빈번한 요구사항 변경이 있는 경우 대응이 번거로울 수 있습니다.&lt;/li&gt;
&lt;li data-start=&quot;11897&quot; data-end=&quot;12081&quot;&gt;&lt;b&gt;서비스 공개 어려움:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;주로 내부 통신용으로 설계되어 외부 공개 API로 사용할 때는 REST만큼 친숙한 표준이 아니며, 서드파티가 쉽게 호출하기 어렵습니다. 따라서 파트너나 공개 API에는 부적합할 수 있고 (대신 SDK 배포 필요), 이러한 경우 보통 REST나 GraphQL로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;게이트웨이&lt;/b&gt;를 마련해 공개합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-end=&quot;3257&quot; data-start=&quot;3232&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;GraphQL: 유연한 데이터 질의 언어&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;graphql-on-http.png&quot; data-origin-width=&quot;2608&quot; data-origin-height=&quot;1452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/babjDI/btsM2m2dnlV/pgGsEnGdrroqfozgiIhka1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/babjDI/btsM2m2dnlV/pgGsEnGdrroqfozgiIhka1/img.png&quot; data-alt=&quot;https://hasura.io/learn/graphql/intro-graphql/what-is-graphql/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/babjDI/btsM2m2dnlV/pgGsEnGdrroqfozgiIhka1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbabjDI%2FbtsM2m2dnlV%2FpgGsEnGdrroqfozgiIhka1%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;630&quot; height=&quot;351&quot; data-filename=&quot;graphql-on-http.png&quot; data-origin-width=&quot;2608&quot; data-origin-height=&quot;1452&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://hasura.io/learn/graphql/intro-graphql/what-is-graphql/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;3826&quot; data-start=&quot;3259&quot; data-ke-size=&quot;size16&quot;&gt;GraphQL은 Facebook(메타)이 2012년 개발하여 2015년 공개한 &lt;b&gt;API 질의 언어(query language)&lt;/b&gt;입니다​. 일반적인 REST API와 달리 &lt;b&gt;단일 엔드포인트&lt;/b&gt;에 요청을 보내서, &lt;b&gt;클라이언트가 필요한 데이터 필드를 선택하여 질의&lt;/b&gt;할 수 있다는 점이 가장 큰 특징입니다​&lt;span data-state=&quot;closed&quot;&gt;&lt;/span&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;/span&gt;. GraphQL 서버는 사전에 정의된 &lt;b&gt;스키마(schema)&lt;/b&gt;를 기반으로 클라이언트의 &lt;b&gt;질의(query)&lt;/b&gt;를 해석하고 적절한 데이터를 모아 응답합니다. 클라이언트는 한 번의 요청으로 &lt;b&gt;필요한 데이터만&lt;/b&gt; 정확히 받아올 수 있으므로, 다수의 REST호출을 대체하여 네트워크 효율을 높일 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;3826&quot; data-start=&quot;3259&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4358&quot; data-start=&quot;3828&quot; data-ke-size=&quot;size16&quot;&gt;GraphQL의 설계 철학은 &lt;b&gt;데이터 중심&lt;/b&gt;의 &lt;b&gt;유연성&lt;/b&gt;에 있습니다. 클라이언트가 데이터 형태를 지정해서 요청할 수 있으므로, API 설계자는 다양한 클라이언트 요구사항을 하나의 통합된 &lt;a href=&quot;https://www.geeksforgeeks.org/introduction-to-graphs-data-structure-and-algorithm-tutorials/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;그래프&lt;/b&gt;&lt;/a&gt;형태로 충족시킬 수 있습니다​. GraphQL 스키마는 타입 시스템으로 모델링된 도메인 객체와 그 관계(예: User 타입이 Post 타입 목록을 가질 수 있음)를 정의하며, 이를 통해 API는 자체 &lt;b&gt;자가문서화(self-documenting)&lt;/b&gt; 특성을 갖게 됩니다. 클라이언트는 스키마를 &lt;b&gt;인스펙션&lt;/b&gt;하여 어떤 쿼리와 필드가 존재하는지 알 수 있고, 필요에 따라 조합하여 사용할 수 있습니다. REST가 URL 단위의 &lt;b&gt;리소스&lt;/b&gt; 표현에 초점을 맞춘 반면, GraphQL은 전체 데이터 모델을 하나의 그래프로 파악하고 &lt;b&gt;필요한 부분집합을 질의&lt;/b&gt;하는 접근을 취합니다​.&lt;span data-state=&quot;closed&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-end=&quot;6078&quot; data-start=&quot;5585&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;GraphQL을 구성하는 요소&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GraphQL의 개념을 살펴본것만으로도 GraphQL Server, Query, Schema, Self-Documenting 와 같은 새로운 개념이 등장했습니다. 사실은 여기에 더해서 Mutation 이라는 개념도 있어서 이를 간단히 다뤄보겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. GraphQL Server&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GraphQL 서버는 클라이언트의 요청을 받아서, 사전에 정의된 스키마에 따라 해당 요청을 처리합니다. 서버는 Query, Mutation, 그리고 (옵션으로) Subscription 요청을 처리하며, 각 요청에 대해 적절한 리졸버(resolver)함수를 호출합니다. 이 리졸버 함수는 실제 데이터 소스(데이터베이스, 외부 API 등)와 연동되어 데이터를 가져오거나 수정하는 역할을 수행합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. Schema&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스키마는 GraphQL API의 계약(contract)을 정의하는 역할을 합니다. 여기에는 어떤 타입의 데이터가 존재하는지, 그리고 클라이언트가 어떤 &lt;b&gt;Query&lt;/b&gt;와 &lt;b&gt;Mutation&lt;/b&gt;을 수행할 수 있는지가 명시되어 있습니다. 스키마는 GraphQL SDL(Schema Definition Language)로 작성되며, 이를 통해 API의 모든 구성 요소를 명확하게 표현하고, 클라이언트는 스키마를 통해 API의 사용법을 쉽게 이해할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. Query&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리는 클라이언트가 서버에 &lt;b&gt;데이터 요청&lt;/b&gt;을 보낼 때 사용하는 명령어입니다. 쿼리 문법을 통해 클라이언트는 어떤 데이터가 필요한지, 그리고 그 데이터의 구조를 명시할 수 있습니다. 이로 인해 한 번의 요청으로 여러 종류의 관련 데이터를 효율적으로 조회할 수 있게 됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. Mutation&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mutation은 &lt;b&gt;데이터의 변경&lt;/b&gt;(생성, 수정, 삭제)을 담당하는 명령어입니다. REST API에서 POST, PUT, DELETE 요청과 유사하지만, GraphQL에서는 Mutation을 통해 요청에 대한 인자와 반환 필드를 구체적으로 지정할 수 있습니다. 이를 통해 클라이언트는 변경된 데이터의 상태를 정확하게 확인할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5. Resolver&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 쿼리나 Mutation이 호출되면, 해당 요청은 &lt;b&gt;리졸버(resolver)&lt;/b&gt;함수로 전달됩니다. 리졸버는 스키마의 각 필드와 매핑되어 있으며, 데이터 소스에 접근해 실제 데이터를 가져오거나 변경하는 핵심 로직을 수행합니다. 이 과정에서 데이터베이스 호출, 캐싱, 또는 외부 API 호출 등의 다양한 작업이 이루어집니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6. Subscription&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Subscription은 실시간 데이터 업데이트를 위한 메커니즘입니다. 클라이언트는 특정 이벤트나 데이터 변경 사항을 &lt;b&gt;구독(subscription)&lt;/b&gt;하고, 서버는 변경 사항이 발생할 때마다 이를 실시간으로 클라이언트에 전송합니다. 이 기능은 실시간 채팅, 알림, 스트리밍 데이터 등 동적 데이터 전송에 매우 유용합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7. Self-Documentation&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GraphQL은 스키마를 기반으로 &lt;b&gt;자동 문서화(Self-Documenting)&lt;/b&gt;가 가능합니다. GraphiQL 같은 도구를 사용하면, 클라이언트 개발자는 인터랙티브하게 API를 탐색하고 테스트할 수 있으며, 스키마에 정의된 모든 타입과 필드를 쉽게 확인할 수 있습니다. 이는 개발자 경험을 크게 향상시킵니다.&lt;/p&gt;
&lt;h3 data-end=&quot;6078&quot; data-start=&quot;5585&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;GraphQL의 활용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;6078&quot; data-start=&quot;5585&quot; data-ke-size=&quot;size16&quot;&gt;GraphQL은 &lt;b&gt;복잡한 도메인&lt;/b&gt;이나 &lt;b&gt;다양한 요구 사항&lt;/b&gt;이 있는 서비스에 특히 잘 맞습니다. 예를 들어, 전자상거래 애플리케이션에서 제품 정보, 재고, 사용자 리뷰를 각각 다른 마이크로서비스나 데이터베이스에서 가져와야 한다면 GraphQL 게이트웨이가 이를 한 번에 조합하여 제공할 수 있습니다. 이러한 이유로 다수의 기업들이 GraphQL을 도입하고 있는데, GitHub는 REST API에 이어 GraphQL 기반 API를 제공하고 있으며, Shopify, PayPal, Netflix, Facebook 등에서도 GraphQL을 통해 클라이언트에게 유연한 API를 제공합니다​. 내부 시스템을 통합하여 단일 API로 노출해야 하는 &lt;b&gt;BFF(Backend for Frontend)&lt;/b&gt; 패턴이나, 모바일/웹 클라이언트마다 데이터 요구 사항이 상이한 경우에 GraphQL은 유용한 선택입니다.&lt;/p&gt;
&lt;h3 data-end=&quot;6078&quot; data-start=&quot;5585&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;GraphQL의 장점과 단점&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;4360&quot; data-end=&quot;4824&quot; data-ke-size=&quot;size16&quot;&gt;GraphQL을 사용하면 클라이언트는 오직 필요한 데이터만 받으므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;네트워크 효율성&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;측면에서 유리합니다​. 특히&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;모바일&lt;/b&gt;이나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;저대역폭 환경&lt;/b&gt;에서는 다수의 REST 호출을 한 번의 GraphQL 호출로 대체함으로써 응답 속도를 향상시킬 수 있습니다. Facebook이 초기 GraphQL을 도입한 계기도 모바일 앱에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;뉴스피드&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;데이터를 가져올 때 여러 REST API 호출을 하나로 통합하기 위함이었습니다​. 또한 GraphQL은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.apollographql.com/blog/the-next-step-for-realtime-data-in-graphql#enter-graphql-subscriptions&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;구독(subscription)&lt;/b&gt;&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기능을 통해 서버가 실시간으로 데이터 변경 사항을 푸시할 수 있는 메커니즘을 제공합니다. 이를 활용하면 실시간 피드나 알림과 같은 기능도 GraphQL 안에서 구현이 가능합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;4360&quot; data-end=&quot;4824&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-start=&quot;4826&quot; data-end=&quot;5583&quot; data-ke-size=&quot;size16&quot;&gt;그러나 GraphQL은 서버 측 구현의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;복잡도&lt;/b&gt;가 높을 수 있습니다. 클라이언트가 임의의 질의를 보낼 수 있기 때문에, 서버는 각 질의에 맞춰 데이터를 조합하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;리졸버(resolver)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;로직을 잘 최적화해야 합니다. 복잡한 질의의 경우 서버 부하가 급증할 우려가 있고​, N+1 쿼리 문제 등 효율적인 데이터 로딩을 위한 추가 구현이 필요합니다. 또한 대부분의 GraphQL 질의는 POST 요청으로 이루어지므로,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;HTTP 레벨 캐싱&lt;/b&gt;이 바로 적용되지 않아 별도의 캐시 계층이나 Persisted Query 등을 설계해야 할 수 있습니다​. 클라이언트가 자유롭게 질의하는 만큼, 쿼리 복잡도나 요청 빈도를 제어하기 위한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;쿼리 제한&lt;/b&gt;(rate limiting)도 어렵습니다​. 마지막으로, REST만 사용해 온 개발자들에게는 GraphQL의 스키마 정의 언어, GraphiQL 등 새로운 도구와 개념을 학습해야 하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;학습 곡선&lt;/b&gt;이 존재합니다​. 반면 일단 스키마가 구축되고 나면, 프런트엔드 개발자는 백엔드 변경을 기다리지 않고도 필요한 데이터를 조합해 쓸 수 있어 개발 속도가 빨라지는 장점이 있습니다.&lt;/p&gt;
&lt;h4 data-end=&quot;6098&quot; data-start=&quot;6080&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;GraphQL의 장점:&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;6904&quot; data-start=&quot;6099&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;6238&quot; data-start=&quot;6099&quot;&gt;&lt;b&gt;필요한 데이터만 조회:&lt;/b&gt; 클라이언트가 요청한 정확한 데이터만 응답하여 불필요한 전송을 줄입니다​. 이로써 &lt;b&gt;오버페칭&lt;/b&gt;을 피하고 네트워크 효율을 높입니다.&lt;/li&gt;
&lt;li data-end=&quot;6400&quot; data-start=&quot;6239&quot;&gt;&lt;b&gt;버전관리 부담 경감:&lt;/b&gt; 스키마에 필드를 추가하더라도 기존 질의에는 영향이 없으므로 API &lt;b&gt;버전 관리&lt;/b&gt; 없이도 확장이 용이합니다​. 새로운 기능을 추가할 때도 구버전 유지 부담이 적습니다.&lt;/li&gt;
&lt;li data-end=&quot;6586&quot; data-start=&quot;6401&quot;&gt;&lt;b&gt;강력한 타입 시스템:&lt;/b&gt; GraphQL 스키마는 엄격한 타입 정의를 가지므로, 클라이언트-서버 간 데이터 계약이 명확하고 오류를 사전에 방지할 수 있습니다​. 또한 스키마를 통한 자동 문서화 및 도구 지원(GraphiQL 등)이 뛰어납니다.&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;6790&quot; data-start=&quot;6587&quot;&gt;&lt;b&gt;클라이언트 주도형 설계:&lt;/b&gt; 프론트엔드가 자신에게 최적화된 데이터 구조를 직접 선택할 수 있어, 다양한 화면이나 디바이스별로 효율적인 데이터 활용이 가능합니다. 하나의 요청으로 복수의 관련 데이터를 가져올 수 있어 복잡한 화면의 데이터를 &lt;b&gt;일괄 조회&lt;/b&gt;하기에 좋습니다​.&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;6904&quot; data-start=&quot;6791&quot;&gt;&lt;b&gt;실시간 기능:&lt;/b&gt; GraphQL의 구독(subscription)을 사용하면 데이터 변경 시 실시간으로 업데이트를 푸시 받을 수 있어, &lt;b&gt;별도 소켓 프로토콜 없이&lt;/b&gt;도 기본적인 실시간 통신이 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;6924&quot; data-start=&quot;6906&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;GraphQL의 단점:&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;7814&quot; data-start=&quot;6925&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;7099&quot; data-start=&quot;6925&quot;&gt;&lt;b&gt;서버 부하 및 복잡성:&lt;/b&gt; 유연성의 대가로 서버 구현이 복잡해질 수 있고, 잘못된 질의나 비효율적인 리졸버 구현은 서버 부하로 이어질 수 있습니다​. 특히 많은 관계를 한꺼번에 요청하는 복잡한 질의는 처리 비용이 큽니다.&lt;/li&gt;
&lt;li data-end=&quot;7270&quot; data-start=&quot;7100&quot;&gt;&lt;b&gt;캐싱 어려움:&lt;/b&gt; GraphQL 응답은 일반적으로 HTTP 레이어에서 캐싱하기 어려워​, REST처럼 브라우저나 CDN 캐시를 자동 활용할 수 없습니다. 개발자가 응답 캐싱 전략을 별도로 마련해야 할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;7450&quot; data-start=&quot;7271&quot;&gt;&lt;b&gt;권한 및 제한 관리:&lt;/b&gt; 엔드포인트가 하나로 통합되어 있고 질의 내용이 동적으로 변하므로, 요청별 &lt;b&gt;권한 제어&lt;/b&gt;나 &lt;b&gt;Rate Limit&lt;/b&gt; 정책 수립이 까다롭습니다​. 쿼리 깊이 제한, 시간 제한 등 별도 대책이 필요합니다.&lt;/li&gt;
&lt;li data-end=&quot;7636&quot; data-start=&quot;7451&quot;&gt;&lt;b&gt;초기 학습 비용:&lt;/b&gt; GraphQL 고유의 스키마 정의 문법, 쿼리 언어, 그리고 서버/클라이언트 라이브러리에 대한 학습이 필요하여 익숙해지기까지 시간이 걸립니다​. 소규모 프로젝트나 단순한 API에는 다소 &lt;b&gt;과설계(Overengineering)&lt;/b&gt;가 될 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;7814&quot; data-start=&quot;7637&quot;&gt;&lt;b&gt;도구 및 생태계 제약:&lt;/b&gt; REST만큼 범용적인 표준은 아니므로 일부 모니터링/디버깅 툴이나 기존 인프라와의 통합에서 제약이 있을 수 있습니다. 또한 HTTP가 아닌 별도 프로토콜을 사용하는 것이 아니기 때문에, HTTP/2 기반의 &lt;b&gt;전송 최적화&lt;/b&gt;나 바이너리 메시지 압축 측면에서는 gRPC보다 불리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-end=&quot;12116&quot; data-start=&quot;12083&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기술 비교: GraphQL vs REST vs gRPC&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;12227&quot; data-start=&quot;12118&quot; data-ke-size=&quot;size16&quot;&gt;앞서 살펴본 바와 같이, GraphQL, REST, gRPC는 개념적인 접근 방식부터 장단점까지 여러 측면에서 뚜렷이 대비됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;특징 RESTgRPCGraphQL: 표로 요약&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-sheets-baot=&quot;1&quot; data-sheets-root=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;특징&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;REST&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;gRPC&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;GraphQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;설계 철학&lt;/td&gt;
&lt;td&gt;&lt;span&gt;리소스 중심 (URL 및 HTTP 메서드로 자원 표현)​&lt;/span&gt;&lt;span&gt;. 클라이언트가 정해진 엔드포인트에 요청.&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;RPC 중심 (함수 호출 형태) 및 계약 기반 인터페이스 정의​&lt;/span&gt;&lt;span style=&quot;color: #1155cc;&quot;&gt;&lt;/span&gt;&lt;span&gt;. 프로토콜에 명시된 메서드 호출.&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;데이터 질의 중심으로 유연한 그래프 모델​&lt;/span&gt;&lt;span style=&quot;color: #1155cc;&quot;&gt;&lt;a href=&quot;https://www.infoq.com/presentations/rest-graphql-grpc/#:~:text=GraphQL%20was%20invented%20to%20solve,data%20through%20a%20single%20endpoint&quot;&gt;i&lt;/a&gt;&lt;/span&gt;&lt;span&gt;. 단일 엔드포인트에서 필요한 데이터 필드 질의.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 포맷&lt;/td&gt;
&lt;td&gt;&lt;span&gt;텍스트 (주로 JSON 또는 XML)&lt;/span&gt;&lt;span&gt;.&lt;br /&gt;사람이 읽을 수 있어 디버깅 용이.&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;바이너리 (프로토콜 버퍼 등)​&lt;/span&gt;&lt;span&gt;.&lt;br /&gt;효율적이지만 사람이 읽기 어려움.&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;텍스트 (일반적으로 JSON)​&lt;/span&gt;&lt;span&gt;. &lt;br /&gt;요청은 전용 질의 언어 문법 사용.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;통신 프로토콜&lt;/td&gt;
&lt;td&gt;&lt;span&gt;HTTP 1.1 (전통적인 요청/응답)​&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;HTTP/2 (멀티플렉싱, 헤더 압축 지원)​.&lt;br /&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;gRPC-Web 사용 시 HTTP/1.1 업그레이드.&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;
&lt;div&gt;
&lt;div&gt;주로 HTTP (HTTP/1.1 또는 HTTP/2 모두 가능).&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;요청-응답 패턴&lt;/td&gt;
&lt;td&gt;단순 요청/응답. 서버 푸시나 스트리밍은 별도 구현 필요.&lt;/td&gt;
&lt;td&gt;&lt;span&gt;다양한 RPC 패턴: 단일 요청-응답, 서버 스트림, 클라이언트 스트림, 양방향 스트림 지원&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;질의(Query)와 변경(Mutation), 구독(Subscription) 패턴 제공&lt;/span&gt;&lt;span&gt;. &lt;br /&gt;구독을 통해 실시간 업데이트 가능.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스키마/타입 체계&lt;/td&gt;
&lt;td&gt;공식 스키마 없음 .&lt;br /&gt;유연하지만 타입 불일치 가능.&lt;/td&gt;
&lt;td&gt;프로토콜 버퍼 (.proto)로 엄격한 스키마 정의. &lt;br /&gt;컴파일 시 검증되어 타입 안전성 보장.&lt;/td&gt;
&lt;td&gt;
&lt;div&gt;
&lt;div&gt;GraphQL SDL로 스키마 정의. &lt;br /&gt;스키마를 통해 질의 구조와 타입 검증, 자체 문서화 제공.&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;성능 및 효율&lt;/td&gt;
&lt;td&gt;&lt;span&gt;요청 당 오버헤드가 비교적 크고 (다중 요청 시 지연 증가)​&lt;/span&gt;&lt;span&gt;, 텍스트 파싱 부담. 캐싱으로 완화 가능.&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;이진 프로토콜과 HTTP/2로 고성능, 저지연. 다수 호출도 단일 연결로 효율적 처리​&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;필요한 데이터만 전송하여 불필요 트래픽 없음​&lt;/span&gt;&lt;span&gt;.&lt;br /&gt;단일 엔드포인트 처리로 서버 부하 증가 가능, JSON 기반이라 gRPC보다는 비효율.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;클라이언트 지원&lt;/td&gt;
&lt;td&gt;폭넓음: 브라우저, 모바일, 임베디드 등 HTTP 가능한 어디서나 사용.&lt;/td&gt;
&lt;td&gt;&lt;span&gt;제한적: 서버 및 네이티브 앱에서는 좋으나, 브라우저에서는 직접 호출 불가 (gRPC-Web 필요)​&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;
&lt;div&gt;
&lt;div&gt;폭넓음: HTTP 사용으로 REST와 유사하게 동작하며, 전용 클라이언트 라이브러리로 생산성 향상.&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;개발 생산성&lt;/td&gt;
&lt;td&gt;단순하고 바로 활용 가능. 방대한 문서/툴 지원. 변경 시 클라이언트 영향 커 버전 관리 필요.&lt;/td&gt;
&lt;td&gt;초기 설정 복잡하지만, 일단 구축 후에는 자동 생성 코드로 개발 편의 높음.양측 강제 계약으로 오류 감소. 디버깅은 어려움.&lt;/td&gt;
&lt;td&gt;
&lt;div&gt;
&lt;div&gt;초기 학습 필요하지만, 스키마 인트로스펙션 등으로 프론트엔드 개발 편의 높음. API 진화가 쉬워 빈번한 변경에 유연.&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;주요 사용 사례&lt;/td&gt;
&lt;td&gt;&lt;span&gt;공개 웹 API, 단순 CRUD 서비스, 광범위한 클라이언트 대상 서비스​&lt;/span&gt;&lt;span&gt;.&lt;br /&gt;브라우저 캐싱 활용이 중요한 경우.&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;마이크로서비스 내부 통신, 고성능 요구 시스템, 실시간 통신 (예: 스트리밍, 채팅)​&lt;/span&gt;&lt;span&gt;.&lt;br /&gt;클라이언트-서버가 통제된 환경.&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;다양한 데이터를 통합하는 게이트웨이, 복잡한 도메인의 API, 모바일 최적화 API​&lt;/span&gt;&lt;span&gt;.&lt;br /&gt;클라이언트별 요구사항이 다양한 서비스.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-end=&quot;16952&quot; data-start=&quot;16931&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;언제 어떤 기술을 선택해야 할까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;17064&quot; data-start=&quot;16954&quot; data-ke-size=&quot;size16&quot;&gt;지금까지 알아본 내용을 바탕으로, 프로젝트의 요구사항에 따라 세 가지 기술 중 무엇을 선택할지 간략히 정리해봅닌다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;18278&quot; data-start=&quot;17066&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;17432&quot; data-start=&quot;17066&quot;&gt;&lt;b&gt;REST를 선택:&lt;/b&gt; 클라이언트 종류가 많고 범용적인 &lt;b&gt;공개 API&lt;/b&gt;를 제공하거나, &lt;b&gt;간단한 CRUD&lt;/b&gt; 위주의 서비스를 신속하게 구축해야 할 때는 REST가 적합해보입니다. REST는 브라우저를 포함한 모든 환경에서 호환되고, 학습 부담이 낮아 팀원 대부분이 익숙하며, HTTP 캐싱 등 &lt;b&gt;웹 표준의 이점&lt;/b&gt;을 쉽게 활용할 수 있습니다​. 예를 들어 블로그나 뉴스 사이트처럼 데이터 변경 빈도가 낮고 캐싱을 극대화하고 싶은 경우, REST의 단순함과 캐시 제어 기능이 유용합니다​.&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;17803&quot; data-start=&quot;17434&quot;&gt;&lt;b&gt;GraphQL을 선택:&lt;/b&gt; 클라이언트별로 필요한 데이터가 천차만별이고, 한 번의 요청으로 여러 종류의 데이터를 &lt;b&gt;연관지어 가져와야 하는 경우&lt;/b&gt; GraphQL이 빛을 발합니다. 모바일 앱처럼 네트워크 환경이 제한적이거나, 다양한 화면에 맞춰 유연하게 데이터를 공급해야 한다면 GraphQL의 이점이 큽니다​. 또한 서비스 기능을 빠르게 확장하면서도 기존 API 버전을 늘리지 않고 &lt;b&gt;유연하게 진화&lt;/b&gt;시키고 싶을 때 GraphQL을 고려할 수 있습니다. 단, 팀에 GraphQL 경험이 충분하고, 캐싱/모니터링 등 운영 상의 복잡성을 감당할 수 있다는 전제가 필요합니다.&lt;/li&gt;
&lt;li data-end=&quot;18278&quot; data-start=&quot;17805&quot;&gt;&lt;b&gt;gRPC를 선택:&lt;/b&gt; &lt;b&gt;내부 시스템&lt;/b&gt; 간의 통신이나, 엔드투엔드 성능이 중요한 &lt;b&gt;마이크로서비스&lt;/b&gt; 아키텍처에서는 gRPC가 최상의 성능을 제공할 수 있습니다​. 예를 들어 실시간 처리가 핵심인 광고 경매 시스템이나, 다수의 마이크로서비스가 밀접하게 상호 작용하는 환경에서는 gRPC의 낮은 지연 시간과 스트리밍 기능이 큰 강점입니다​. 서버와 클라이언트를 모두 제어할 수 있고, 여러 언어로 구축된 서비스 간에 효율적이고 타입 안전한 통신이 필요하다면 gRPC가 적합합니다. 다만 &lt;b&gt;브라우저 클라이언트&lt;/b&gt;가 직접 호출해야 하는 공개 API라면 gRPC는 권장되지 않으며, 이 경우 REST나 GraphQL 위에 별도 게이트웨이를 두어 변환하여 제공하는 방안도 고려됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;18671&quot; data-start=&quot;18280&quot; data-ke-size=&quot;size16&quot;&gt;요약하면, &lt;b&gt;REST&lt;/b&gt;는 단순성과 호환성 면에서 기본값에 가까운 선택이고, &lt;b&gt;GraphQL&lt;/b&gt;은 유연한 데이터 질의와 클라이언트 최적화에 강점이 있으며, &lt;b&gt;gRPC&lt;/b&gt;는 성능과 효율을 극대화해야 하는 환경에서 돋보입니다. 각 기술은 상호 배타적이라기보다는 상황에 따라 병행 활용되기도 합니다. 예를 들어, 마이크로서비스 내부 통신에는 gRPC를 사용하면서, 외부 공개 API 계층에는 GraphQL이나 REST로 변환해 제공하는 아키텍처도 가능합니다. 궁극적으로는 애플리케이션의 &lt;b&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;style5&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;&lt;b&gt;Introduction to GraphQL: &lt;a href=&quot;https://graphql.org/learn/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://graphql.org/learn/&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PostMan, gRPC vs GrpahQL: &lt;a href=&quot;https://blog.postman.com/grpc-vs-graphql/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.postman.com/grpc-vs-graphql/&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Comparing REST, gRPC, and GraphQL: &lt;a href=&quot;https://systemdesignschool.io/blog/rest-grpc-graphql&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://systemdesignschool.io/blog/rest-grpc-graphql&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;kong, What is GraphQL: &lt;a href=&quot;https://konghq.com/blog/learning-center/graphql&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://konghq.com/blog/learning-center/graphql&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;gatsby, GraphiQL: &lt;a href=&quot;https://www.gatsbyjs.com/docs/how-to/querying-data/running-queries-with-graphiql/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.gatsbyjs.com/docs/how-to/querying-data/running-queries-with-graphiql/&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;rest arch style: &lt;a href=&quot;https://ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>탐구 생활/GraphQL</category>
      <category>grpc vs graphql</category>
      <category>rest vs grpc</category>
      <category>rest vs grpc vs graphql</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/84</guid>
      <comments>https://probehub.tistory.com/84#entry84comment</comments>
      <pubDate>Sat, 29 Mar 2025 13:52:43 +0900</pubDate>
    </item>
    <item>
      <title>Python asyncio에 대한 탐구</title>
      <link>https://probehub.tistory.com/82</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1_zTa718E_YuXoFozMgzgjAg.webp&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c84W2v/btsMT9aYidV/UhqIMOtdGNd85C1Abk4IiK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c84W2v/btsMT9aYidV/UhqIMOtdGNd85C1Abk4IiK/img.webp&quot; data-alt=&quot;https://medium.com/@yashwanthnandam/a-beginners-guide-to-python-s-asyncio-lets-get-async-ing-b2c9c81557cd&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c84W2v/btsMT9aYidV/UhqIMOtdGNd85C1Abk4IiK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc84W2v%2FbtsMT9aYidV%2FUhqIMOtdGNd85C1Abk4IiK%2Fimg.webp&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;360&quot; height=&quot;332&quot; data-filename=&quot;1_zTa718E_YuXoFozMgzgjAg.webp&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;664&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://medium.com/@yashwanthnandam/a-beginners-guide-to-python-s-asyncio-lets-get-async-ing-b2c9c81557cd&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python Web 개발을 FastAPI로&amp;nbsp;시작하다보니&amp;nbsp;비동기&amp;nbsp;상태에서&amp;nbsp;Python을&amp;nbsp;다루는게&amp;nbsp;너무나&amp;nbsp;당연했습니다.&amp;nbsp;그동안&amp;nbsp;막연히&amp;nbsp;Python을&amp;nbsp;비동기적으로&amp;nbsp;쓰려면&amp;nbsp;asnyc&amp;nbsp;를&amp;nbsp;써야지&amp;nbsp;생각만해왔습니다.&amp;nbsp;주말을&amp;nbsp;맞이해서&amp;nbsp;이번에&amp;nbsp;그&amp;nbsp;기반을&amp;nbsp;닦아보려고&amp;nbsp;합니다.&amp;nbsp;오늘은&amp;nbsp;그래서&amp;nbsp;asyncio&amp;nbsp;는&amp;nbsp;무엇인지&amp;nbsp;그리고&amp;nbsp;왜&amp;nbsp;필요한지,&amp;nbsp;node.js와의&amp;nbsp;차이점은&amp;nbsp;무엇인지&amp;nbsp;등을&amp;nbsp;알아보고&amp;nbsp;정리하려&amp;nbsp;합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;asyncio란 무엇인가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식&amp;nbsp;문서에서는&amp;nbsp;&lt;a href=&quot;https://docs.python.org/ko/3/library/asyncio.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;asyncio&lt;/a&gt;를&lt;b&gt; &amp;ldquo;async/await 문법을 이용하는 동시성(concurrent)처리를 위한 라이브러리&quot;&lt;/b&gt; 라고 소개하고 있습니다. 이러한 asyncio 라이브러리를 이용하여 IO bound 되어있는 작업을 효율적으로 처리할 수 있고, 이러한 이유로 고성능 웹 프라임워크, 네트워크 연결, 데이터베이스 연결등의 다른 라이브러리의 근간이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;asyncio는 Python에서 비동기 I/O를 지원하기 위한 라이브러리로, async/await 구문을 사용하여 동시성 있는 코드를 작성할 수 있게 해줍니다, 즉 Python 코드에서 동시성을 구현하기 위한 내장 도구입니다​.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;asyncio를 사용하면 한 번에 여러 작업을 동시에 수행하는 듯한 코드를 작성할 수 있지만, 실제로는 &lt;b&gt;단일 스레드 상에서 비동기적으로 실행&lt;/b&gt;됩니다​. 이러한 비동기 프로그래밍은 주로 I/O-bound 작업(입출력 대기 시간이 많은 작업)에 적합합니다. 예를 들어 파일 읽기/쓰기, 네트워크 요청 처리, DB 쿼리 등에서 asyncio를 사용하면 한 작업이 I/O를 기다리는 동안 다른 작업을 수행함으로써 효율을 높일 수 있습니다. 실제로 공식 문서에서도 asyncio는 I/O-bound와 고수준 네트워크 코드에 이상적으로 맞는다고 언급합니다​. 반면 CPU 연산 위주의 작업에는 asyncio가 적합하지 않을 수 있는데, 이는 뒤에서 자세히 다루겠습니다. 정리하자면 asyncio는 Python에 내장된 &lt;b&gt;&amp;ldquo;이벤트 루프 기반 비동기 I/O 라이브러리&amp;rdquo;&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;asyncio를 사용하면 개발자가 명시적으로 코루틴(coroutine)을 정의하고, 이벤트 루프를 통해 이들을 동시 실행하여 동시성을 달성할 수 있습니다. 이러한 디자인은 전통적인 스레드 기반 접근과 달리 &lt;b&gt;협력적 멀티태스킹(cooperative multitasking)&lt;/b&gt;으로 이루어지며, 하나의 스레드안에서 여러 작업이 교대로 실행되는 방식입니다​.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;asnycio 가 Python 공식 라이브러리로 등장하기 전에는 &lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://twisted.org/&quot;&gt;Twisted,&lt;/a&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://www.gevent.org/&quot;&gt;Gevent&lt;/a&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Python에서 asyncio가 등장한 배경&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python에서&amp;nbsp;asyncio가&amp;nbsp;등장한&amp;nbsp;배경에는&amp;nbsp;&lt;b&gt;언어&amp;nbsp;자체의&amp;nbsp;실행&amp;nbsp;모델과&amp;nbsp;I/O&amp;nbsp;병목&amp;nbsp;문제&lt;/b&gt;,&amp;nbsp;그리고&amp;nbsp;&lt;b&gt;기존&amp;nbsp;스레드&amp;nbsp;모델의&amp;nbsp;한계&lt;/b&gt;&amp;nbsp;등이&amp;nbsp;있습니다.&amp;nbsp;Python은&amp;nbsp;전통적으로&amp;nbsp;동시성&amp;nbsp;처리를&amp;nbsp;위한&amp;nbsp;기본&amp;nbsp;수단으로&amp;nbsp;멀티스레딩(threading)과&amp;nbsp;멀티프로세싱(multiprocessing)을&amp;nbsp;제공해&amp;nbsp;왔습니다.&amp;nbsp;그러나&amp;nbsp;두&amp;nbsp;접근법&amp;nbsp;모두&amp;nbsp;장단점이&amp;nbsp;있고,&amp;nbsp;Python만의&amp;nbsp;제약도&amp;nbsp;존재합니다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;GIL과 싱글 스레드 중심 구조&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python 인터프리터(CPython 구현)에는 &lt;b&gt;GIL(Global Interpreter Lock)&lt;/b&gt;이라는 락이 있습니다. GIL 때문에 한 시점에 하나의 스레드만 Python 바이트코드를 실행할 수 있게됩니다. 그 결과, 멀티스레드를 사용해도 CPU-bound 작업(순수 파이썬 연산이 많은 작업)의 경우 동시에 여러 스레드가 실행되지 못하고 하나씩 실행됩니다. 즉, CPU를 많이 쓰는 작업에서는 스레드를 늘려도 성능 향상이 거의 없고, 오히려 컨텍스트 스위칭 오버헤드로 약간 느려질 수도 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://news.hada.io/topic?id=16978&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Python 3.13 부터는 GIL 제약조건이 약화되었습니다.&lt;/a&gt; 하지만 이러한 변화가 바로 asyncio의 장점을 퇴색시키기는 어려울것으로 보입니다. 아래에도 나오지만 CPU 스케줄링의 원리에 따라 스레드가 I/O 작업을 처리할때 다른 스레드가 실행되기 때문에 GIL을 없애는 것이 Python 스레드의 I/O를 크게 개선시키는 것은 아닙니다. 다만 Python의 CPU Bound 작업 성능이 크게 개선될것으로 기대됩니다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;I/O-bound 작업의 병목&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 I/O-bound 작업(파일/네트워크 입출력처럼 대기 시간이 큰 작업)의 경우, GIL 하에서도 스레드가 유용하긴 합니다​. 예를 들어 하나의 스레드가 네트워크 응답을 기다리는 동안 GIL을 놓고 다른 스레드가 실행될 수 있기 때문입니다​. 하지만 전통적인 스레드 기반 I/O 처리에는 다른 문제가 있습니다. 만약 수천 개의 동시 연결을 처리해야 한다면, 수천 개의 스레드를 생성하는 것은 큰 메모리 소모와 문맥 전환 비용을 초래합니다. &lt;b&gt;Python 스레드는 운영체제 스레드로 구현되므로, 많은 양을 띄우면 OS 스케줄러 부하도 커지고 성능이 떨어집니다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;스레드 기반 모델의 복잡도&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드를 사용하면 공유 자원 접근 동기화(락 등) 문제, 데드락, 경쟁조건 같은 복잡도가 따라옵니다. 특히 Python에서는 데이터 공유에 대한 동기화를 프로그래머가 신경써야 하고, 디버깅도 어려울 수 있습니다. I/O처리를 위해 단순히 동시성을 얻고 싶었는데, 이러한 부수적인 어려움이 있는 것이죠.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;python eventloop.svg&quot; data-origin-width=&quot;677&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdwk9Z/btsMUvZdVTn/kS5CllH9LqMLQnKjuuUfxK/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdwk9Z/btsMUvZdVTn/kS5CllH9LqMLQnKjuuUfxK/tfile.svg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdwk9Z/btsMUvZdVTn/kS5CllH9LqMLQnKjuuUfxK/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbdwk9Z%2FbtsMUvZdVTn%2FkS5CllH9LqMLQnKjuuUfxK%2Ftfile.svg&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;550&quot; height=&quot;359&quot; data-filename=&quot;python eventloop.svg&quot; data-origin-width=&quot;677&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한&amp;nbsp;이유들로,&amp;nbsp;Python&amp;nbsp;커뮤니티에서는&amp;nbsp;보다&amp;nbsp;가벼운&amp;nbsp;동시성&amp;nbsp;모델을&amp;nbsp;필요로&amp;nbsp;했습니다.&amp;nbsp;&lt;b&gt;이벤트&amp;nbsp;루프&amp;nbsp;기반의&amp;nbsp;비동기&amp;nbsp;처리&lt;/b&gt;는 이를 해결할 대안으로 떠올랐습니다. 이미 Node.js나 JavaScript 런타임이 이벤트 루프 기반 비동기 처리를 활용하여 높은 동시성을 효율적으로 다루고 있었고, Python 생태계 내에서도 Twisted, Tornado 같은 이벤트 루프 라이브러리가 존재하고 있었습니다. 다만 Python 표준 라이브러리에는 통합된 솔루션이 없었죠. asyncio는 이러한 요구에 따라 &lt;b&gt;Python 3.4 (2014년경)&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;Guido van Rossum이 주도한 Tulip프로젝트(&lt;a href=&quot;https://peps.python.org/pep-3156/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PEP 3156&lt;/a&gt;)로부터 시작하여 표준화되었으며, 이후 Python 3.5에서 async/await 구문 지원(&lt;a href=&quot;https://peps.python.org/pep-0492/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PEP 492&lt;/a&gt;)이 추가되면서 비로소 개발자가 쓰기 편한 형태의 비동기 프로그래밍이 가능해졌습니다​. Python 3.7에서 asyncio.run() 함수가 도입되어 이벤트 루프 실행이 더욱 간편해지고, ayncio는 사실상 Python에서 비동기 프로그래밍의 표준이 되었습니다​.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Python의 thread와의 차이점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 말한것처럼 Tulip프로젝트 이전에는 Python에서 동시성은 &lt;b&gt;스레드(thread)&lt;/b&gt;로 구현되곤 했습니다. asyncio의 &lt;b&gt;코루틴 기반 동시성&lt;/b&gt;은 겉보기에는 스레드와 비슷하게 여러 작업을 동시에 처리하지만, &lt;b&gt;내부 동작과 성능 특성에서 차이&lt;/b&gt;가 있습니다. 주요 차이점을 하나씩 살펴보겠습니다.&lt;/p&gt;
&lt;h4 data-end=&quot;4513&quot; data-start=&quot;4492&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;동시성 구현 방식&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;5502&quot; data-start=&quot;4515&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4901&quot; data-start=&quot;4515&quot;&gt;&lt;b&gt;스레드(Thread)&lt;/b&gt;: 하나의 프로세스 내에서 여러 OS 스레드를 실행함으로써 동시성을 얻습니다. 운영체제가 CPU 시간을 여러 스레드에 분배해주므로, 스레드들은&amp;nbsp;&lt;b&gt;선점형 멀티태스킹(preemptive multitasking)&lt;/b&gt;을 합니다. 각 스레드는 독립적인 호출 스택을 갖고 병렬로 실행되지만, &lt;b&gt;Python의 경우 GIL로 인해 동일 시점에 하나의 스레드만 실행된다는 제약&lt;/b&gt;이 있습니다​. 그래도 I/O 대기 중에는 자연스럽게 다른 스레드로 전환되므로 I/O-bound 작업에서 병렬성이 어느 정도 실현됩니다​&lt;/li&gt;
&lt;li data-end=&quot;5502&quot; data-start=&quot;4903&quot;&gt;&lt;b&gt;asyncio(코루틴)&lt;/b&gt;: 하나의 스레드 안에서 &lt;b&gt;이벤트 루프(event loop)&lt;/b&gt;가 여러 코루틴(coroutine)을 &lt;b&gt;협력적으로 스케줄링&lt;/b&gt;합니다​. 각 코루틴은 await 키워드를 만나면 &lt;b&gt;자신이 제어권을 양보&lt;/b&gt;하여 이벤트 루프가 다른 코루틴을 실행하도록 합니다. 따라서 asyncio의 동시성은 &lt;b&gt;협력적 멀티태스킹(cooperative multitasking)&lt;/b&gt;입니다. OS가 강제로 태스크를 바꾸는 것이 아니라, 코루틴들이 스스로 양보하는 것이죠​. 이러한 코루틴 전환은 &lt;b&gt;결정적인 시점&lt;/b&gt;(주로 I/O 작업 전에 await할 때)에 일어나므로 오버헤드가 적고, 여러 코루틴이 &lt;b&gt;하나의 호출 스택&lt;/b&gt;을 공유합니다. 결과적으로, 수천 개의 작업도 &lt;b&gt;단일 스레드&lt;/b&gt;에서 효율적으로 관리할 수 있게 됩니다​&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;5526&quot; data-start=&quot;5504&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;성능과 리소스 사용&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;4_01_ThreadDiagram.jpg&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d2Qo7u/btsMUoy75wU/8MjTc277bQa2pgeVR7iDK0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d2Qo7u/btsMUoy75wU/8MjTc277bQa2pgeVR7iDK0/img.jpg&quot; data-alt=&quot;https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/4_Threads.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d2Qo7u/btsMUoy75wU/8MjTc277bQa2pgeVR7iDK0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd2Qo7u%2FbtsMUoy75wU%2F8MjTc277bQa2pgeVR7iDK0%2Fimg.jpg&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;704&quot; height=&quot;406&quot; data-filename=&quot;4_01_ThreadDiagram.jpg&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;406&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/4_Threads.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;7300&quot; data-start=&quot;5528&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;6361&quot; data-start=&quot;5528&quot;&gt;&lt;b&gt;스레드의 성능 및 오버헤드:&lt;/b&gt; Python에서 스레드는 I/O-bound 작업에 한해 유용하며, 이러한 경우 여러 스레드가 있으면 (GIL로 인해 한 번에 한 스레드씩 실행되지만) 한 스레드가 파일/네트워크 대기 시 다른 스레드가 실행되어 &lt;b&gt;대기 시간을 숨길 수 있습니다&lt;/b&gt;​. 하지만 스레드를 많이 생성하면 각 스레드는&amp;nbsp;&lt;b&gt;스택 메모리&lt;/b&gt;를 차지하고, 컨텍스트 스위치에 비용이 듭니다.&amp;nbsp;&lt;/li&gt;
&lt;li data-end=&quot;7300&quot; data-start=&quot;6363&quot;&gt;&lt;b&gt;asyncio의 성능 특성:&lt;/b&gt; asyncio는 &lt;b&gt;수많은 작업을 동시 처리&lt;/b&gt;할 때도 &lt;b&gt;메모리 오버헤드가 낮고&lt;/b&gt;(OS 스레드를 생성하지 않으므로), 컨텍스트 스위치가 필요할 때만 발생하여 매우 효율적입니다​​, 즉&amp;nbsp;&lt;b&gt;스레드보다 적은 자원으로 높은 동시성&lt;/b&gt;을 달성합니다​&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;GIL때문에 Python MultiThread 환경에서 CPU Bound 작업의 효율성이 낮아지는것은 언급했습니다. 하지만 그렇기 때문에 asyncio 를 이용한 CPU Bound 작업이 효율적이라고 생각하면 안됩니다. &lt;b&gt;하나의 코루틴이 CPU를 오래 점유하면(예: 복잡한 계산을 계속하면) await로 양보하기 전까지는 다른 코루틴이 실행되지 못하므로 전체 이벤트 루프가 정지된 것처럼 됩니다​.&lt;/b&gt;&amp;nbsp; 따라서 CPU 집약적인 일은 asyncio 안에서 하지 않고, 별도의 프로세스나 스레드로 offload 하거나(예: run_in_executor), 계산을 적절히 await로 나눠주어야 합니다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;코드 작성 및 구조&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;쓰레드 코드:&lt;/b&gt; 쓰레드를 사용하려면 각 작업을 함수로 만들고 threading.Thread(target=함수) 방식으로 실행합니다. 결과를 모으려면 join()으로 모든 쓰레드가 끝날 때까지 기다려야 하고, 작업 사이에 공유 데이터가 있다면 Lock 같은 동기화 도구도 직접 사용해야 합니다. 예를 들어 여러 URL을 다운로드하는 코드를 스레드로 짜면 다음과 같습니다:&lt;/p&gt;
&lt;pre id=&quot;code_1742737154442&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import threading
import requests

urls = [ ... 리스트 ...]
results = []

def download(url):
    resp = requests.get(url)        # 네트워크 I/O (블로킹)
    results.append(resp.text)       # (공유 리스트 접근 - 동기화 필요할 수 있음)

threads = []
for url in urls:
    t = threading.Thread(target=download, args=(url,))
    t.start()
    threads.append(t)
for t in threads:
    t.join()  # 모든 스레드 작업 완료 대기
print(len(results), &quot;개의 응답 수신 완료&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 requests.get 호출이 &lt;b&gt;블로킹 I/O&lt;/b&gt;이지만, 여러 스레드를 통해 동시 요청이 가능합니다. 다만 results.append는 공유 리스트 접근이라 스레드 간 경합이 발생할 수 있어서, 엄격히 따지면 Lock이 필요할 수도 있습니다. 이렇듯 스레드 코드는 동시성 처리를 비교적 &lt;b&gt;자동으로 OS에 맡기지만&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;asyncio 코드:&lt;/b&gt; asyncio를 사용하면 우선 async def로 코루틴 함수를 정의하고, 그 안에서 await으로 비동기 작업을 호출합니다. 그리고 asyncio.run() 또는 이벤트 루프를 통해 이 코루틴들을 실행합니다. 예를 들어 위와 동일한 작업을 asyncio와 aiohttp로 구현하면 다음처럼 작성할 수 있습니다 (requests는 동기 라이브러리라 사용할 수 없고, aiohttp 같은 비동기 HTTP 클라이언트를 사용해야 합니다):&lt;/p&gt;
&lt;pre id=&quot;code_1742737199471&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import asyncio
import aiohttp

urls = [ ... 리스트 ...]
results = []

async def download(url, session):
    async with session.get(url) as resp:   # 비동기 HTTP GET
        text = await resp.text()          # 결과 본문 읽기 (비동기 I/O)
        results.append(text)

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [asyncio.create_task(download(url, session)) for url in urls]
        await asyncio.gather(*tasks)      # 모든 다운로드 코루틴 병행 실행
    print(f&quot;{len(results)}개의 응답 수신 완료&quot;)

asyncio.run(main())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 asyncio 코드는 async with 문으로 HTTP 세션을 관리하고, asyncio.gather로 다수의 다운로드 작업을 &lt;b&gt;동시에 실행&lt;/b&gt;합니다. await resp.text() 하는 동안 다른 task들이 실행되므로, 진짜 동시에 네트워크를 기다리고 처리하는 효과를 얻습니다. 또한 코드에서 공유 리스트 results에 append 하는 부분은 한 스레드 내에서 실행되므로 &lt;b&gt;데이터 경합 없이&lt;/b&gt; 안전합니다. 전반적으로, asyncio 코드는 &lt;b&gt;동기 코드처럼 순차적 구조&lt;/b&gt;를 유지하면서도, await 키워드 덕분에 자연스럽게 동시성을 부여합니다.&lt;/p&gt;
&lt;h4 data-end=&quot;9418&quot; data-start=&quot;9392&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;상황별 효율성과 선택 기준&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;10536&quot; data-start=&quot;9420&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;9753&quot; data-start=&quot;9420&quot;&gt;&lt;b&gt;I/O-bound 작업이 많은 서버나 애플리케이션&lt;/b&gt;에서는 asyncio가 빛을 발합니다. 예를 들어 웹 크롤러, 대용량 API 호출 처리, 채팅 서버 등 &lt;b&gt;동시에 수천 개의 네트워크 I/O&lt;/b&gt;를 해야 하는 경우, 쓰레드로 구현하면 수천 스레드를 만들기 어려운 반면 asyncio는 한 이벤트 루프에서 수천 코루틴을 관리해낼 수 있습니다. 이러한 경우 asyncio가 &lt;b&gt;더 가볍고 확장성 있게(scale) 동작&lt;/b&gt;합니다​​&lt;/li&gt;
&lt;li data-end=&quot;10223&quot; data-start=&quot;9755&quot;&gt;&lt;b&gt;CPU-bound 작업&lt;/b&gt;(예: 이미지 처리, 머신러닝 연산 등 계산 위주)은 asyncio로 처리하면 오히려 전체가 느려집니다. 이때는 오히려 멀티스레드보다 &lt;b&gt;멀티프로세싱&lt;/b&gt;(또는 C 확장 모듈 활용)이 필요합니다. CPU 작업은 여러 코어에서 병렬로 돌려야 빨라지는데, Python 쓰레드는 GIL로 막혀 있으니, 차라리 멀티프로세싱으로 별도 프로세스를 돌리는것이 권장됩니다. 혹은 Python 3.11 이후 도입된 &lt;a href=&quot;https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.ThreadPoolExecutor&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;concurrent.futures.ThreadPoolExecutor&lt;/a&gt;/&lt;a href=&quot;https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ProcessPoolExecutor&lt;/a&gt;를 적절히 쓰는 것이 나을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;I/O 효율적이라고 알려진 node.js 와의 비교&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;nodejs_eventloop.png&quot; data-origin-width=&quot;941&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4Ozok/btsMUYUpqhk/R3VkHywoaZRV7ZnncsOZw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4Ozok/btsMUYUpqhk/R3VkHywoaZRV7ZnncsOZw1/img.png&quot; data-alt=&quot;https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4Ozok/btsMUYUpqhk/R3VkHywoaZRV7ZnncsOZw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4Ozok%2FbtsMUYUpqhk%2FR3VkHywoaZRV7ZnncsOZw1%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;704&quot; height=&quot;364&quot; data-filename=&quot;nodejs_eventloop.png&quot; data-origin-width=&quot;941&quot; data-origin-height=&quot;486&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python의 asyncio를 이야기할 때 자주 비교되는 대상이 &lt;b&gt;Node.js&lt;/b&gt;입니다. Python은 Node.js의 &lt;b&gt;이벤트 루프&lt;/b&gt;에서 영향을 받아 asyncio를 도입했다고 해도 과언이 아닙니다. 두 환경의 공통점과 차이를 살펴보겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이벤트 루프와 싱글 스레드 아키텍처&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;공통점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js와 Python asyncio 모두 &lt;b&gt;이벤트 루프(event loop)&lt;/b&gt;가 핵심입니다. 이벤트 루프는 &lt;b&gt;비동기 작업&lt;/b&gt;(네트워크 I/O, 타이머 등)의 완료 이벤트를 감지하고 등록된 콜백이나 코루틴을 실행하는 역할을 합니다. 또한 &lt;b&gt;단일 스레드&lt;/b&gt; 기반이라는 점도 같습니다. Node.js의 JavaScript 코드 실행은 기본적으로 하나의 쓰레드에서 일어나고, asyncio도 하나의 메인 쓰레드에서 코루틴들을 처리합니다​.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;하나의 작업이 CPU를 너무 오래 점유하면 다른 작업이 지연된다&lt;/b&gt;는 점도 공통됩니다.&lt;/p&gt;
&lt;p data-end=&quot;12354&quot; data-start=&quot;11595&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;12354&quot; data-start=&quot;11595&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;차이점&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;12354&quot; data-start=&quot;11595&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이벤트 루프의 시작과 관리 방식&lt;/b&gt;이 다릅니다. Node.js에서는 이벤트 루프가 &lt;b&gt;런타임 자체에 내장&lt;/b&gt;되어 있습니다. 즉, Node.js로 실행되는 스크립트는 기본적으로 이벤트 루프 위에서 동작하며, 모든 I/O API가 비동기(논블로킹)로 설계되어 있습니다. 반면 Python에서는 &lt;b&gt;이벤트 루프가 기본 활성화되어 있지 않고&lt;/b&gt;, asyncio를 사용할 때 명시적으로 asyncio.run() 등을 호출하여 &lt;b&gt;이벤트 루프를 돌립니다&lt;/b&gt;​.&lt;/p&gt;
&lt;p data-end=&quot;12354&quot; data-start=&quot;11595&quot; data-ke-size=&quot;size16&quot;&gt;Python은 필요할 때만 이벤트 루프를 &amp;ldquo;켜는&amp;rdquo; 온/오프 개념이고, Node.js는 항상 이벤트 루프가 동작하고 있는 형태라고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;12693&quot; data-start=&quot;12356&quot; data-ke-size=&quot;size16&quot;&gt;또 다른 차이는 &lt;b&gt;언어와 런타임의 역사&lt;/b&gt;에서 비롯됩니다. Node.js (그리고 브라우저의 JS)는 원래부터 비동기 작업을 &lt;b&gt;콜백&lt;/b&gt;으로 처리해왔고, 이후 &lt;b&gt;Promise&lt;/b&gt;와 async/await을 도입하여 개선했습니다. Python은 전통적으로 동기/블로킹 함수들이 많고, asyncio 도입 후에도 기존 라이브러리 상당수가 동기 방식으로 남아 있습니다. 그래서 Python 개발자는 &lt;b&gt;동기 코드와 비동기 코드의 호환&lt;/b&gt;을 고민해야 하는 경우가 많습니다. (예: Django ORM은 동기이기 때문에 asyncio 웹 서버에서 사용할 때 별도의 쓰레드풀로 돌리는 등 추가 작업이 필요함)&lt;/p&gt;
&lt;h4 data-end=&quot;14197&quot; data-start=&quot;13593&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;성능 및 활용 차이&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;14197&quot; data-start=&quot;13593&quot; data-ke-size=&quot;size16&quot;&gt;Node.js와 Python asyncio 중 어떤 것이 성능이 더 좋냐는 단순 비교는 어렵습니다. Node.js는 V8 엔진으로 구동되어 JavaScript가 &lt;b&gt;JIT 컴파일&lt;/b&gt;된다는 이점이 있고, Python은 인터프리터라 단순 반복문 같은 CPU 작업은 JS보다 느릴 수 있습니다. 하지만 &lt;b&gt;I/O 처리 성능&lt;/b&gt;은 양쪽 다 훌륭하며, 큰 차이가 없습니다. 둘 다 커널 비동기 I/O 기술(epoll 등)을 활용하고 최적화되어 있어 &lt;b&gt;수만 개 소켓 연결 처리도 가능한 수준&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-end=&quot;14197&quot; data-start=&quot;13593&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;차이가 있다면&lt;/b&gt;, Python asyncio는 &lt;b&gt;선택적&lt;/b&gt;이므로, 상황에 따라 동기 코드와 혼용이 가능하고 필요시 멀티스레드/멀티프로세스로 전환이 가능합니다. Node.js는 기본적으로 싱글 스레드이므로, CPU-bound 처리에 한계를 느낄 땐 &lt;b&gt;Cluster/fork&lt;/b&gt;나 &lt;b&gt;Worker Threads&lt;/b&gt; 등을 사용해야 합니다.&lt;/p&gt;
&lt;p data-end=&quot;14197&quot; data-start=&quot;13593&quot; data-ke-size=&quot;size16&quot;&gt;결국 Node vs Python asyncio는 &lt;b&gt;동시성 모델의 철학은 비슷&lt;/b&gt;하나, Python이 &lt;b&gt;동기와 비동기 모델을 모두 포괄&lt;/b&gt;하는 유연함이 있고 Node는 &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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;asyncio 를 이용하는 방법 (기본 사용법 정리)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 &lt;b&gt;코루틴 정의&lt;/b&gt;부터 &lt;b&gt;asyncio.run()&lt;/b&gt;으로 실행, 여러 코루틴을 한꺼번에 실행하는 &lt;b&gt;asyncio.gather()&lt;/b&gt;, 그리고 비동기 &lt;b&gt;반복문(async for)&lt;/b&gt;, &lt;b&gt;컨텍스트 매니저(async with)&lt;/b&gt;, 마지막으로 &lt;b&gt;코루틴에서의 yield 사용법&lt;/b&gt;까지 하나씩 예제 코드와 함께 정리해보겠습니다.&lt;/p&gt;
&lt;h3 data-end=&quot;21078&quot; data-start=&quot;21043&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코루틴 정의와 asyncio.run()&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;21239&quot; data-start=&quot;21080&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코루틴(coroutine)&lt;/b&gt;은 Python에서 async/await 기능의 핵심 단위입니다. 코루틴은 async def로 정의된 함수로, 호출 시 즉시 실행되는 것이 아니라 &lt;b&gt;코루틴 객체&lt;/b&gt;를 반환합니다. 이 코루틴 객체는 &lt;b&gt;&amp;ldquo;미래에 실행될 작업&amp;rdquo;&lt;/b&gt;으로 생각하면 됩니다. 아래에 정의된 hello 라는 코루틴을 실제로 &lt;b&gt;실행&lt;/b&gt;하려면 이벤트 루프가 필요합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1742738689313&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import asyncio

async def hello():
    print(&quot;Hello ...&quot;)
    await asyncio.sleep(1)       # 1초 비동기 대기 (다른 작업에 양보)
    print(&quot;... World!&quot;)

# 코루틴 함수를 호출해도 바로 실행되지 않음
coro = hello()  
print(coro)  
# &amp;lt;coroutine object hello at 0x...&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;asyncio.run()&lt;/b&gt;은 &lt;b&gt;이벤트 루프를 시작&lt;/b&gt;하고 주어진 코루틴을 최종 완료까지 실행해주는 편의 함수입니다​&lt;span data-state=&quot;closed&quot;&gt;&lt;/span&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;/span&gt;. 보통 asyncio.run(main()) 형태로, 하나의 메인 코루틴을 실행하는 데 사용합니다. asyncio.run이 내부에서 이벤트 루프를 새로 만들어 main() 코루틴을 실행하고, 끝나면 루프를 정리해줍니다. Python 3.7+에서 권장되는 진입점 실행 방식이죠.&lt;/p&gt;
&lt;pre id=&quot;code_1742738780275&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async def main():
    print(&quot;작업 시작&quot;)
    await hello()            # 위에서 정의한 코루틴을 await로 실행
    print(&quot;모든 작업 완료&quot;)

asyncio.run(main())         # 이벤트 루프를 열고 main 코루틴 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 실행하면 main() 코루틴 내부에서 await hello()를 만나 hello() 코루틴으로 들어갔다가, hello가 1초 쉬는 동안 다시 main으로 복귀&amp;hellip; 이런 식으로 동작하며, 결과적으로 &quot;Hello ...&quot; (즉시 출력) &amp;rarr; 1초 대기 &amp;rarr; &quot;... World!&quot; &amp;rarr; &quot;모든 작업 완료&quot; 순으로 출력됩니다.&lt;/p&gt;
&lt;p data-end=&quot;22641&quot; data-start=&quot;22271&quot; data-ke-size=&quot;size16&quot;&gt;핵심은, &lt;b&gt;await 키워드&lt;/b&gt;입니다. await는 &lt;b&gt;코루틴의 실행을 일시 중지하고(await 키워드 뒤의 작업 완료를 기다린 뒤) 다시 재개&lt;/b&gt;시킵니다​. await를 호출하면 현재 코루틴은 이벤트 루프에게 제어권을 넘기고, 그 사이에 다른 코루틴이 실행될 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;22641&quot; data-start=&quot;22271&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;22648&quot; data-start=&quot;22643&quot; data-ke-size=&quot;size16&quot;&gt;흐름을 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;22805&quot; data-start=&quot;22650&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;22685&quot; data-start=&quot;22650&quot;&gt;async def func(): ... : 코루틴 정의.&lt;/li&gt;
&lt;li data-end=&quot;22754&quot; data-start=&quot;22686&quot;&gt;await something : 코루틴 내에서 다른 코루틴/작업이 끝날 때까지 &lt;b&gt;non-blocking 대기&lt;/b&gt;.&lt;/li&gt;
&lt;li data-end=&quot;22805&quot; data-start=&quot;22755&quot;&gt;asyncio.run(coro) : 코루틴 실행을 위해 이벤트 루프 생성 및 돌입.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;동시에 여러 코루틴 실행: asyncio.gather()와 create_task()&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;await를 사용하면 순차적으로 코루틴을 실행하게 됩니다. 만약 여러 코루틴을 &lt;b&gt;동시에 실행&lt;/b&gt;하고 싶다면 어떻게 해야 할까요? 대표적으로 &lt;b&gt;asyncio.gather()&lt;/b&gt;와 &lt;b&gt;asyncio.create_task()&lt;/b&gt;를 활용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;23449&quot; data-start=&quot;23175&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;23331&quot; data-start=&quot;23175&quot;&gt;&lt;b&gt;asyncio.create_task(coro)&lt;/b&gt;: 주어진 코루틴을 &lt;b&gt;백그라운드 Task&lt;/b&gt;로 스케줄링합니다. 즉, 즉시 해당 코루틴 실행을 시작하고, Task 객체를 반환합니다. Task는 미래에 결과를 담게 되는 &lt;b&gt;Future&lt;/b&gt;의 한 종류라고 볼 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;23449&quot; data-start=&quot;23333&quot;&gt;&lt;b&gt;asyncio.gather(*coros)&lt;/b&gt;: 여러 코루틴들을 동시에 실행하고 &lt;b&gt;모두 완료될 때까지 기다린 뒤 결과를 한꺼번에 반환&lt;/b&gt;합니다. 내부적으로 각 코루틴을 Task로 만들고 추적합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3개의 작업을 동시에 실행하여 실행 시간을 단축해보겠습니다. 각 작업은 1초씩 걸린다고 가정합니다:&lt;/p&gt;
&lt;pre id=&quot;code_1742739122266&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import asyncio
import time

async def work(id):
    print(f&quot;작업 {id} 시작&quot;)
    await asyncio.sleep(1)          # 1초짜리 비동기 작업 (예: 네트워크 요청)
    print(f&quot;작업 {id} 완료&quot;)
    return id

async def main():
    start = time.time()
    # 방법 1: asyncio.gather로 동시에 실행
    results = await asyncio.gather(work(1), work(2), work(3))
    end = time.time()
    print(&quot;동시 실행 결과:&quot;, results)
    print(f&quot;총 실행 시간: {end - start:.2f}초&quot;)

asyncio.run(main())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 결과가 출력됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1742739674453&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;작업 1 시작
작업 2 시작
작업 3 시작
작업 1 완료
작업 2 완료
작업 3 완료
동시 실행 결과: [1, 2, 3]
총 실행 시간: 1.00초 (내외)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;asyncio.gather를 사용하지 않고, create_task를 사용해서 다음과 같이 할 수 있습니다:&lt;/p&gt;
&lt;pre id=&quot;code_1742739708062&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async def main():
    start = time.time()
    # 방법 2: create_task를 사용하여 태스크 생성 후 개별 await
    task1 = asyncio.create_task(work(1))
    task2 = asyncio.create_task(work(2))
    task3 = asyncio.create_task(work(3))
    # 이 시점에서 작업들이 백그라운드로 진행 중
    result1 = await task1
    result2 = await task2
    result3 = await task3
    end = time.time()
    print(&quot;동시 실행 결과:&quot;, [result1, result2, result3])
    print(f&quot;총 실행 시간: {end - start:.2f}초&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;create_task로 태스크를 생성하면 즉시 작업이 시작&lt;/b&gt;되므로, 바로 await를 하지 않고 있다면 다른 코드도 동시에 실행될 수 있습니다. 위 예에서 task1 생성 후 task2를 생성하는 동안도 task1의 작업은 진행 중입니다. 결국 await task1 등을 통해 결과를 모을 때까지 3개 작업이 병행되는 것이죠. asyncio.gather는 이런 패턴을 한 줄로 축약한 것이라 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;25061&quot; data-start=&quot;24857&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;에러 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;25061&quot; data-start=&quot;24857&quot; data-ke-size=&quot;size16&quot;&gt;asyncio.gather는 기본적으로 내부 태스크 중 하나에서 예외가 발생하면 즉시 예외를 전파하고 다른 태스크도 취소합니다. 반면 return_exceptions=True 옵션을 주면, 예외도 결과로 모아서 반환해줍니다. 일반적으로는 예외 처리를 개별 코루틴 내에서 하거나, gather 바깥에서 try/except로 묶어 처리합니다.&lt;/p&gt;
&lt;p data-end=&quot;25061&quot; data-start=&quot;24857&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;25204&quot; data-start=&quot;25063&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;asyncio.wait&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;25204&quot; data-start=&quot;25063&quot; data-ke-size=&quot;size16&quot;&gt;gather와 비슷한 asyncio.wait도 있는데, 이건 태스크 집합을 주면 완료된 것과 안 된 것을 구분해서 돌려주는 저수준 함수입니다. 대부분의 경우 gather가 더 편리하고, Python 공식문서에서도 저수준 API를 직접 다루는 것을 권장하지 않기 때문에 깊이 다루지 않겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;비동기 이터레이션: async for (비동기 제너레이터)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;async for&lt;/b&gt;는 &lt;b&gt;비동기 이터레이터/제너레이터&lt;/b&gt;와 함께 사용되는 구문입니다. 일반적인 for문은 이터레이터의 __iter__/__next__를 호출해 동작하는데, &lt;b&gt;비동기 이터레이터&lt;/b&gt;는 __anext__라는 &lt;b&gt;비동기(next) 메서드&lt;/b&gt;를 가집니다. 그리고 async for 루프는 이 __anext__가 반환하는 &lt;b&gt;코루틴을 await&lt;/b&gt;하면서 루프를 돕니다. 말이 어렵지만, 실제로 언제 쓰이냐 하면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;25680&quot; data-start=&quot;25503&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;25633&quot; data-start=&quot;25503&quot;&gt;파일이나 소켓에서 비동기적으로 한 줄씩 읽어들일 때 (aiofiles나 asyncio.StreamReader 등은 async iterator를 지원하여 async for line in reader 형태로 쓸 수 있음).&lt;/li&gt;
&lt;li data-end=&quot;25680&quot; data-start=&quot;25634&quot;&gt;대기 시간이 있는 데이터를 스트리밍 받을 때 (예: 비동기 발생 이벤트 소비)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;25916&quot; data-start=&quot;25682&quot; data-ke-size=&quot;size16&quot;&gt;아울러 &lt;b&gt;비동기 제너레이터(async generator)&lt;/b&gt;도 있습니다. 이는 async def 함수 안에서 yield 키워드를 써서 &lt;b&gt;비동기적으로 값을 순차 생성&lt;/b&gt;하는 함수입니다. &lt;a href=&quot;https://peps.python.org/pep-0525/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PEP 525&lt;/a&gt;에 도입된 기능으로, 이러한 함수는 호출 시 &lt;b&gt;AsyncGenerator&lt;/b&gt; 객체를 반환하며, 이는 앞서 말한 비동기 이터레이터 프로토콜을 따릅니다. 비동기 제너레이터는 async for로 순회해야 합니다.&lt;/p&gt;
&lt;p data-end=&quot;25916&quot; data-start=&quot;25682&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;25916&quot; data-start=&quot;25682&quot; data-ke-size=&quot;size16&quot;&gt;간단한 비동기 제너레이터와 async for 사용법을 보겠습니다&lt;/p&gt;
&lt;pre id=&quot;code_1742740107202&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 비동기 제너레이터 함수 정의
async def async_count(stop):
    for i in range(stop):
        await asyncio.sleep(1)   # 매 반복마다 1초 비동기 대기
        yield i                 # 값을 산출 (yield) - async generator

async def main():
    # async for를 사용하여 비동기 제너레이터로부터 값 받기
    async for num in async_count(3):
        print(f&quot;{num} 출력 (시간: {time.time():.0f})&quot;)

asyncio.run(main())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 숫자가 1초 간격으로 출력되는 것을 볼 수 있습니다. async_count 함수는 매 loop마다 await asyncio.sleep(1)으로 1초 쉬고 yield i로 값을 내보냅니다. 이처럼 &lt;b&gt;async def 내에서 yield를 쓰면&lt;/b&gt; 일반 함수의 제너레이터와 달리 &lt;b&gt;Async Generator&lt;/b&gt;가 됩니다. 그리고 async for num in async_count(3) 구문이 내부적으로 다음과 같이 동작합니다. 개발자는 async for 구문만 써주면 이런 복잡한 처리가 자동으로 이루어집니다.&lt;/p&gt;
&lt;pre id=&quot;code_1742740200869&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;iterator = async_count(3).__aiter__()
while True:
    try:
        num = await iterator.__anext__()
    except StopAsyncIteration:
        break
    # loop body
    print(num)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;비동기 컨텍스트 매니저: async with&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async with 구문은 &lt;b&gt;비동기 컨텍스트 매니저&lt;/b&gt;를 사용할 때 쓰입니다. 일반적인 with 문은 객체의 __enter__와 __exit__를 호출해서 자원 관리를 하는데, 비동기 상황에서는 __aenter__와 __aexit__라는 비동기 메서드를 사용합니다. 예를 들어, &lt;b&gt;asyncio.Lock&lt;/b&gt;은 비동기 락 객체인데, 이것을 async with로 획득/해제할 수 있습니다​&lt;span data-state=&quot;closed&quot;&gt;&lt;/span&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;/span&gt;. 또 aiohttp의 ClientSession이나 데이터베이스의 AsyncSession 등도 async with로 열고 닫는 처리를 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;&lt;b&gt;Python document, asyncio: &lt;a href=&quot;https://docs.python.org/ko/3/library/asyncio.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.python.org/ko/3/library/asyncio.html&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Python document, corruines and task: &lt;a href=&quot;https://docs.python.org/3/library/asyncio-task.html#coroutine&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.python.org/3/library/asyncio-task.html#coroutine&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Python document, eventloop: &lt;a href=&quot;https://docs.python.org/3/library/asyncio-eventloop.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.python.org/3/library/asyncio-eventloop.html&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Python Asyncio vs Threading: &lt;a href=&quot;https://www.geeksforgeeks.org/asyncio-vs-threading-in-python/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.geeksforgeeks.org/asyncio-vs-threading-in-python/&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Async IO in Python: A Complete Walkthrough: &lt;a href=&quot;https://www.geeksforgeeks.org/asyncio-vs-threading-in-python/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.geeksforgeeks.org/asyncio-vs-threading-in-python/&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>탐구 생활/Python</category>
      <category>python asyncio</category>
      <category>python asyncio tip</category>
      <category>python asyncio 왜?</category>
      <category>python eventloop</category>
      <category>python vs node</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/82</guid>
      <comments>https://probehub.tistory.com/82#entry82comment</comments>
      <pubDate>Sun, 23 Mar 2025 18:57:05 +0900</pubDate>
    </item>
    <item>
      <title>Python의 Type System</title>
      <link>https://probehub.tistory.com/80</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;DALL&amp;amp;middot;E 2025-03-20 15.54.56 - A conceptual illustration of Python's TypeHint feature. The image should include a Python script with function annotations using TypeHint, such as def.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sH5v0/btsMP9pz5rm/XiDX3r3zyhpF71OAWPWXf0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sH5v0/btsMP9pz5rm/XiDX3r3zyhpF71OAWPWXf0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sH5v0/btsMP9pz5rm/XiDX3r3zyhpF71OAWPWXf0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsH5v0%2FbtsMP9pz5rm%2FXiDX3r3zyhpF71OAWPWXf0%2Fimg.webp&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;360&quot; height=&quot;360&quot; data-filename=&quot;DALL&amp;middot;E 2025-03-20 15.54.56 - A conceptual illustration of Python's TypeHint feature. The image should include a Python script with function annotations using TypeHint, such as def.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&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;&amp;nbsp; &lt;a href=&quot;https://probehub.tistory.com/78&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;gRPC에 대해서 알아보던중&lt;/a&gt;&amp;nbsp;&lt;/span&gt;pyi 파일을 처음 보았고, 이 파일이 Python Type System 중 하나인 stub file 이라는 것을 알게되었습니다. stub file 이 무엇인지 궁금해졌고, 이 궁금증은 Python의&amp;nbsp; Type System 전체로 확장되었습니다. 전반적인 내용을 정리해보고자 합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Python 공식문서중&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://typing.python.org/en/latest/guides/index.html&quot;&gt;TypeHint Guide&lt;/a&gt;에서 많은 내용을 참고했습니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 Type System이 필요한가?&lt;/b&gt;&lt;/h2&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 기준에서 봤을때 &lt;b&gt;Python은 약 타입이자, 동적 타이핑 언어&lt;/b&gt;입니다. 즉 명시적인 타입 캐스팅 없이 타입 변환이 가능하며, &lt;a href=&quot;https://docs.python.org/3/whatsnew/3.13.html#an-experimental-just-in-time-jit-compiler&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JIT 컴파일&lt;/a&gt;을 하긴 하지만 기본적으로 Interpreter 를 통해 동작하기 때문에 런타임에 타입을 체크하는 동적 타입 언어인 것입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 Python에게 Type System 이 필요한 이유는 다음의 이점이 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 안정성과 버그 조기 발견:&lt;/b&gt;&lt;br /&gt;동적 타이핑은 실행 시점에서 타입 에러를 발견하기 때문에, 큰 규모의 프로젝트나 복잡한 코드에서는 예기치 않은 버그가 발생할 위험이 큽니다. 정적 타입 힌트를 도입하면 코드 작성 시점에서 오류를 미리 잡아내어 안정성을 높일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 코드 가독성과 문서화:&lt;/b&gt;&lt;br /&gt;타입 주석은 함수와 클래스의 의도를 명확하게 해 주며, 이를 통해 코드의 이해도가 향상됩니다. 이는 협업 환경에서 특히 중요한 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 개발 도구와의 시너지 효과:&lt;/b&gt;&lt;br /&gt;타입 힌트를 활용하면 IDE 및 정적 분석 도구(예: mypy, Pyright)와의 연동이 원활해져 자동완성, 리팩토링, 코드 탐색 등에서 생산성을 크게 향상시킬 수 있습니다.&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;그럼에도 불구하고, Python은 정적 타입 검사를 강제하지 않는다.&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 타입 힌트가 제공하는 여러 이점에도 불구하고, Python은 여전히 약 타입(dynamic typing) 언어로 남아있습니다. &lt;a href=&quot;https://typing.python.org/en/latest/guides/typing_anti_pitch.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Python 공식 문서&lt;/a&gt;에서는 다양한 이유를 들고 있지만 짧게 정리하자면 다음과 같습니다:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Python은 처음부터 동적 타이핑 언어였고, 이 언어를 사용하는 개발자에게 정적 타입 검사를 강제할 순 없다. 타입 어노테이션자체가 가독성을 해치는 경우도 존재하며, 동적 프레임워크의 특성상 정적 타입을 지정하기 어려울 수 있다.&amp;nbsp; 또한 &lt;a href=&quot;https://www.hyrumslaw.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Hyrum's Law&lt;/a&gt;에 따르면 타입 어노테이션은 복잡해지거나 Any 로 치환될 가능성이 있다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Type System How to&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 왜 Type System이 필요한지, 그럼에도 불구하고 왜 정적 타입을 강제하지 않는지 알게되었습니다. 그렇다면 Python에서는 Type System을 이용하는 방법을 알아보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;간단한 Typing&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python의&amp;nbsp;typing&amp;nbsp;모듈을&amp;nbsp;이용하면&amp;nbsp;함수&amp;nbsp;인자,&amp;nbsp;반환값&amp;nbsp;등&amp;nbsp;코드의&amp;nbsp;주요&amp;nbsp;부분에&amp;nbsp;타입&amp;nbsp;정보를&amp;nbsp;추가할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;이는&amp;nbsp;코드의&amp;nbsp;의도를&amp;nbsp;명확히&amp;nbsp;하고,&amp;nbsp;정적&amp;nbsp;분석&amp;nbsp;도구(예:&amp;nbsp;mypy)를&amp;nbsp;통한&amp;nbsp;오류&amp;nbsp;검출에&amp;nbsp;도움을&amp;nbsp;줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1742453207570&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def greet(name: str) -&amp;gt; str:
    return f&quot;Hello, {name}!&quot;

# 사용 예시
print(greet(&quot;Alice&quot;))&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;name:&amp;nbsp;str&amp;nbsp;은&amp;nbsp;매개변수&amp;nbsp;name이&amp;nbsp;문자열임을&amp;nbsp;명시합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;-&amp;gt;&amp;nbsp;str&amp;nbsp;은&amp;nbsp;함수가&amp;nbsp;문자열을&amp;nbsp;반환함을&amp;nbsp;나타냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Generic Type&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Generic&amp;nbsp;Type은&amp;nbsp;여러&amp;nbsp;타입에&amp;nbsp;대해&amp;nbsp;동일한&amp;nbsp;로직을&amp;nbsp;재사용할&amp;nbsp;수&amp;nbsp;있게&amp;nbsp;해줍니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeVar와&amp;nbsp;Generic을&amp;nbsp;사용하여&amp;nbsp;타입을&amp;nbsp;일반화함으로써,&amp;nbsp;보다&amp;nbsp;유연하면서도&amp;nbsp;타입&amp;nbsp;안전한&amp;nbsp;코드를&amp;nbsp;작성할&amp;nbsp;수&amp;nbsp;있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1742453304402&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from typing import TypeVar, Generic, List

T = TypeVar('T')

class Stack(Generic[T]):
    def __init__(self) -&amp;gt; None:
        self._items: List[T] = []
    
    def push(self, item: T) -&amp;gt; None:
        self._items.append(item)
    
    def pop(self) -&amp;gt; T:
        return self._items.pop()

# 정수형 스택 예시
int_stack = Stack[int]()
int_stack.push(10)
print(int_stack.pop())

# 문자열 스택 예시
str_stack = Stack[str]()
str_stack.push(&quot;hello&quot;)
print(str_stack.pop())&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;T&amp;nbsp;=&amp;nbsp;TypeVar('T')를&amp;nbsp;통해&amp;nbsp;임의의&amp;nbsp;타입&amp;nbsp;변수를&amp;nbsp;선언합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Stack(Generic[T])&amp;nbsp;클래스는&amp;nbsp;다양한&amp;nbsp;타입의&amp;nbsp;데이터를&amp;nbsp;저장할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;스택을&amp;nbsp;구현합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;각각의 스택 인스턴스는 타입 인자를 통해 정해진 타입만을 다루게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Stub File&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stub&amp;nbsp;File(.pyi&amp;nbsp;파일)은&amp;nbsp;&lt;b&gt;기존&amp;nbsp;코드와&amp;nbsp;별도로&amp;nbsp;타입&amp;nbsp;정보를&amp;nbsp;제공&lt;/b&gt;하는&amp;nbsp;파일입니다.&amp;nbsp;이를&amp;nbsp;통해&amp;nbsp;라이브러리나&amp;nbsp;기존&amp;nbsp;코드베이스에&amp;nbsp;직접&amp;nbsp;손대지&amp;nbsp;않고도&amp;nbsp;정적&amp;nbsp;타입&amp;nbsp;정보를&amp;nbsp;추가할&amp;nbsp;수&amp;nbsp;있어,&amp;nbsp;&lt;b&gt;점진적&amp;nbsp;도입에&amp;nbsp;매우&amp;nbsp;유용&lt;/b&gt;합니다. 특히 외부 라이브러리(external library)의 경우, 원본 코드를 수정할 수 없거나 수정이 어려운 상황에서 Stub File을 이용하여 타입 정보를 보완할 수 있습니다. 이러한 이점 때문에 Python에서 gRPC를 활용할때 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;mylib.py:&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1742453535355&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def add(a, b):
    return a + b&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;mylib.pyi:&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1742453552591&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def add(a: int, b: int) -&amp;gt; int: ...&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제&amp;nbsp;코드(mylib.py)는&amp;nbsp;타입&amp;nbsp;정보&amp;nbsp;없이&amp;nbsp;동작하지만,&amp;nbsp;타입&amp;nbsp;스텁&amp;nbsp;파일(mylib.pyi)은&amp;nbsp;정적&amp;nbsp;타입&amp;nbsp;검사기에게&amp;nbsp;함수&amp;nbsp;add의&amp;nbsp;인자와&amp;nbsp;반환값이&amp;nbsp;모두&amp;nbsp;정수임을&amp;nbsp;알려줍니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;외부&amp;nbsp;라이브러리의&amp;nbsp;경우,&amp;nbsp;해당&amp;nbsp;라이브러리가&amp;nbsp;타입&amp;nbsp;정보를&amp;nbsp;포함하고&amp;nbsp;있지&amp;nbsp;않다면,&amp;nbsp;별도의&amp;nbsp;Stub&amp;nbsp;File을&amp;nbsp;제공하여&amp;nbsp;사용자들이&amp;nbsp;정적&amp;nbsp;타입&amp;nbsp;검사&amp;nbsp;도구를&amp;nbsp;통해&amp;nbsp;타입&amp;nbsp;안전성을&amp;nbsp;확보할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;돕습니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;예를&amp;nbsp;들어,&amp;nbsp;typeshed&amp;nbsp;프로젝트는&amp;nbsp;Python의&amp;nbsp;표준&amp;nbsp;라이브러리와&amp;nbsp;다양한&amp;nbsp;외부&amp;nbsp;라이브러리의&amp;nbsp;타입&amp;nbsp;정보를&amp;nbsp;수집하여&amp;nbsp;관리하며,&amp;nbsp;이를&amp;nbsp;통해&amp;nbsp;많은&amp;nbsp;라이브러리들이&amp;nbsp;정적&amp;nbsp;타입&amp;nbsp;검사를&amp;nbsp;활용할&amp;nbsp;수&amp;nbsp;있게&amp;nbsp;합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;&lt;b&gt;Python Document, TypeSystem Guides: &lt;a href=&quot;https://typing.python.org/en/latest/guides/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://typing.python.org/en/latest/guides/index.html&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Python Docuemnt,Stubfile: &lt;a href=&quot;https://typing.python.org/en/latest/spec/distributing.html#stub-files&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://typing.python.org/en/latest/spec/distributing.html#stub-files&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Python Document, Writing&amp;nbsp;and&amp;nbsp;Maintaining: &lt;a href=&quot;https://typing.python.org/en/latest/guides/writing_stubs.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://typing.python.org/en/latest/guides/writing_stubs.html&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;pep TypeHints: &lt;a href=&quot;https://peps.python.org/pep-0484/#stub-files&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://peps.python.org/pep-0484&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Python 3.13 JIT: &lt;a href=&quot;https://docs.python.org/3/whatsnew/3.13.html#an-experimental-just-in-time-jit-compiler&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.python.org/3/whatsnew/3.13.html#an-experimental-just-in-time-jit-compiler&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>탐구 생활/Python</category>
      <category>python stubfile</category>
      <category>python typehint</category>
      <category>python 타입힌트</category>
      <category>stubfile</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/80</guid>
      <comments>https://probehub.tistory.com/80#entry80comment</comments>
      <pubDate>Thu, 20 Mar 2025 14:42:52 +0900</pubDate>
    </item>
    <item>
      <title>Python gRPC (2) - 기본 구조와 python 구현</title>
      <link>https://probehub.tistory.com/78</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;gRPC-image.png&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d6vgUt/btsMFlCH0yN/o6kAHeJvbEJWeLURYMime1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d6vgUt/btsMFlCH0yN/o6kAHeJvbEJWeLURYMime1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d6vgUt/btsMFlCH0yN/o6kAHeJvbEJWeLURYMime1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd6vgUt%2FbtsMFlCH0yN%2Fo6kAHeJvbEJWeLURYMime1%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;280&quot; height=&quot;280&quot; data-filename=&quot;gRPC-image.png&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난글에서 RPC (gRPC) 를 선택한 이유를 알아봤습니다. 이제는 python 에서 어떻게 gRPC 를 사용할 수 있는지 공유하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;.proto 파일&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RPC는 엄격한 규칙을 따르기 때문에 &lt;b&gt;IDL(Interface Definication Language)&lt;/b&gt;이 제공됩니다. 특히 구글에서 만들고 배포하는 gRPC는 관련 문서와 tool이 잘 구비되어 있어서 구현하기 용이합니다. gRPC를 구현하기 위해 가장 먼저 해야할 것은 서버와 메시지 규약을 정하는것입니다. 이러한 규약은 &lt;b&gt;.proto&lt;/b&gt; 파일에 정의됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Service 와 Message&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC는 &lt;a href=&quot;https://grpc.io/docs/what-is-grpc/core-concepts/#service-definition&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Service&lt;/a&gt;를 통해서 호출될 함수를 정의하고, Message 를 통해서 호출 파라미터, 응답 값을 정의합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예제는 &lt;a href=&quot;https://protobuf.dev/best-practices/1-1-1/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;protobuf bestpractice 1-1-1 rule&lt;/a&gt; 에 따라 message 별로 파일을 나누고, 각 message 파일을 service 파일에서 import 하여 정의하고 있습니다:&lt;/p&gt;
&lt;pre id=&quot;code_1741615857302&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;syntax = &quot;proto3&quot;;

package hello;

import message/hello_request.proto
import message/hello_response.proto

service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1741616328354&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;syntax = &quot;proto3&quot;;

package hello;

message HelloRequest {
  string greeting = 1;
  // data_type | filed_name | tag_number
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1741616635463&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;syntax = &quot;proto3&quot;;

package hello;

message HelloResponse {
  string reply = 1;
  // data_type | filed_name | tag_number  
}&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;service: 클라이언트가 호출할 수 있는 원격 메서드 집합&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;message: &lt;b&gt;클라이언트와 서버 간에 주고받는 데이터 구조&lt;/b&gt;를 정의하는 개념&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 package, 그리고 field 옆에 있는 숫자는 무엇인지 설명되지 않았습니다. protocol buffers 문서에서 그 답을 얻어보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Protocol buffers&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 &lt;a href=&quot;https://protobuf.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;protocol buffer&lt;/a&gt; 란 gRPC 에서 활용되는 IDL이면서 하나의 데이터 직렬화 매커니즘입니다. 줄여서 protobuf, 혹은 pb라고도 합니다. 하나의 언어체계인만큼 위에서 언급된 package, tag number(filed 옆 숫자) 는 pb 만의 특수한 약속입니다. pb 를 사용하면서 알아두면 유용한 개념을 정리하겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;package&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;namespace 를 정의합니다. 동일한 이름의 Service, Message 일지라도 namespace 가 다르면 gRPC 내부적으로 다르게 취급됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;tag number&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필드를 식별하는 고유한 식별자입니다. json 이 데이터 타입을 정의할때 전체 필드이름을 사용하는것과 달리 pb 는 이 tag number 를 이용해서 필드를 특정합니다. &lt;b&gt;덕분에 json에 비해 더 경량화된 데이터 구조&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;tag number 를 기준으로 필드를 구분하기 때문에 동일한 tag number 에 대해서 다른 필드를 정의하면 예상치 못한 동작을 초리할 수 있습니다. &lt;a href=&quot;https://protobuf.dev/best-practices/dos-donts/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;즉, 한번 폐기된 tag number 를 재사용하는 것은 엄격히 금지&lt;/a&gt;됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;데이터 타입&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예제에서는 오로지 string 타입만 나와있지만, gRPC 는 다양한 데이터 타입을 지원합니다. 모든 내용을 블로그로 옮겨오는것은 비효율적이기 때문에 &lt;a href=&quot;https://protobuf.dev/programming-guides/proto3/#scalar&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;pb의 scala type 에 대한 링크&lt;/a&gt;를 남겨두겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;b&gt;gRPC 의 구조&lt;/b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;landing-2.svg&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;327&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r4wYy/btsMGfKurdb/0LM4JHEGV2GmOxdgdNHA41/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r4wYy/btsMGfKurdb/0LM4JHEGV2GmOxdgdNHA41/tfile.svg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r4wYy/btsMGfKurdb/0LM4JHEGV2GmOxdgdNHA41/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr4wYy%2FbtsMGfKurdb%2F0LM4JHEGV2GmOxdgdNHA41%2Ftfile.svg&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;552&quot; height=&quot;327&quot; data-filename=&quot;landing-2.svg&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;327&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 구글에서 제공하는 gRPC의 도식입니다. gRPC Server 에서 각 gRPC Stub 과 Proto Request, Response 를 주고 받는것을 알 수 있습니다. 이제 각각이 무엇인지 간단히 정리하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt;server&lt;/b&gt;&lt;/b&gt;&lt;b&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 data-end=&quot;128&quot; data-start=&quot;65&quot;&gt;&lt;b&gt;gRPC 서버&lt;/b&gt;는 클라이언트가 호출할 수 있는 원격 프로시저(RPC)를 정의하고 처리하는 역할을 합니다.&lt;/li&gt;
&lt;li data-end=&quot;185&quot; data-start=&quot;129&quot;&gt;proto 파일에서 정의한 &lt;b&gt;service&lt;/b&gt;를 구현하여 요청을 처리하고 응답을 반환합니다.&lt;/li&gt;
&lt;li data-end=&quot;249&quot; data-start=&quot;186&quot;&gt;보통 &lt;b&gt;멀티스레딩을 지원하는 고성능 서버&lt;/b&gt;로 동작하며, HTTP/2 기반의 스트리밍을 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;stub&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 data-end=&quot;945&quot; data-start=&quot;890&quot;&gt;Stub(스텁)은 &lt;b&gt;클라이언트와 서버 간의 gRPC 요청을 중개하는 역할&lt;/b&gt;을 합니다.&lt;/li&gt;
&lt;li data-end=&quot;945&quot; data-start=&quot;890&quot;&gt;server와 stub은 동일한, 혹은 적어도 이전버전과 호환되는(Backwards-compatibility) proto 파일을 기반으로 통신해야합니다.&lt;/li&gt;
&lt;li data-end=&quot;1014&quot; data-start=&quot;946&quot;&gt;클라이언트에서 &lt;b&gt;서버를 호출하는 객체&lt;/b&gt;이며, 서버의 원격 메서드를 &lt;b&gt;로컬 메서드처럼 호출&lt;/b&gt;할 수 있도록 합니다.&lt;/li&gt;
&lt;li data-end=&quot;1076&quot; data-start=&quot;1015&quot;&gt;Stub에는 &lt;b&gt;Blocking(동기)&lt;/b&gt;, &lt;b&gt;Non-Blocking(비동기)&lt;/b&gt; 두 가지 유형이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt;channel&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 data-end=&quot;1961&quot; data-start=&quot;1910&quot;&gt;Channel(채널)은 &lt;b&gt;클라이언트와 서버 간의 연결을 담당하는 객체&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li data-end=&quot;2024&quot; data-start=&quot;1962&quot;&gt;gRPC는 HTTP/2 기반이므로, 하나의 Channel에서 여러 개의 gRPC 호출을 수행할 수 있습니다.&lt;/li&gt;
&lt;li data-end=&quot;2083&quot; data-start=&quot;2025&quot;&gt;보통 &lt;b&gt;TLS(보안)&lt;/b&gt; 또는 &lt;b&gt;플레인 텍스트(일반 HTTP/2)&lt;/b&gt; 연결을 설정할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Python과 gRPC&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 proto 파일의 존재와, server, stub 그리고 channel 의 개념을 알았습니다. 이제는&amp;nbsp;&lt;b&gt;Python에서 gRPC 서버를 구현하는 방법&lt;/b&gt;과 &lt;b&gt;Python만의 특징&lt;/b&gt;을 살펴보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Python의 gRPC 특징&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;Python에서는 grpcio-tools를 사용하여 proto 파일을 컴파일할 필요 없이&amp;nbsp;런타임에 동적으로 스텁을 생성할 수 있습니다.&lt;/li&gt;
&lt;li&gt;Python의 asyncio를 활용한 비동기 gRPC 서버/클라이언트를 쉽게 구현할 수 있습니다. &lt;a href=&quot;https://grpc.io/docs/guides/performance/#python&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;반면 Stream을 사용하는 것은 권장되지 않습니다.&lt;/a&gt;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Python에서는 다른 언어보다 더 간결하게 gRPC를 구현할 수 있으며, 클래스를 직접 상속받아 서비스 구현을 할 수 있습니다. 또한 &lt;a href=&quot;https://github.com/grpc/grpc/blob/master/examples/python/helloworld/greeter_server_with_reflection.py&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Reflection API를 지원&lt;/a&gt;하기 때문에 gRPC 서비스를 동적으로 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약하자면, Python에서 gRPC를 이용하는 것은 쉽지만 Stream 을 이용하기 보다는 asyncio 를 직접 이용하는 방안을 채택해야합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Python gRPC 서버 구축 (gRPC Tools 활용)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 정리한 개념을 통해 python 코드에서 gRPC를 세팅하려면 어떻게 해야하는지 명확해진것 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;proto 파일이 있어야하며&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://github.com/grpc/grpc/tree/master/tools/distrib/python/grpcio_tools&quot;&gt;grcpio_tools&lt;/a&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 을 이용하여 python 에서 gRPC를 구현할떄 필요한 protobuf 파일들을 생성합니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;protobuf 파일을 기반으로 gRPC server 가 정의되어야 하고&lt;/li&gt;
&lt;li&gt;protobuf 파일을 기반으로 gRPC stub 을 클라이언트가 활용해야합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 완성 코드는 &lt;a href=&quot;https://github.com/timothy-jeong/python-grpc-example/tree/simple-example&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github&lt;/a&gt; 에서 확인할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. proto 파일 작성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째로 proto 파일을 만들어보겠습니다. 아래와 같은 구조로 proto 파일은 service 와 message 를 분리해둘겁니다.&lt;/p&gt;
&lt;pre id=&quot;code_1742098812917&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.
├── proto
│   ├── member_service.proto
│   └── message
│       ├── member_request.proto
│       └── member_response.proto&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 message 를 먼저 만들고,&lt;/p&gt;
&lt;pre id=&quot;code_1742098864871&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;syntax = &quot;proto3&quot;;

package member;


message MemberRequest {
    string member_id = 1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1742098875180&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;syntax = &quot;proto3&quot;;

package member;


message MemberResponse {
    string member_id = 1;
    string member_name = 2;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;service 를 정의해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1742098902561&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;syntax = &quot;proto3&quot;;

package member;

import &quot;message/member_request.proto&quot;;
import &quot;message/member_response.proto&quot;;

service MemberService {
    rpc GetMember (MemberRequest) returns (MemberResponse);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. grpcio_tools 활용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;grpcio_tools 는 정의해둔 proto 파일을 통해 python에서 활용 가능한 protobuf 파일을 만들어줍니다. 이때 참 주의할 사항이 많습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;service 파일이 참조하는 message 파일의 구조가 python 파일 구조에도 그대로 반영되어야 합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;생성된 protobuf 파이썬 파일의 위치에 따라 활용 의도가 달라지며, python 실행 컨텍스트로 고려되어야 합니다.&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 이번 예제에서 app 밑에 pb 를 두는 구조를 선택했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;app 에서 정의한 기능(비즈니스 로직, DB 연결) 을 공유하려면 app 밑에서 grpc 를 정의하는게 유리하다고 생각했습니다.&lt;/li&gt;
&lt;li&gt;또한 pb 밑에 message 디렉터리를 두어서 service 가 message 를 참조하는 구조를 그대로 모방하도록 했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1742099309442&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.
├── proto
│   ├── member_service.proto
│   └── message
│       ├── member_request.proto
│       └── member_response.proto
└── services
    ├── board_service
    │   ├── app
    │   │   ├── main.py
    │   │   └── pb
    │   │       └── message
    │   ├── proto.sh
    └── member_service
        ├── app
        │   ├── grpc_server.py
        │   ├── main.py
        │   └── pb
        │       └── message&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 grpc_tools 를 이용해보겠습니다. 우선 pip install grpc_tools 를 해주세요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 각 서비스마다 다음의 스크립트를 만들어두고, 실행하겠습니다. service 와 message 를 따로따로 긁어와줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1742099167329&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

python -m grpc_tools.protoc \
  -I=../../proto \
  --python_out=./app/pb \
  --grpc_python_out=./app/pb \
  --pyi_out=./app/pb \
  ../../proto/member_service.proto

python -m grpc_tools.protoc \
  -I=../../proto \
  --python_out=./app/pb \
  --grpc_python_out=./app/pb \
  --pyi_out=./app/pb \
  ../../proto/message/*.proto&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 data-start=&quot;53&quot; data-end=&quot;129&quot;&gt;&lt;b&gt;입력 경로 지정(-I):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;77&quot; data-end=&quot;129&quot;&gt;
&lt;li data-start=&quot;77&quot; data-end=&quot;129&quot;&gt;-I=../../proto 옵션은 proto 파일들이 위치한 디렉토리를 지정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-start=&quot;130&quot; data-end=&quot;333&quot;&gt;&lt;b&gt;Python 메시지 파일 생성(--python_out):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;172&quot; data-end=&quot;333&quot;&gt;
&lt;li data-start=&quot;172&quot; data-end=&quot;276&quot;&gt;이 옵션은 proto 파일의 메시지 정의를 기반으로 Python 코드를 생성하여 지정된 경로(여기서는 ./app/pb)에 &amp;lt;proto파일명&amp;gt;_pb2.py 파일을 생성합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-start=&quot;334&quot; data-end=&quot;459&quot;&gt;&lt;b&gt;gRPC 관련 파일 생성(--grpc_python_out):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;378&quot; data-end=&quot;459&quot;&gt;
&lt;li data-start=&quot;378&quot; data-end=&quot;459&quot;&gt;이 옵션은 gRPC 서비스 인터페이스에 대한 코드를 생성하여 &amp;lt;proto파일명&amp;gt;_pb2_grpc.py 파일을 같은 출력 경로에 생성합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-start=&quot;460&quot; data-end=&quot;592&quot;&gt;&lt;b&gt;Python 타입 스텁 생성(--pyi_out):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;498&quot; data-end=&quot;592&quot;&gt;
&lt;li data-start=&quot;498&quot; data-end=&quot;592&quot;&gt;이 옵션은 생성된 메시지에 대한 정적 타입 검사용 stub 파일 (.pyi)을 생성합니다. 이를 통해 Python 코드 작성 시&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;타입 힌트&lt;/b&gt;를 활용할 수 있습니다.&lt;/li&gt;
&lt;li data-start=&quot;498&quot; data-end=&quot;592&quot;&gt;개인적으로 이게 정말 중요합니다. gRPC 타입 힌트 유무에 따라 개발 난이도와 런타임 오류 빈도가 많이 달라질겁니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-start=&quot;593&quot; data-end=&quot;667&quot;&gt;&lt;b&gt;마지막 인자로 proto 파일 지정:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;624&quot; data-end=&quot;667&quot;&gt;
&lt;li data-start=&quot;624&quot; data-end=&quot;667&quot;&gt;스크립트의 마지막 부분에서 실제로 컴파일할 proto 파일들을 지정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;더 자세한 설명은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot; href=&quot;https://protobuf.dev/reference/python/python-generated/&quot;&gt;공식문서&lt;/a&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;에서 확인하실 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 pb 밑에 다양한 파일들이 생성되었지만, 대표적으로 3가지만 살펴보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;member_service_pb2.py&lt;/li&gt;
&lt;li&gt;member_service_pb2_grpc.py&lt;/li&gt;
&lt;li&gt;member_response_pb.pyi&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;member_service_pb2 와&amp;nbsp; member_service_pb2_grpc 의 차이는 뭘까요?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;직관적으로 pb2는 protocol buffer 를 위한 python 구현체일것이고, grpc는 그러한 pb2를 이용한 grpc 구현체일것이라고 알 수 있을 것입니다. 조금만 더 파고들어서 message 내부를 보면 더 명확해지죠. 이러한 직관 말고 내부 코드를 조금 살펴보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;member_service_pb2&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1742100216122&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ...

_sym_db = _symbol_database.Default()


from message import member_request_pb2 as message_dot_member__request__pb2
from message import member_response_pb2 as message_dot_member__response__pb2


DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14member_service.proto\x12\x06member\x1a\x1cmessage/member_request.proto\x1a\x1dmessage/member_response.proto2K\n\rMemberService\x12:\n\tGetMember\x12\x15.member.MemberRequest\x1a\x16.member.MemberResponseb\x06proto3')

_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'member_service_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
  DESCRIPTOR._loaded_options = None
  _globals['_MEMBERSERVICE']._serialized_start=93
  _globals['_MEMBERSERVICE']._serialized_end=168
# @@protoc_insertion_point(module_scope)&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;다소 생소한 개념이 보입니다. 얼추 보았을떄 pb 파이썬 구현체에서는 &lt;b&gt;descriptor&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;p data-ke-size=&quot;size16&quot;&gt;pb에서 &lt;b&gt;descriptior는 pb에서 정의된 데이터 타입에 대한 메타데이터를 설명하는 역할&lt;/b&gt;을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고&amp;nbsp;&lt;b&gt;member_service_pb2&amp;nbsp;&lt;/b&gt;파일은 descriptor pool 에 정의된 proto를 추가하고 (AddSerializedFile), 이러한 pool 을 전역적으로 build 하는 역할을 한다고 이해할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 작업을 통해 proto로 정의된 메타데이터가 python에서 다룰 수 있는 무언가가 되는것이라고 이해하면 되겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;member_service_pb2_grpc&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1742102104207&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ...

class MemberServiceStub(object):
    &quot;&quot;&quot;Missing associated documentation comment in .proto file.&quot;&quot;&quot;

    def __init__(self, channel):
        &quot;&quot;&quot;Constructor.

        Args:
            channel: A grpc.Channel.
        &quot;&quot;&quot;
        self.GetMember = channel.unary_unary(
                '/member.MemberService/GetMember',
                request_serializer=message_dot_member__request__pb2.MemberRequest.SerializeToString,
                response_deserializer=message_dot_member__response__pb2.MemberResponse.FromString,
                _registered_method=True)


class MemberServiceServicer(object):
    &quot;&quot;&quot;Missing associated documentation comment in .proto file.&quot;&quot;&quot;

    def GetMember(self, request, context):
        &quot;&quot;&quot;Missing associated documentation comment in .proto file.&quot;&quot;&quot;
        context.set_code(grpc.StatusCode.UNIMPLEMENTED)
        context.set_details('Method not implemented!')
        raise NotImplementedError('Method not implemented!')


def add_MemberServiceServicer_to_server(servicer, server):
    rpc_method_handlers = {
            'GetMember': grpc.unary_unary_rpc_method_handler(
                    servicer.GetMember,
                    request_deserializer=message_dot_member__request__pb2.MemberRequest.FromString,
                    response_serializer=message_dot_member__response__pb2.MemberResponse.SerializeToString,
            ),
    }
    generic_handler = grpc.method_handlers_generic_handler(
            'member.MemberService', rpc_method_handlers)
    server.add_generic_rpc_handlers((generic_handler,))
    server.add_registered_method_handlers('member.MemberService', rpc_method_handlers)&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;grpc 영역으로 넘어오니 조금 익숙한 개념이 보입니다. servicer, channel, stub 이 그것입니다. 이 글의 목적이 python에서 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;MemberServiceStub은 proto 파일에서 정의한 service를 끌어다 쓰는 클라이언트쪽에서 사용할 것이고, 각 요청을 클래스 수준에서 멤버변수로 갖게 된다는것을 알 수 있습니다.(특히 여기서는 &lt;a href=&quot;https://grpc.io/docs/what-is-grpc/core-concepts/#unary-rpc&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;단항요청&lt;/a&gt;으로 구성되어 있습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemberServiceServicer 는 proto 파일에서 정의한 service 를 정의하는 서버쪽에서 사용할 것이고, 각 요청이 함수로 정의되어 있다는 것을 알 수 있습니다. context 파라미터를 이용하여 마치 HTTP 에서 상태코드를 정의하듯이 활용할 수 있습니다,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;add_MemberServiceServicer_to_server 함수를 보면, rpc 핸들러를 정의하고, 이를 등록하는 과정이 있음을 알 수 있습니다. 이상을 통해 gRPC 서버를 정의할때 활용되어야 한다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 생각하면 Servicer 에서 해당 함수를 고쳐서 grpc 서버 기능을 구현하고 싶지만 &lt;b&gt;protoc에 의해 생성된 코드를 임의로 고치는 것은 권장되지 않습니다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;member_response_pb.pyi&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일은 &lt;a href=&quot;https://peps.python.org/pep-0484/#stub-files&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;stub file&lt;/a&gt; 이라고 합니다. stub file 덕분에 gRPC Request와 Response 를 일반 Python 클래스를 다루듯이 사용할수 있게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1742447265271&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Optional as _Optional

DESCRIPTOR: _descriptor.FileDescriptor

class MemberResponse(_message.Message):
    __slots__ = (&quot;member_id&quot;, &quot;member_name&quot;)
    MEMBER_ID_FIELD_NUMBER: _ClassVar[int]
    MEMBER_NAME_FIELD_NUMBER: _ClassVar[int]
    member_id: str
    member_name: str
    def __init__(self, member_id: _Optional[str] = ..., member_name: _Optional[str] = ...) -&amp;gt; None: ...&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;b&gt;3. gRPC 서버 정의&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 &lt;b&gt;member_service_pb2_grpc&amp;nbsp;&lt;/b&gt;를 활용하여 member_service 에서 grpc_server.py 를 만드는 방법을 공유하겠습니다. MemberServiceServicer 를 확장하는 클래스를 정의하고, GetMember 메서드를 오버라이딩합니다. 이때 serer 를 grpc.aio.server() 로 정의하여 python에서 steram 을 사용할경우 발생하는 성능상 불이익을 완화합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1742103274169&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import asyncio
import grpc
import uuid

from sqlalchemy import select

from app.pb import member_service_pb2_grpc
from app.pb.message import member_request_pb2, member_response_pb2
from app.pb.member_service_pb2_grpc import MemberServiceServicer
from app.database.connection import async_session, engine
from app.database.model import MemberModel, BaseModelDeclarative

class MemberServiceServicerImpl(MemberServiceServicer):
    async def GetMember(self, request: member_request_pb2.MemberRequest, context):
        async with async_session() as db:
            result = await db.execute(
                select(MemberModel).where(MemberModel.id == uuid.UUID(request.member_id))
            )
            member = result.scalars().first()

            if not member:
                context.set_code(grpc.StatusCode.NOT_FOUND)
                context.set_details(&quot;Member not found&quot;)
                return member_response_pb2.MemberResponse()

        return member_response_pb2.MemberResponse(
            member_id=str(member.id),
            member_name=member.name
        )

async def init_db():
    async with engine.begin() as conn:
        await conn.run_sync(BaseModelDeclarative.metadata.create_all)

async def serve():
    await init_db()

    server = grpc.aio.server()
    member_service_pb2_grpc.add_MemberServiceServicer_to_server(MemberServiceServicerImpl(), server)
    listen_addr = &quot;[::]:50051&quot;
    server.add_insecure_port(listen_addr)
    await server.start()
    await server.wait_for_termination()


if __name__ == &quot;__main__&quot;:
    asyncio.run(serve())&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;b&gt;4. gRPC 클라이언트 정의&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;client 를 정의하는건 더 간단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;channel 을 만들때에서 grpc.aio 를 이용하여 비동기적으로 grpc 서버를 호출할 수 있도록 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;member-service:50051 로 채널의 연결점을 정의한건, &lt;a href=&quot;https://github.com/timothy-jeong/python-grpc-example/tree/simple-example&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github 코드&lt;/a&gt;에서 docker-compose 설정에 기반한것으로 언제든 바뀔 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1742103664717&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from uuid import UUID

import grpc
from google.protobuf.json_format import MessageToDict

from app.pb import member_service_pb2_grpc
from app.pb.message import member_request_pb2

channel = grpc.aio.insecure_channel(&quot;member-service:50051&quot;)
stub = member_service_pb2_grpc.MemberServiceStub(channel)


class MemberServiceClient:
    @staticmethod
    async def get_member_by(member_id: UUID):
        member_response = await stub.GetMember(member_request_pb2.MemberRequest(member_id=str(member_id)))
        return MessageToDict(member_response, preserving_proto_field_name=True)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상의 작업을 완료한 뒤 테스트를 해보면 gRPC 통신이 잘 동작하는 것을 알 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;&lt;b&gt;gRPC core concepts: &lt;a href=&quot;https://grpc.io/docs/what-is-grpc/core-concepts/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://grpc.io/docs/what-is-grpc/core-concepts/&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;grpc-tools: &lt;a href=&quot;https://grpc.io/docs/languages/python/quickstart/#grpc-tools&quot;&gt;https://grpc.io/docs/languages/python/quickstart/#grpc-tools&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Protocol buffers overview: &lt;a href=&quot;https://protobuf.dev/overview/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://protobuf.dev/overview/&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Protocol buffers dos-donts: &lt;a href=&quot;https://protobuf.dev/best-practices/dos-donts/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://protobuf.dev/best-practices/dos-donts/&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Protocol buffers python generatede code: &lt;a href=&quot;https://protobuf.dev/reference/python/python-generated/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://protobuf.dev/reference/python/python-generated/&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;what is descriptors by buf.build: &lt;a href=&quot;https://buf.build/docs/reference/descriptors/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://buf.build/docs/reference/descriptors/&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>탐구 생활/gRPC&amp;amp;Python</category>
      <category>grpc</category>
      <category>Python</category>
      <category>python grpc how to</category>
      <category>python grpc tutorial</category>
      <category>python proto file</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/78</guid>
      <comments>https://probehub.tistory.com/78#entry78comment</comments>
      <pubDate>Sat, 8 Mar 2025 22:01:47 +0900</pubDate>
    </item>
    <item>
      <title>Python gRPC (1) - 왜 gRPC 를 선택했나</title>
      <link>https://probehub.tistory.com/76</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;gRPC-image.png&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brL7Mo/btsMD4hR0MO/kn45pF5Fyw9anD2rnxP6v1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brL7Mo/btsMD4hR0MO/kn45pF5Fyw9anD2rnxP6v1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brL7Mo/btsMD4hR0MO/kn45pF5Fyw9anD2rnxP6v1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrL7Mo%2FbtsMD4hR0MO%2Fkn45pF5Fyw9anD2rnxP6v1%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;280&quot; height=&quot;280&quot; data-filename=&quot;gRPC-image.png&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA를 구현할때 각 서비스별로 분리된 데이터베이스를 관리합니다. 이 상황에서 화면으로 전달될 데이터가 하나의 서비스에 국한될 가능성은 거의 없을 것입니다. 이때 &lt;a href=&quot;https://samnewman.io/patterns/architectural/bff/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BFF&lt;/a&gt; 와 같이 각 서비스와 통신하여 데이터를 모아주는 역할을 하는 서비스가 존재하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 데이터를 동기화하는 다양한 패턴이 있지만, &lt;b&gt;실시간으로 데이터를 동기화해야하는 경우에는 gRPC 가 적합하다 판단&lt;/b&gt;했습니다. 이 글에서는 왜 그렇게 생각했는지 정리했습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;MSA 에서 데이터 동기화 패턴&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://microservices.io/patterns/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Chris Richardson의 Microservice Pattern&lt;/a&gt;에 따르면 &amp;lt;데이터 동기화 패턴&amp;gt; 이라는 용어가 적합하지 않을 수 있습니다. 이 글에서&amp;nbsp;말하는 데이터 동기화 패턴이란 &lt;b&gt;마이크로 서비스 환경에서 사용자의 요구에 따라 일관성있는 데이터를 전달하기 위한 패턴&lt;/b&gt;으로 정의하겠습니다. 그리고 데이터 동기화 패턴에는 다음의 것들이 있습니다:&lt;/p&gt;
&lt;h3 data-end=&quot;122&quot; data-start=&quot;105&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. API 기반 동기화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스들이 필요한 데이터를 실시간으로 다른 서비스의 API를 통해 요청하여 가져옵니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터가 필요한 순간에 요청을 통해 실기간으로 데이터를 동기화할 수 있다는 점,&lt;/li&gt;
&lt;li&gt;해당 데이터를 획득하기 위한 프로토콜만 맞추면 된다는 점에서 다소 결합도가 낮은 방법이라는 장점이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다만 네트워크 지연 및 호출 빈도에 따라 성능이 영향을 받을 우려가 있어 Loadbalancing 이 주요하며&lt;/li&gt;
&lt;li&gt;요청을 받아서 처리하는 서비스의 상태에 따라 데이터 품질이 달라진다는 점에서 Service Discovery 를 고려해야할 뿐만 아니라 SPOF 문제도 내포하고 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;REST API, Remote Procedure Call 등 각 서비스간 포로토콜에 기반한 통신을 포함합니다.&lt;/li&gt;
&lt;/ul&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-start=&quot;313&quot; data-end=&quot;341&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 이벤트 기반 동기화 (메시지 큐 활용)&lt;/b&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;&lt;b&gt;장점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스간 느슨한 결합을 유지할 수 있으며 DLQ를 활용하여 장애허용(fault tolerance)도가 높은 시스템을 기대할 수 있습니다. 또한 polling 방식을 이용하여 더 안전하게 이벤트 동기화를 처리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실시간 데이터 동기화 구현을 위해 pushing 방식을 채택한다면 멱등성(idempotency) 보장을 위해 별도의 설계가 추가되어야하며,&lt;/li&gt;
&lt;li&gt;트랜잭션 동기화를 위해 Saga Pattern 을 도입한다면 전체 MSA 설계가 어려워지는 측면이 존재합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지 큐(예: Kafka, RabbitMQ)를 이용한 트랜잭션 동기화가 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-end=&quot;618&quot; data-start=&quot;573&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. Change Data Capture (CDC) / ETL 기반 동기화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;CDC는 데이터베이스에서 발생하는 변경 사항(Insert, Update, Delete)을 실시간 또는&lt;b&gt; 준실시간&lt;/b&gt;으로 캡처하여 다른 서비스나 데이터 저장소로 전파하는 기술입니다. 주로 데이터 동기화, 실시간 분석, 이벤트 기반 아키텍처 구축 등에서 사용됩니다. ETL(Extract, Transform, Load)은 추출된 데이터를 변환하고 목적지 시스템에 적재하는 일련의 작업을 의미합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span&gt;아직 직접 해본적이 없어서 다소 이론적인 내용들입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;장점&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터가 변경되는 즉시 이를 캡처하여 동기화하기 때문에 실시간 데이터 일관성 보장에 매우 효과적이라고 알려져있습니다.&lt;/li&gt;
&lt;li&gt;분산 데이터베이스 환경에서도 안정적이면서 효율적인 동기화를 지원하는것으로 알려져있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;단점&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CDC 프로세스를 관리하기 위한 추가 인프라를 운영해야 하며, 장애 관리 및 모니터링 시스템 구성이 복잡해질 수 있습니다.&lt;/li&gt;
&lt;li&gt;CDC 프로세스가 중단될 경우 데이터 유실 또는 데이터 복제 지연 등의 문제가 발생할 수 있어서 역시 설계가 복잡해집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;예시&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Debezium과 Kafka를 활용한 실시간 데이터 복제, AWS Database Migration Service(DMS) 등이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;결국 실시간으로 데이터를 동기화하는 선택지로는 API 기반 동기화와 CDC 기반 동기화가 있습니다. 그런데 CDC로 접근하는것은 공부가 부족하기도 하지만 관련 유지보수 비용이 높아진다는 부담이 존재했습니다. 그래서 API 기반 동기화를 선택하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 API 기반 동기화 중 구체적으로 어떤 프로토콜을 이용할것인지 정해야합니다.&lt;/p&gt;
&lt;h3 data-end=&quot;735&quot; data-start=&quot;690&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. RPC(gRPC)와 GraphQL 중 어떤 프로토콜을 선택할지에 대한 고민&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-end=&quot;746&quot; data-start=&quot;737&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;RPC란?&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;896&quot; data-start=&quot;747&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;851&quot; data-start=&quot;747&quot;&gt;Remote Procedure Call의 약자로, 원격에서 함수를 호출하는 방식입니다. 대표적으로 gRPC가 있으며, Protocol Buffers를 이용해 데이터 구조를 정의합니다.&lt;/li&gt;
&lt;li data-end=&quot;896&quot; data-start=&quot;852&quot;&gt;주로 서비스 간 빠른 속도와 효율적이고 엄격한 규격이 필요할 때 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;912&quot; data-start=&quot;898&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;GraphQL이란?&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1037&quot; data-start=&quot;913&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;955&quot; data-start=&quot;913&quot;&gt;Facebook에서 개발된 데이터 쿼리 및 조작을 위한 쿼리 언어입니다.&lt;/li&gt;
&lt;li data-end=&quot;1037&quot; data-start=&quot;956&quot;&gt;클라이언트가 데이터를 정확히 원하는 대로 요청하여 받을 수 있고, &lt;a href=&quot;https://graphql.org/learn/subscriptions/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Subscription&lt;/a&gt;을 통해 데이터 변경을 실시간으로 구독할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1042&quot; data-start=&quot;1039&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-end=&quot;1071&quot; data-start=&quot;1044&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;gRPC vs GraphQL 간 주요 비교&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;1460&quot; data-start=&quot;1073&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;gRPC&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;GraphQL&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1159&quot; data-start=&quot;1123&quot;&gt;
&lt;td&gt;데이터 형식&lt;/td&gt;
&lt;td&gt;Protocol Buffers&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1209&quot; data-start=&quot;1160&quot;&gt;
&lt;td&gt;통신 프로토콜&lt;/td&gt;
&lt;td&gt;HTTP/2 (binary)&lt;/td&gt;
&lt;td&gt;HTTP (주로 POST 방식)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1262&quot; data-start=&quot;1210&quot;&gt;
&lt;td&gt;성능&lt;/td&gt;
&lt;td&gt;우수(낮은 지연, 높은 성능)&lt;/td&gt;
&lt;td&gt;양호(하지만 데이터 선택성으로 효율성 높음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1307&quot; data-start=&quot;1263&quot;&gt;
&lt;td&gt;실시간성&lt;/td&gt;
&lt;td&gt;Streaming 방식 지원&lt;/td&gt;
&lt;td&gt;Subscription 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1364&quot; data-start=&quot;1308&quot;&gt;
&lt;td&gt;데이터 유연성&lt;/td&gt;
&lt;td&gt;낮음(정의된 데이터 형식만 가능)&lt;/td&gt;
&lt;td&gt;매우 높음(필요한 데이터만 선택 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1418&quot; data-start=&quot;1365&quot;&gt;
&lt;td&gt;클라이언트 접근성&lt;/td&gt;
&lt;td&gt;낮음(엄격한 정의 필요)&lt;/td&gt;
&lt;td&gt;높음(쿼리 유연성, 클라이언트 친화적)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1460&quot; data-start=&quot;1419&quot;&gt;
&lt;td&gt;유지보수&lt;/td&gt;
&lt;td&gt;schema 관리 부담 존재&lt;/td&gt;
&lt;td&gt;schema 진화 용이&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-end=&quot;1465&quot; data-start=&quot;1462&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1500&quot; data-start=&quot;1467&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 기술의 선택&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;그런 측면에서 데이터 타입 유연성이 뛰어난 GraphQL을 선택하기보다 RPC가 적합했으며, 특히 gRPC는 HTTP2 를 이용하여 다수의 요청을 비동기적으로 처리할 수 있다는 것도 큰 장점이었습니다. 이러한 이유로 RPC, 그 중에서도 가장 대표적인 gRPC를 선택하게 되었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;- BFF Pattern, Sam Newman: &lt;a href=&quot;https://samnewman.io/patterns/architectural/bff/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://samnewman.io/patterns/architectural/bff/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Microservice Patterns, Chris Richardson: &lt;a href=&quot;https://microservices.io/patterns/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://microservices.io/patterns/index.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>탐구 생활/gRPC&amp;amp;Python</category>
      <category>grpc</category>
      <category>Python</category>
      <category>python grpc</category>
      <category>python proto</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/76</guid>
      <comments>https://probehub.tistory.com/76#entry76comment</comments>
      <pubDate>Sat, 8 Mar 2025 22:00:33 +0900</pubDate>
    </item>
    <item>
      <title>FastAPI log (4) - 개선하기</title>
      <link>https://probehub.tistory.com/75</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 시리즈는 AWS EKS &amp;amp; FastAPI 환경에서 로그를 적용하는 과정을 다루고 있습니다. 전체 시리즈는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://probehub.tistory.com/manage/newpost/64&quot;&gt;&lt;span&gt;FastAPI&amp;nbsp;log&amp;nbsp;(1)&amp;nbsp;-&amp;nbsp;AWS&amp;nbsp;EKS&amp;nbsp;Fargate&amp;nbsp;환경에서&amp;nbsp;log&amp;nbsp;를&amp;nbsp;외부시스템에&amp;nbsp;보내기&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://probehub.tistory.com/65&quot;&gt;&lt;span&gt;FastAPI log (2) -AWS EKS Fargate, 왜 Fluent Bit 인가?&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://probehub.tistory.com/66&quot;&gt;FastAPI&amp;nbsp;log&amp;nbsp;(3)&amp;nbsp;-&amp;nbsp;설계,&amp;nbsp;구현&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://probehub.tistory.com/75&quot;&gt;FastAPI log (4) - 개선하기&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://probehub.tistory.com/66&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;이전 글&lt;/b&gt;&lt;/a&gt;에서 FsatAPI 의 request, stacktrace를 logging 하는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Middleware 를 만들었습니다. 하지만 이러한 구현 접근에는 결정적인 오류와 개선사항이 있습니다.&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;오류&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;결정적인 오류는 Middleware 에서 Exception 을 catch 하고 있다는 점입니다. &lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;FastAPI 의 기본 middleware chain 을 고려하지 않은 접근이기 때문에 문제가 됩니다. &lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;간단히 이야기하자면, FastAPI 에서는 처리 되지 않은 Exception 에 대한 책임을 다른 Middleware 에게 위임하고 있는데, 중간에 Logging 책임을 지는 Middleware 가 Exception 을 처리하고 있는 것이지요.&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(이에 대해서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://probehub.tistory.com/68#%EB%AC%B8%EC%A0%9C%EC%9D%98%20%EC%9B%90%EC%9D%B8-1&quot;&gt;CORS Middleware 에서 발생하는 문제를 분석&lt;/a&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;b&gt;따라서 Exception 을 처리하는 부분이 제외되어야 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;BsaeHTTPMiddelware 사용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Starlette 에서 제공하는 BsaeHttpMiddleware 는 성능상 불이익이 있습니다. 대신 &lt;a href=&quot;https://www.starlette.io/middleware/#pure-asgi-middleware&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Pure ASGI Middleware&lt;/a&gt; 로 구현되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORSMiddleware 문제를 해결하기 위해 &lt;a href=&quot;https://asgi.readthedocs.io/en/latest/specs/main.html#middleware&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ASGI Spec&lt;/a&gt; 을 공부한 저에게는 조금 기대되는 작업입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;확장성 부족&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 로깅 라이브러리 레퍼런스를 알아본다고 고작 python-json-logger 정도를 알아봤습니다. 하지만, 엘라스틱 서치에서 공통 로깅사양을 구현한 &lt;a href=&quot;https://github.com/elastic/ecs-logging-python&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;python 라이브러리&lt;/a&gt;가 있다는 것을 이제 막 발견했습니다. 이 코드를 침고하여 더 확장서있는 middleware 를 만들 수 있을 것 같습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결합도가 높음&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 버전은 middleware 자체의 기능보다는 전체 어플리케이션에서의 사용에 초점을 맞췄습니다. 그러한 이유로 여러 환경변수를 통해 logger 를 가져오고, stack_trace 를 출력하고 등의 행위를 제한했습니다. 그런데 이제보니 이러한 행위는 파이썬답지 못하며 middleware 에 집중된 행위가 아닙니다. 불필요한 외부 참조를 제거하고 middleware 내부에만 집중해야합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;개선 적용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전체 코드 결과물은 &lt;a href=&quot;https://github.com/timothy-jeong/fastapi-logging-example&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github&lt;/a&gt; 에서 확인할 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1740906093089&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import logging
import time
import uuid
import json

from starlette.types import ASGIApp, Message, Scope, Receive, Send

class JsonRequestLoggerMiddleware:
    def __init__(
        self, app: ASGIApp,
        error_info_name: str = &quot;error_info&quot;,
        error_info_mapping: dict[str, str] | None = None,
        event_id_header: str | None = None,
        client_ip_headers: list[str] | None = None,
        logger: logging.Logger | None = None,
    ) -&amp;gt; None:
        &quot;&quot;&quot;
        Initializes the JSON Request Logger Middleware.

        Args:
            app (ASGIApp): The ASGI application instance to wrap.
            error_info_name (str, optional): The key name in the request state from which to extract error information.
                Defaults to &quot;error_info&quot;.
            error_info_mapping (dict[str, str] | None, optional): A dictionary mapping error information keys (from the request
                state) to desired log field names. For example, {&quot;code&quot;: &quot;error_code&quot;, &quot;message&quot;: &quot;error_message&quot;}.
                Defaults to None.
            event_id_header (str | None, optional): The HTTP header name to extract an event ID from. If not provided or if the header
                is missing, a new UUID will be generated. Defaults to None.
            client_ip_headers (list[str] | None, optional): A list of HTTP header names to check for the client IP address,
                in order of priority. If none are provided, the client IP will be obtained from the scope's &quot;client&quot; value.
                Defaults to None.
            logger (logging.Logger | None, optional): A custom logger to use for logging requests. If not provided, a default
                logger with INFO level is created. Defaults to None.
        &quot;&quot;&quot;
        self.app = app
        self.error_info_name = error_info_name
        self.error_info_mapping = error_info_mapping or {
            &quot;code&quot;: &quot;error_code&quot;,
            &quot;message&quot;: &quot;error_message&quot;,
            &quot;stack_trace&quot;: &quot;stack_trace&quot;,
        }
        self.event_id_header = event_id_header
        self.client_ip_headers = client_ip_headers or [&quot;x-forwarded-for&quot;, &quot;x-real-ip&quot;]
        # logger setting
        if logger:
            self.logger = logger
        else:
            logger = logging.getLogger(&quot;request-logger&quot;)
            logger.setLevel(logging.INFO)
            if logger.hasHandlers():
                logger.handlers.clear()
            stream_handler = logging.StreamHandler()
            stream_handler.setLevel(logging.INFO)
            formatter = logging.Formatter(&quot;%(message)s&quot;)  
            stream_handler.setFormatter(formatter)
            logger.addHandler(stream_handler)
            logger.propagate = False
            self.logger = logger
            
    async def __call__(self, scope: Scope, receive: Receive, send: Send) -&amp;gt; None:
        if scope[&quot;type&quot;] != &quot;http&quot;:
            return await self.app(scope, receive, send)

        start_time = time.time()
        
        # parse header
        headers = {k.decode(&quot;latin1&quot;): v.decode(&quot;latin1&quot;) for k, v in scope.get(&quot;headers&quot;, [])}
        # event_id
        if self.event_id_header and self.event_id_header in headers:
            event_id = headers[self.event_id_header]
        else:
            event_id = str(uuid.uuid4())
            
        # extract client ip from client_ip_headers
        client_ip = None
        for header in self.client_ip_headers:
            if header in headers:
                # X-Forwarded-For case
                client_ip = headers[header].split(&quot;,&quot;)[0].strip()
                break
        if not client_ip:
            client_ip = scope.get(&quot;client&quot;, (&quot;unknown&quot;,))[0]
                        
        # default log data
        log_data = {
            &quot;timestamp&quot;: time.strftime(&quot;%Y-%m-%dT%H:%M:%S.%fZ&quot;, time.gmtime()),
            &quot;event_id&quot;: event_id,
            &quot;method&quot;: scope.get(&quot;method&quot;),
            &quot;path&quot;: scope.get(&quot;path&quot;),
            &quot;client_ip&quot;: client_ip,
            &quot;user_agent&quot;: headers.get(&quot;user-agent&quot;),
        }

        response_status_code = None

        async def send_wrapper(message: Message) -&amp;gt; None:
            nonlocal response_status_code
            if message[&quot;type&quot;] == &quot;http.response.start&quot;:
                response_status_code = message.get(&quot;status&quot;)
            await send(message)

        await self.app(scope, receive, send_wrapper)
        
        time_taken_ms = int((time.time() - start_time) * 1000)
        
        # based on response status_code
        if response_status_code is None:
            response_status_code = 500
        log_type = &quot;access&quot; if response_status_code &amp;lt; 400 else &quot;error&quot;
        log_level = &quot;ERROR&quot; if response_status_code &amp;gt;= 400 else &quot;INFO&quot;
                
        # error log data
        
        error_info = scope.get(&quot;state&quot;, {}).get(self.error_info_name, None)
        if not error_info:
            log_data.update({&quot;error&quot;: None})
        if error_info:
            log_data.update({&quot;error&quot;: {}})
            for src_key, dest_key in self.error_info_mapping.items():
                log_data[&quot;error&quot;][dest_key] = error_info.get(src_key)            
        
        # log data update
        log_data.update({
            &quot;time_taken_ms&quot;: time_taken_ms,
            &quot;status_code&quot;: response_status_code,
            &quot;log_type&quot;: log_type,
            &quot;level&quot;: log_level,
        })     
        
        log_level_int = logging.getLevelNamesMapping()[log_level]
        self.logger.log(log_level_int, json.dumps(log_data, ensure_ascii=False))&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI Tips: &lt;a href=&quot;https://github.com/Kludex/fastapi-tips&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Kludex/fastapi-tips&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ecs-logging: &lt;a href=&quot;https://github.com/elastic/ecs-logging-python&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/elastic/ecs-logging-python&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>탐구 생활/FastAPI Log</category>
      <category>fastapi json</category>
      <category>fastapi logging</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/75</guid>
      <comments>https://probehub.tistory.com/75#entry75comment</comments>
      <pubDate>Sun, 2 Mar 2025 15:38:39 +0900</pubDate>
    </item>
    <item>
      <title>SQLAlchemy read-only session</title>
      <link>https://probehub.tistory.com/74</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cd3cSL/btsPaMqXa1w/qA7hwq77usxua8KwKWVMj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cd3cSL/btsPaMqXa1w/qA7hwq77usxua8KwKWVMj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cd3cSL/btsPaMqXa1w/qA7hwq77usxua8KwKWVMj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcd3cSL%2FbtsPaMqXa1w%2FqA7hwq77usxua8KwKWVMj1%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;520&quot; height=&quot;260&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;SQLAlchemy Readonly 에 대한 질문&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/79439390/does-sqlalchemy-automatically-create-a-transaction-for-read-only-operations/79448689#79448689&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SQLAlchemy 도 Spring의 Transaction 처럼 read-only 를 만드는 기능이 있는지 물어보는 질문&lt;/a&gt;이 있었습니다. 이 글의 답변에도 작성했지만, 저도 SpringBoot 를 사용할때는 Transaction 격리수준, 전파수준 등을 엄청 신경썼었는데 FastAPI&amp;amp;SQLAlchemy 를 사용하면서 한번도 이런 고민을 안했습니다.&lt;/p&gt;
&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;이라고 생각하면서 SQLAlchemy 에서 read-only transaction 을 만드는 방법을 알아보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1740213156290&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;Does SQLAlchemy automatically create a transaction for read-only operations?&quot; data-og-description=&quot;I am currently learning backend development using FastAPI + SQLAlchemy, and I have a question regarding transaction handling in SQLAlchemy. While reading the official SQLAlchemy documentation, I came&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/79439390/does-sqlalchemy-automatically-create-a-transaction-for-read-only-operations/79448689#79448689&quot; data-og-url=&quot;https://stackoverflow.com/questions/79439390/does-sqlalchemy-automatically-create-a-transaction-for-read-only-operations&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ciMlFs/hyYjAVpxzR/z6ys4E9Ekb1tch7KmEjUA1/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/79439390/does-sqlalchemy-automatically-create-a-transaction-for-read-only-operations/79448689#79448689&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/79439390/does-sqlalchemy-automatically-create-a-transaction-for-read-only-operations/79448689#79448689&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ciMlFs/hyYjAVpxzR/z6ys4E9Ekb1tch7KmEjUA1/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&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;Does SQLAlchemy automatically create a transaction for read-only operations?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I am currently learning backend development using FastAPI + SQLAlchemy, and I have a question regarding transaction handling in SQLAlchemy. While reading the official SQLAlchemy documentation, I came&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;&lt;b&gt;왠만한 고민에 대한 해답은(특히 소프트웨어 개발 영역에서) 인터넷에 있기 마련&lt;/b&gt;입니다. 보통 stackoverflow 에 질문을 올리는 사람들은 인터넷에서 답을 찾지 못한 경우가 많은데 이번 질문은 github issue 를 찾아봤다면 답을 알 수 있는 질문이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQLAlchemy issue 에 비슷한 질문이 있었고, maintainer 는 공식적으로 read-only transaction 을 만드는 방법이 제공되진 않지만 SQLAlchemy 에서 제공하는 &lt;a href=&quot;https://docs.sqlalchemy.org/en/20/orm/events.html#sqlalchemy.orm.SessionEvents&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;events&lt;/a&gt; 를 이용해서 충분히 구현이 가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740213077796&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from sqlalchemy import create_engine, Column, Integer, String, event
from sqlalchemy.orm import Session, sessionmaker, declarative_base

# Define the base and ORM model.
Base = declarative_base()

class User(Base):
    __tablename__ = &quot;users&quot;
    id = Column(Integer, primary_key=True)
    name = Column(String)
    
    def __repr__(self):
        return self.name

# Utility functions to mark a session as read-only.
def set_read_only(session: Session, read_only: bool = True):
    session.info['read_only'] = read_only

def is_read_only(session: Session) -&amp;gt; bool:
    return session.info.get('read_only', False)

# Listen for before_flush event and prevent flushing if session is read-only.
@event.listens_for(Session, &quot;before_flush&quot;)
def before_flush(session, flush_context, instances):
    if is_read_only(session):
        raise Exception(&quot;Read-only session: flush not allowed&quot;)

# Setup the database and create a session.
if __name__ == &quot;__main__&quot;:
    engine = create_engine(&quot;sqlite:///:memory:&quot;)
    SessionLocal = sessionmaker(bind=engine)
    
    # Create tables.
    Base.metadata.create_all(engine)
    
    # Get Session
    session = SessionLocal()
    
    # normal Session ()
    new_user = User(name=&quot;Alice&quot;)
    session.add(new_user)

    users = session.query(User).all()
    print(&quot;Users:&quot;, users)    
    session.commit()    
    
    # read-only Session ()
    set_read_only(session, True)  # Mark session as read-only
    
    new_user = User(name=&quot;James&quot;)
    session.add(new_user)
    
    users = session.query(User).all()
    print(&quot;Users:&quot;, users)    
    session.commit()&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;Spring 에서는 read-only Transaction 을 만들어서 약간의 성능상 이점을 얻을 수도 있었지만, SQLAlchemy 에서는 그런 성능상 이점을 기대할 수 있을지는 모르겠습니다.&lt;/p&gt;</description>
      <category>탐구 생활/개발 탐구</category>
      <category>sqlalchemy read-only</category>
      <category>sqlalchemy read-only session</category>
      <category>sqlalchemy readonly</category>
      <category>sqlalchemy readonly session</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/74</guid>
      <comments>https://probehub.tistory.com/74#entry74comment</comments>
      <pubDate>Sat, 22 Feb 2025 17:33:24 +0900</pubDate>
    </item>
    <item>
      <title>FastAPI ErrorHandling 과 CORS (3) - 문제 해결</title>
      <link>https://probehub.tistory.com/73</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://probehub.tistory.com/68&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;FastAPI 에서 ErrorHandling 하는 과정에서 CORS 문제가 발생하는 이슈와 그 원인&lt;/a&gt;&lt;/b&gt;을 그리고 그에 따른 &lt;b&gt;&lt;a href=&quot;https://probehub.tistory.com/69&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;사람들의 대응&lt;/a&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;그리고 저는 문제의 원인이 Starlette에 있다고 생각했고, 나름대로 연구하여 Starlette codebase 에 코드 3줄을 추가하여 이러한 문제를 해소할 수 있는 방법을 찾았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당장이라도 Starlette 에 PR 을 올리고 싶었지만, 의외로 다른 사람들의 더 나은 대안을 가지고 있거나 FastAPI 나 Starlette 에서 이미 관련 이슈를 해소할 계획일 있을지도 모르기 때문에 우선 discussion 을 이용하기로 했습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;FastAPI Discussion&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[&lt;a href=&quot;https://github.com/fastapi/fastapi/discussions/13398&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;discussion link&lt;/a&gt;]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제가 FastAPI 에 문서화 될 계획이 있다면 당장 급한 이슈는 해소될 것으로 보여서 물어봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 FastAPI 팀은 문서화 계획은 없으며, Starlette 에 관련 제안을 해달라는 답볃을 받았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;FastAPI Discussion.png&quot; data-origin-width=&quot;1910&quot; data-origin-height=&quot;1478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dTYD2A/btsMuepWY7i/qgEhHS2SzIK7kmcBkxKxuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dTYD2A/btsMuepWY7i/qgEhHS2SzIK7kmcBkxKxuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTYD2A/btsMuepWY7i/qgEhHS2SzIK7kmcBkxKxuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdTYD2A%2FbtsMuepWY7i%2FqgEhHS2SzIK7kmcBkxKxuK%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;720&quot; height=&quot;557&quot; data-filename=&quot;FastAPI Discussion.png&quot; data-origin-width=&quot;1910&quot; data-origin-height=&quot;1478&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Starlette Discussion&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[&lt;a href=&quot;https://github.com/encode/starlette/discussions/2876&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;discussion link&lt;/a&gt;]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Starlette 에는 codebase 변경과 문서화를 함께 제안해보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 제가 문제가 있다고 생각했던 방식이 사실은 Starlette 의 공식적인 접근이라는 답변을 받았습니다. (FastAPI 의 maintainer 와 동일한 사람입니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-02-22 오후 11.13.21.png&quot; data-origin-width=&quot;1860&quot; data-origin-height=&quot;668&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rjACp/btsMtbniaHS/SO8c5JfjyLI3snJB9GyiM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rjACp/btsMtbniaHS/SO8c5JfjyLI3snJB9GyiM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rjACp/btsMtbniaHS/SO8c5JfjyLI3snJB9GyiM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrjACp%2FbtsMtbniaHS%2FSO8c5JfjyLI3snJB9GyiM0%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;720&quot; height=&quot;259&quot; data-filename=&quot;스크린샷 2025-02-22 오후 11.13.21.png&quot; data-origin-width=&quot;1860&quot; data-origin-height=&quot;668&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 Starlette 진영에서 해당 문제에 대한 공식적인 접근방법이 있다고 말을 한 이상 내가 코드베이스를 바꾸는것은 어려운일이라고 생각됩니다. 다만 Starlette 공식문서에 해당 내용을 추가할 순 있을 것 같습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제해결:Starlette 문서추가&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Starlette 공식문서에 해당 내용을 추가할 수 있었습니다. &lt;a href=&quot;https://github.com/encode/starlette/commit/708930c4a3a62f9674cb08f3ed776039a5c5b8ce&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RP#2885&lt;/a&gt;, 개발자로서 직접 소스코드에 기여하고 싶은 욕심이 있어서 &lt;a href=&quot;https://www.starlette.io/middleware/#corsmiddleware-global-enforcement&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서로&lt;/a&gt;만 끝난게 아쉽지만, Starlette 과 FastAPI 생태계를 더 잘 알게된 계기가 된것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;starlette result.png&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;717&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GPrb6/btsMTLawTeQ/khy0qld68C3HNFc2QsNtWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GPrb6/btsMTLawTeQ/khy0qld68C3HNFc2QsNtWK/img.png&quot; data-alt=&quot;공식문서에 추가된 내용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GPrb6/btsMTLawTeQ/khy0qld68C3HNFc2QsNtWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGPrb6%2FbtsMTLawTeQ%2Fkhy0qld68C3HNFc2QsNtWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;705&quot; height=&quot;629&quot; data-filename=&quot;starlette result.png&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;717&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공식문서에 추가된 내용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>탐구 생활/FastAPI CORS</category>
      <category>fastapi cors</category>
      <category>fastapi starlette</category>
      <category>starlette cors</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/73</guid>
      <comments>https://probehub.tistory.com/73#entry73comment</comments>
      <pubDate>Sat, 22 Feb 2025 17:08:57 +0900</pubDate>
    </item>
    <item>
      <title>티스토리 스킨 hELLO 에 기여해보기</title>
      <link>https://probehub.tistory.com/71</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;hELLO discussion.png&quot; data-origin-width=&quot;1858&quot; data-origin-height=&quot;1856&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tDIba/btsMnKc1PRN/p1i6XiwZkQdTpDPnWWNDkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tDIba/btsMnKc1PRN/p1i6XiwZkQdTpDPnWWNDkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tDIba/btsMnKc1PRN/p1i6XiwZkQdTpDPnWWNDkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtDIba%2FbtsMnKc1PRN%2Fp1i6XiwZkQdTpDPnWWNDkK%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;650&quot; height=&quot;649&quot; data-filename=&quot;hELLO discussion.png&quot; data-origin-width=&quot;1858&quot; data-origin-height=&quot;1856&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://probehub.tistory.com/60&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; 이전에 FatAPI 의 문서 번역에 기여한것&lt;/a&gt;에 이어서 FastAPI 의 공식문서에 내용을 변경하는 등 기여를 하고 있는중이다.&lt;/p&gt;
&lt;figure id=&quot;og_1739965670755&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;오픈소스 초보자의 FastAPI 기여하기&quot; data-og-description=&quot;회사에서 FastAPI 로 전환을 하기에 FastAPI Document 를 보면서 공부중이었다. 한국어로 번역된 문서도 있는반면 영어 그대로인 문서들도 있었다. 왜일까? 하고 찾아보던중 FastAPI Github&amp;nbsp;에서 번역활동&quot; data-og-host=&quot;probehub.tistory.com&quot; data-og-source-url=&quot;https://probehub.tistory.com/60&quot; data-og-url=&quot;https://probehub.tistory.com/60&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/7OSYD/hyYjFB3Qhv/Pkk7FQ2S5PEL8EqkosOAs0/img.png?width=800&amp;amp;height=696&amp;amp;face=0_0_800_696,https://scrap.kakaocdn.net/dn/c4V6y0/hyYf5JcDK0/wkBnyC4bvzGas8KdkhgAO1/img.png?width=800&amp;amp;height=696&amp;amp;face=0_0_800_696,https://scrap.kakaocdn.net/dn/btTSOW/hyYfRxs8kc/TnuD8mns72OPmQib24HPSK/img.png?width=963&amp;amp;height=838&amp;amp;face=0_0_963_838&quot;&gt;&lt;a href=&quot;https://probehub.tistory.com/60&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://probehub.tistory.com/60&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/7OSYD/hyYjFB3Qhv/Pkk7FQ2S5PEL8EqkosOAs0/img.png?width=800&amp;amp;height=696&amp;amp;face=0_0_800_696,https://scrap.kakaocdn.net/dn/c4V6y0/hyYf5JcDK0/wkBnyC4bvzGas8KdkhgAO1/img.png?width=800&amp;amp;height=696&amp;amp;face=0_0_800_696,https://scrap.kakaocdn.net/dn/btTSOW/hyYfRxs8kc/TnuD8mns72OPmQib24HPSK/img.png?width=963&amp;amp;height=838&amp;amp;face=0_0_963_838');&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;오픈소스 초보자의 FastAPI 기여하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;회사에서 FastAPI 로 전환을 하기에 FastAPI Document 를 보면서 공부중이었다. 한국어로 번역된 문서도 있는반면 영어 그대로인 문서들도 있었다. 왜일까? 하고 찾아보던중 FastAPI Github&amp;nbsp;에서 번역활동&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;probehub.tistory.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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI 에서 기여하는 흐름은 이런식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[Fork 를 한다] -&amp;gt; [적절한 이름의 Branch 를 만든다] -&amp;gt; [변경하고자하는 사항을 변경하고 PR을 올린다] -&amp;gt; [리뷰를 받는다] -&amp;gt; [머지 혹은 리젝된다]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이번에 티스토리 hELLO 에 기여했던 흐름은 전혀 달랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 hELLO 에 기여하게 된 계기는 완전히 개인적인 이유다. 이 티스토리에 hELLO 스킨을 적용중이다. 그리고 나는 에디터에 쓰여진 글이 적절히 구조화되었는지를 본 후 &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;그런데 ul, ol 태그 요소가 에디터에서는 들여쓰기가 되었지만 실제 발행된 글에서는 들여쓰기가 되지 않는 것을 발견한 것이다. 이 문제를 해결하는건 간단했다. 웹브라우저에서 개발자 도구를 열고 관련 태그를 분석한 후 CSS 요소를 넣으면 해결이다.&lt;/p&gt;
&lt;pre id=&quot;code_1739963288213&quot; class=&quot;css&quot; data-ke-language=&quot;css&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* 글 내용에 포함된 ul의 들여쓰기 적용 */
ul[data-ke-list-type=&quot;disc&quot;] {
  list-style-type: disc;
  padding-left: 1.5rem;
}

ol[data-ke-list-type=&quot;decimal&quot;] {
  list-style-type: decimal;
  padding-left: 1.5rem;
}&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;티스토리 스킨 편집에서 이걸 적용하고 만족하던 중 문득 나와 같은 이유로 고생하는 사람이 있지 않을까? 하는 생각이 들었다. 그리고 깃헙을 찾아보니 hELLO 저자분이 운영중인 리포지토리를 발견할 수 있었다.&lt;/p&gt;
&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;[ISSUE에 글을 쓴다] -&amp;gt; [Discussion 으로 옮겨진다] -&amp;gt; [저자분이 나중에 반영될것이라고 한다]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 가볍게 반영되는 오픈소스도 있다는 사실이 새로웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그 과정이 쉽든 어렵든 어떻든 다른 사람들에게 도움이 되었다는 사실은 변함 없으니 오히려 좋다.&lt;/p&gt;</description>
      <category>탐구 생활/개발 탐구</category>
      <category>hello 기여해보기</category>
      <category>가볍게 오픈소스</category>
      <category>오픈소스</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/71</guid>
      <comments>https://probehub.tistory.com/71#entry71comment</comments>
      <pubDate>Wed, 19 Feb 2025 20:10:02 +0900</pubDate>
    </item>
    <item>
      <title>FastAPI &amp;amp; Postgres 로 multi-tenancy 구현하기</title>
      <link>https://probehub.tistory.com/70</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;fastapi-multi-tenant-race-condition.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpQywI/btsMkdFOD2i/6Uj4YeZhJFTkdqaxeRaKYk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpQywI/btsMkdFOD2i/6Uj4YeZhJFTkdqaxeRaKYk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpQywI/btsMkdFOD2i/6Uj4YeZhJFTkdqaxeRaKYk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpQywI%2FbtsMkdFOD2i%2F6Uj4YeZhJFTkdqaxeRaKYk%2Fimg.webp&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;650&quot; height=&quot;650&quot; data-filename=&quot;fastapi-multi-tenant-race-condition.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stackoverflow 를 돌아다니다가 &lt;b&gt;&lt;a href=&quot;https://stackoverflow.com/questions/79435884/fastapi-middleware-for-postgres-multi-tenant-schema-switching-causes-race-condit/79443321#79443321&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&quot;FastAPI multi-tenant 를 구현하는데 경쟁조건이 발생한다&quot;&lt;/a&gt;&lt;/b&gt; 는 내용의 질문을 발견했습니다. 회사에서 FastAPI 에서 PostgreSQL 의 schema 단위로 tenant 를 구분하여 DB 에 연결하는 기능을 구현했는데, 생각보다 여기서 어려움을 겪는 사람이 있는것 같아서 내용을 정리해봤습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;우선 저는 질문자가 구현한 middleware 를 살펴봤습니다. 큰 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/ko/3.13/library/contextvars.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ContextVar&lt;/a&gt; 를 이용한다.&lt;/li&gt;
&lt;li&gt;Request 가 있을때마다 SessionLocal 에서 session 을 얻어온다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;얻어온 session 에서 switch schema 를 실행하고 request.state 에 db 라는 이름으로 넘겨준다.&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 주어진 코드로만 보면 왜 ContextVar  로 current_schema 정보를 저장하는지 의문이지만 &lt;b&gt;ContextVar 자체는 비동기 프렘워크에서 로컬 변수를 저장&lt;/b&gt;하는데 사용되며, FastAPI 에서는 request 마다 격리되어 있기 때문에 경쟁조건을 만드는 원인이 아닐것으로 보았습니다. 그래서 주목할만한 특이한 사항은 &lt;b&gt;session 을 request.state 로 넘겨준다는 것&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;백엔드 어플리케이션에서는 DB와의 연결이 격리되는것이 중요합니다. 그러한 이유로 SpringBoot 도 Transaction 마다 Connection 을 따로 만들고, FastAPI도 공식문서에서 database session 에 대해서는 yield 와 Depends 사용을 권장하고 있지요. 특히 ASGI 를 염두에둔 FastAPI 에서는 DB Session 의 격리는 더 중요한 사항일 것입니다.&lt;/p&gt;
&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;middleware 수준에서 생성한 session 을 request.state 에 넘겨주는것이 DB Session 을 각 요청별로 격리&lt;/b&gt;할 수 있을지 파악해보면 될것 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739731010806&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# ...
from typing import Optional, Callable
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from app.db.session import SessionLocal, switch_schema
from contextvars import ContextVar

# Point1: ContextVar 를 이용한다.
current_schema: ContextVar[str] = ContextVar(&quot;current_schema&quot;, default=&quot;public&quot;)

class SchemaSwitchMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next: Callable) -&amp;gt; Response:
    	# Point2: 모든 요청마다 SessionLocal 에서 Session 을 얻어온다.
        db = SessionLocal()  # Create a session here
        try:
            tenant_id: Optional[str] = request.headers.get(&quot;X-Tenant-ID&quot;)

            if tenant_id:
                # tenant_id 를 schem_name 으로 변환하는 로직
                except Exception as e:
                # exception 처리
            else:
                schema_name = &quot;public&quot;

            current_schema.set(schema_name)
            # Point3: middleware 수준에서 schema 를 변경하고 session을 request.state 에 넣는다.
            switch_schema(db, schema_name)
            request.state.db = db  # request state 에 session 저장

            response = await call_next(request)
            return response

        except Exception as e:
        	# Exception 처리
        finally:
            switch_schema(db, &quot;public&quot;)
            db.close()&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제의 원인&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 눈치챘겠지만, middleware 수준에서 생성한 session 을 request.state 에 넘겨주는것이 DB Session 을 각 요청별로 격리 할 수 있다는 &lt;b&gt;보장이 없습니다.&lt;/b&gt; 특히 위에 구현된 middleware 의 경우에는 middleware 수준에서 commit 을 한번 하여 상태를 변화시키고 있기 때문에 더욱 문제가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 SQLAlchemy 를 이용해 Engine 을 만들고 SessionLocal 을 만들때 Connection Pool 을 이용하게 됩니다. 매 요청마다 pool 에서 session 을 얻어올 것이고, 기존 session이 제대로 관리되지 않는다면 이미 schema 변경이 일어난 session 에 대해서 다른 request 가 다시 schema 를 변경시킬 수 있는 가능성이 생깁니다. 명확하지 않지만 그럴 &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;middleware 에서 만든 DB session 을 middleware 와 application 모든 곳에서 적절히 관리할 수 있다면 이러한 &lt;b&gt;여지&lt;/b&gt;가 없겠지만 인간은 실수하기 나름입니다. Framework 수준에서 제공해주는 Depends 를 이용하는게 더 적절한 선택이 될 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;아래와 같이 FastAPI의 Depends 안에서 Session 의 수명주기를 처리하면 큰 문제가 없을 것입니다:&lt;/p&gt;
&lt;pre id=&quot;code_1739733083288&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker, declarative_base, Session
from app.core.logger import logger
from app.core.config import settings
from typing import Annotated
from fastapi import Header

# Base for models
Base = declarative_base()

DATABASE_URL = settings.DATABASE_URL

# SQLAlchemy engine
engine = create_engine(
    DATABASE_URL,
    pool_pre_ping=True,
    pool_size=20,
    max_overflow=30,
)

# Session factory
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# TODO: use this get_db_session function in path operation.
def get_db_session(tenant_id: Annotated[str, Header(alias=&quot;X-Tenant-ID&quot;)]) -&amp;gt; Generator[Session, None, None]:
    session = SessionLocal()
    try:
        # TODO: Implement tenant_id to tenant_schema here
        session.execute(text(f&quot;SET search_path TO {tenant_id};&quot;))
        session.commit()  # Ensure the schema change is applied immediately
        yield session
    finally:
        session.close()&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;</description>
      <category>탐구 생활/개발 탐구</category>
      <category>fastapi db 경쟁조건</category>
      <category>fastapi multi-tenant</category>
      <category>fastapi mutitenant</category>
      <category>fastapi postgresql tenant</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/70</guid>
      <comments>https://probehub.tistory.com/70#entry70comment</comments>
      <pubDate>Mon, 17 Feb 2025 08:26:51 +0900</pubDate>
    </item>
    <item>
      <title>FastAPI ErrorHandling 과 CORS (2) - 대안 파악</title>
      <link>https://probehub.tistory.com/69</link>
      <description>&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;fastapi-cors.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zGux3/btsMjeL9cbR/MjQs7ttXebyZoGoDpfHblK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zGux3/btsMjeL9cbR/MjQs7ttXebyZoGoDpfHblK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zGux3/btsMjeL9cbR/MjQs7ttXebyZoGoDpfHblK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzGux3%2FbtsMjeL9cbR%2FMjQs7ttXebyZoGoDpfHblK%2Fimg.webp&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;360&quot; height=&quot;360&quot; data-filename=&quot;fastapi-cors.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Starlette 와 FastAPI 의 ISSUE(&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://github.com/encode/starlette/issues/617&quot;&gt;link&lt;/a&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;/span&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://github.com/fastapi/fastapi/discussions/8027&quot;&gt;link&lt;/a&gt;) 에서는 다음의 해결법이 논의 되었습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;논의된 해결법&lt;/b&gt;&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;FastAPI 앱을 CORSMiddleware로 감싸기&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;from:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://github.com/thomasleveil&quot;&gt;thomasleveil&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739612461806&quot; class=&quot;python&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from starlette.applications import Starlette
from starlette.authentication import AuthenticationBackend, AuthenticationError
from starlette.exceptions import HTTPException
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import JSONResponse

import uvicorn

class DumbAuthBackend(AuthenticationBackend):
    async def authenticate(self, request):
        raise AuthenticationError(&quot;You will never be authenticated!&quot;)

app = Starlette(debug=True)

# Global authentication
app.add_middleware(AuthenticationMiddleware, backend=DumbAuthBackend())

@app.route('/')
async def index(request):
    return JSONResponse({&quot;hello&quot;: &quot;world&quot;})

if __name__ == '__main__':
    uvicorn.run(
        CORSMiddleware(app=app, allow_origins=[&quot;*&quot;]),
        host='0.0.0.0',
        port=8000
    )&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;CORSMiddleware가 자체적으로 ASGI 앱이므로 FastAPI 앱을 감싸도록 구성합니다. 이를 통해 outermost middleware 가 CORSMiddleware 가 되기 때문에 CORS 헤더 문제를 자연스럽게 해결하게 됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;평가&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;✅ 장점: 비교적 간단하게 적용 가능&lt;/li&gt;
&lt;li&gt;⚠️ 단점: 초보자에게는 이러한 wrapping 개념이 이해하기 어려울 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 문제를 해결하는 것은 Starlette App 구성이 갖는 문제를 또다른 ASGI App 을 만들어서 해결하는 것으로 뭔가 와닿지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;차라리 Starlette 측에서 CORSMiddleware 를 추가하는 새로운 방법을 제공하는게 맞을 것입니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ExceptionHandler에서 직접 CORS 헤더 추가&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;from:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://github.com/ddahan&quot;&gt;ddahan&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739612461808&quot; class=&quot;python&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def add_cors(request: Request, response: JSONResponse) -&amp;gt; JSONResponse:
    &quot;&quot;&quot;
    CORS headers are not automatically added to error handlers with FastAPI
    cf. https://github.com/fastapi/fastapi/discussions/8027
    &quot;&quot;&quot;
    cors_headers = {
        &quot;Access-Control-Allow-Methods&quot;: &quot;, &quot;.join(settings.CORS_ALLOW_METHODS),
        &quot;Access-Control-Allow-Headers&quot;: &quot;, &quot;.join(settings.CORS_ALLOW_HEADERS),
        &quot;Access-Control-Allow-Credentials&quot;: str(settings.CORS_ALLOW_CREDENTIALS).lower(),
    }

    # If the origin is in the allowed list, add it to the CORS headers
    origin = request.headers.get(&quot;origin&quot;)
    if origin in settings.CORS_ALLOW_ORIGIN:
        cors_headers[&quot;Access-Control-Allow-Origin&quot;] = origin

    response.headers.update(cors_headers)
    return response

def internal_exception_handler(request: Request, exc: Exception) -&amp;gt; JSONResponse:
    response = JSONResponse(
        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
        content={
            &quot;errors&quot;: {
                &quot;general&quot;: [
                    &quot;An unknown error has occurred. Please contact an administrator.&quot;
                ]
            }
        },
    )
    return add_cors(request, response)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;settings 로 CORS 관련 세팅을 전역적으로 관리하려고 하는 시도가 돋보입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExceptionHandler 는 ServerErrorMiddleware 와 ExceptionMiddleware 에 적용되기 때문에 CORSMiddleware 를 우회하여 반환되는 이유로 CORS 헤더가 누락되는 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;평가&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;✅ 장점: 원하는 예외 상황에서 정확하게 CORS 헤더를 제어할 수 있습니다.&lt;/li&gt;
&lt;li&gt;⚠️ 단점: 전역적으로 처리해야 할 CORS 헤더를 개별 ExceptionHandler에서 설정하는 것은 비효율적이며 비표준적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비록 CORS 관련 세팅을 전역적으로 처리하려고 했으나 본질적으로 예외 처리 시 응답에 CORS 헤더를 수동으로 추가하는 것입니다. CORS에 처리는 Applciation 전역적으로 처리되어야 하는데, 지엽적인 처리에 머물러 있는것 같아 아쉽습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지극히 개인적인 의견입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CORS Middleware&lt;/b&gt; 로 FastAPI application 을 감싸는 행위는 이해하기 어려습니다. 과연 라이브러리 수준에서 정식으로 권장하는 문제라고 받아들일 수 있을까요?&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한 &lt;b&gt;ExceptionHandler 에서 직접 header 를 추가&lt;/b&gt;하는것도 이상합니다. 일부 경우를 제외하고 CORS 에 대한 대응은 application 전역적인 처리가 되어야 하기 때문입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;제 3의 해법이 필요해 보입니다, 그리고 이 문제를 해결하는 과정은 &lt;a href=&quot;https://probehub.tistory.com/73&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&quot;FastAPI ErrorHandling 과 CORS (3) - 문제 해결&quot;&lt;/a&gt; 에 정리했습니다.&lt;/p&gt;</description>
      <category>탐구 생활/FastAPI CORS</category>
      <category>fastapi cors</category>
      <category>fastapi exception cors</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/69</guid>
      <comments>https://probehub.tistory.com/69#entry69comment</comments>
      <pubDate>Sat, 15 Feb 2025 18:48:12 +0900</pubDate>
    </item>
    <item>
      <title>FastAPI ErrorHandling 과 CORS (1) - 문제 파악</title>
      <link>https://probehub.tistory.com/68</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;fastapi-and-cors.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdMyv3/btsMb07zskz/YBfawEoAzWwf6ZYrFQzHK1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdMyv3/btsMb07zskz/YBfawEoAzWwf6ZYrFQzHK1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdMyv3/btsMb07zskz/YBfawEoAzWwf6ZYrFQzHK1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdMyv3%2FbtsMb07zskz%2FYBfawEoAzWwf6ZYrFQzHK1%2Fimg.webp&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;360&quot; height=&quot;360&quot; data-filename=&quot;fastapi-and-cors.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Starlette와 FastAPI에서 제공하는 CORSMiddleware와 ExceptionHandler가 함께 작동하지 않는 상황은 꽤 혼란스럽습니다. 이는 REST API 개발자 입장에서 상당히 중요한 이슈로, 많은 FastAPI 사용자들이 다양한 해결책을 논의(&lt;a href=&quot;https://github.com/encode/starlette/issues/617&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;link&lt;/a&gt;, &lt;a href=&quot;https://github.com/encode/starlette/issues/1175&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;link&lt;/a&gt;, &lt;a href=&quot;https://github.com/fastapi/fastapi/discussions/8027&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;link&lt;/a&gt;)해 왔지만 제가 봤을때 명확하고 만족스러운 답이 나오지 않은 상태입니다. 우선 문제의 원인과 그 해법이 만족스럽지 않은 이유를 이야기해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다소 장황한 글이 될 수 있기 때문에 문제 원인과 문제 상황만 파악하고자 하시는 분은 &lt;a href=&quot;https://probehub.tistory.com/68#%EB%AC%B8%EC%A0%9C%EC%9D%98%20%EC%9B%90%EC%9D%B8-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&quot;문제의 원인&quot;&lt;/a&gt; 그리고 &lt;a href=&quot;https://probehub.tistory.com/68#%EB%AC%B8%EC%A0%9C%20%EC%83%81%ED%99%A9-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&quot;문제 상황&quot;&lt;/a&gt; 만 보셔도 됩니다.&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;Starlette&amp;nbsp; Middlewere chain&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Strlette 는 ASGI Spec 을 준수하며, 그에 따라 &lt;a href=&quot;https://asgi.readthedocs.io/en/latest/specs/main.html#middleware&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Middleware&lt;/a&gt; 라는 개념이 적용되어 있습니다. 그리고 이러한 Middleware 들이 요청, 응답 과정에서 연쇄적으로 동작하는 것을 Middleware chain 이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI application 은 Starlette application 을 더 사용하기 편하게 포장한것이고, 대부분의 주요 철학과 구현은 Starlette에 의존하고 있기 때문에 Sarlette의 Middleware chain 을 분석하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 Middleware&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Starlette 의 middleware chain 에서 cors 문제가 발생하는 문제를 이해하기위해 다음의 개념을 알아야 한비다.&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;Middleware:&lt;/b&gt; 요청이 Starlette 애플리케이션에 도달하기 전과 응답이 반환된 후에 개입하여 처리합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CORSMiddleware:&lt;/b&gt; 사용자가 설정한 CORS 정책에 따라 preflight 요청을 검증하고 응답에 CORS 헤더를 추가합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ExceptionMiddleware&lt;/b&gt;: 특정 status_code 혹은 Exception 대해서 어떻게 대응할지 설정합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ServerErrorMiddleware&lt;/b&gt;: ExceptionMiddleware 에서 처리되지 않은 나머지 Exception 에 대응합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ExceptionHandler:&lt;/b&gt; 예외가 발생했을 때 이를 처리하는 방법을 정의합니다. ExceptionMiddleware 에 등록됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HttpException:&lt;/b&gt; Starlette와 FastAPI에서 정의된 예외 클래스로, 상태 코드와 헤더 정보를 포함할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Starlette 코드 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 직접 Starlette application 에 대한 코드를 보겠습니다. 이를 통해&amp;nbsp;&lt;b&gt;ExceptionMiddleware&lt;/b&gt; 와 &lt;b&gt;ServerErrorMiddleware&lt;/b&gt; 는 자동으로 Startlette application 이 최초 요청을 받을때 초기화 되어(Lazy) 이후에는 캐싱되어 재사용되는 구조를 가지고 있다는 사실을 알 수 있습니다.&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;또한 ExceptionHandler 에 등록된 Exception 에 대한 처리는 ServerErrorMiddleware 에 할당된다는 사실도 파악이 가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739609821357&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# starlette/applications.py

class Starlette:
    &quot;&quot;&quot;Creates an Starlette application.&quot;&quot;&quot;

    def __init__(...)
    
    # line 79
    def build_middleware_stack(self) -&amp;gt; ASGIApp:
        debug = self.debug
        error_handler = None
        exception_handlers: dict[typing.Any, typing.Callable[[Request, Exception], Response]] = {}

        for key, value in self.exception_handlers.items():
            if key in (500, Exception):
                error_handler = value
            else:
                exception_handlers[key] = value

        middleware = (
        	# outermost
            [Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)]
            + self.user_middleware
            + [Middleware(ExceptionMiddleware, handlers=exception_handlers, debug=debug)]
            # innermost
        )

        app = self.router
        for cls, args, kwargs in reversed(middleware):
            app = cls(app, *args, **kwargs)
        return app

    # line 108
    async def __call__(self, scope: Scope, receive: Receive, send: Send) -&amp;gt; None:
        scope[&quot;app&quot;] = self
        if self.middleware_stack is None:
            self.middleware_stack = self.build_middleware_stack()
        await self.middleware_stack(scope, receive, send)
        
    # line 123 
    def add_middleware(
        self,
        middleware_class: _MiddlewareFactory[P],
        *args: P.args,
        **kwargs: P.kwargs,
    ) -&amp;gt; None:
        if self.middleware_stack is not None:  # pragma: no cover
            raise RuntimeError(&quot;Cannot add middleware after an application has started&quot;)
        self.user_middleware.insert(0, Middleware(middleware_class, *args, **kwargs))

    # line 133
    def add_exception_handler(
        self,
        exc_class_or_status_code: int | type[Exception],
        handler: ExceptionHandler,
    ) -&amp;gt; None:  # pragma: no cover
        self.exception_handlers[exc_class_or_status_code] = handler&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;실제 코드 line 79 에 위치한 build_middleware_stack 함수를 통해 유저가 주입한 exception_handler&amp;nbsp; 의 값에 따라 error_handler 와 exception_handler 가 구분되며, middleware 에 유저가 정의한 user_middleware 를 감싸는 구조로 ServerErrorMiddleware 와 ExceptionMiddleware 가 주입됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;실제코드 line 108 에 위치한 __call__ 에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;build_middleware_stack 함수가 호출되며, 이를 통해 미들웨어는 Starlette application 생성 시점이 아니라 최초에&lt;span&gt;&amp;nbsp;&lt;/span&gt;Starlette application 이 요청을 처리하는 시점에 초기화 된다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제의 원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 ExceptionMiddleware, ServerErrorMiddleware, CORSMiddleware 중 어디가 문제인걸까요?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http status code 500 이나 Exception, 혹은 ExceptionHandler 에서 처리되지 않은 Exception 을 상속한 예외들에 대한 책임은 오롯이 ServerErrorMiddleware 의 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Outermost 에 ServerErrorMiddleware 가 존재하고 status_code 500 이나 Exception 으로 밖에 감지되지 않는 Exception 은 ServerErrorMiddleware로 바로 전달됩니다. 정확히 말하면, &lt;b&gt;다른&amp;nbsp;middleware 에서 처리하지 않고 다음 middleware 를 계속 호출한 끝에 ServerErrorMiddleware 에게 전달&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 요청에서는 CORS 를 검사하지만 Exception 에 의한 응답에는 CORS 관련 헤더를 받을 수 없는 겁니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Starlette Middleware Chain with Exception.png&quot; data-origin-width=&quot;1666&quot; data-origin-height=&quot;942&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pNc7V/btsMllioGGN/NUBNkNrFeyAqPTkUZ5dIKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pNc7V/btsMllioGGN/NUBNkNrFeyAqPTkUZ5dIKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pNc7V/btsMllioGGN/NUBNkNrFeyAqPTkUZ5dIKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpNc7V%2FbtsMllioGGN%2FNUBNkNrFeyAqPTkUZ5dIKk%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;750&quot; height=&quot;424&quot; data-filename=&quot;Starlette Middleware Chain with Exception.png&quot; data-origin-width=&quot;1666&quot; data-origin-height=&quot;942&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;그렇다면 도대체 Exception(ServerError)가 발새할때 CORS 헤더가 반환되지 않는게 왜 문제일까요?&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;1. 클라이언트와의 약속이 깨집니다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 클라이언트는 서버와 약속된 에러코드에 따른 적절한 처리를 구현할 것입니다. 하지만 Exception 이 발생할때 의도치 않은 동작을 서버가 반환하기 때문에 클라이언트단에서 적절히 에러에 대응할 수 없게 됩니다.&lt;/p&gt;
&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;h3 data-ke-size=&quot;size23&quot;&gt;2. 일관적이지 못한 예외처리로 혼란을 초래합니다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Exception 이 아니라 HttpException 과 WebsocketException 은 ExceptionMiddleware에서 처리되기 때문에 CORSMiddleware 의 처리를 거치게 됩니다. 즉, &lt;b&gt;오로지 Exception 에 대해서만 CORS 응답 헤더가 추가되지 않는 것이지요.&lt;/b&gt; 이러한 Starlette의 동작을 이해하지 못한 개발자는 다소 혼란을 겪을 수 밖에 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 FastAPI 에서 제공되는 HttpException 이 발생할때는 CORS 응답이 제대로 처리되 기 때문에 &quot;원인을 알 수 없지만, 종종 서버가 CORS 에러를 만들어낸다&quot; 는 결론에 도달하게 됩니다. 게다가 FastAPI 공식문서에서는 이러한 이슈를 다루고 있지 않기 때문에 더욱더 혼란스러울 뿐입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI 유저들은 이 문제를 어떻게 대응하고 있는지 &lt;a href=&quot;https://probehub.tistory.com/69&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&quot;FastAPI ErrorHandling 과 CORS (2) - 대안 파악&quot;&lt;/a&gt; 에서 알아보겠습니다.&lt;/p&gt;</description>
      <category>탐구 생활/FastAPI CORS</category>
      <category>fastapi cors</category>
      <category>fastapi corsmiddleware</category>
      <category>fastapi exceptionhanlder cors</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/68</guid>
      <comments>https://probehub.tistory.com/68#entry68comment</comments>
      <pubDate>Mon, 10 Feb 2025 00:02:11 +0900</pubDate>
    </item>
    <item>
      <title>FastAPI log (3) - 설계, 구현</title>
      <link>https://probehub.tistory.com/66</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 시리즈는 AWS EKS &amp;amp; FastAPI 환경에서 로그를 적용하는 과정을 다루고 있습니다. 전체 시리즈는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://probehub.tistory.com/manage/newpost/64&quot;&gt;&lt;span&gt;FastAPI&amp;nbsp;log&amp;nbsp;(1)&amp;nbsp;-&amp;nbsp;AWS&amp;nbsp;EKS&amp;nbsp;Fargate&amp;nbsp;환경에서&amp;nbsp;log&amp;nbsp;를&amp;nbsp;외부시스템에&amp;nbsp;보내기&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://probehub.tistory.com/65&quot;&gt;&lt;span&gt;FastAPI log (2) -AWS EKS Fargate, 왜 Fluent Bit 인가?&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://probehub.tistory.com/66&quot;&gt;FastAPI&amp;nbsp;log&amp;nbsp;(3)&amp;nbsp;-&amp;nbsp;설계,&amp;nbsp;구현&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://probehub.tistory.com/75&quot;&gt;FastAPI log (4) - 개선하기&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&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;&quot;&lt;a href=&quot;https://probehub.tistory.com/64&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AWS EKS Fargate 환경에서 log 를 외부시스템에 보내기&lt;/a&gt;&quot;&lt;/b&gt; 을 통해 로그를 Cloudwatch 로 보내는 인프라 세팅을 맞췄습니다. 이제는 FastAPI 프레임워크에서 어떻게 로그를 형성하여 보내는게 좋을지 더 실무적인 관점에서 고민한 내용을 기록해놓으려고 합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;uvicorn 의 기본 로그&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI 는 보통 uvicorn 을 통해 구동됩니다. 이때 별도의 설정이 없다면 uvicorn 은 default access log 와 exception log 를 만들어낸다. 이런 형식의 로그를&amp;nbsp; &lt;a href=&quot;https://httpd.apache.org/docs/current/ko/logs.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;Apache Common Log Format(CLF)&lt;/b&gt; &lt;/a&gt;라고 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_uvicorn default log.png&quot; data-origin-width=&quot;1058&quot; data-origin-height=&quot;122&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AXUQM/btsL2B8alzW/PSZAzNnz5UKbsw2WwTN3nK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AXUQM/btsL2B8alzW/PSZAzNnz5UKbsw2WwTN3nK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AXUQM/btsL2B8alzW/PSZAzNnz5UKbsw2WwTN3nK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAXUQM%2FbtsL2B8alzW%2FPSZAzNnz5UKbsw2WwTN3nK%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;740&quot; height=&quot;85&quot; data-filename=&quot;edited_uvicorn default log.png&quot; data-origin-width=&quot;1058&quot; data-origin-height=&quot;122&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLF 는직관적이고 가볍다는 장점이 있지만 필요한 정보를 충분히 담아내지 못한다는 단점이 있다. 그렇다면 로그에 &lt;b&gt;&quot;필요한 정보&quot;&lt;/b&gt; 는 무엇이 있을까요?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;API 서버의 표준 로그&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 요청, 응답을 로깅할때 사용되는 로그의 타입 Access Log, Error Log, Security Log, Performace Log 로 나뉩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Access Log: 누가 언제 어떤 API 를 호출&lt;/b&gt;했는지 추적하는 로그.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Error Log&lt;/b&gt;: API 호출 중 예외가 발생했을 때의 &lt;b&gt;오류 발생 원인&lt;/b&gt;을 파악하는 로그.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Security Log:&amp;nbsp; &lt;/b&gt;API의 &lt;b&gt;인증, 권한, 공격 시도&lt;/b&gt;와 관련된 정보를 기록하는 로그.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. Performance Log:&amp;nbsp;&lt;/b&gt;API의 &lt;b&gt;응답 속도, DB 쿼리 성능, 캐싱 여부&lt;/b&gt; 등 성능 관련 데이터를 기록하는 로그.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;표준 로그셋&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 로그 타입마다 요구하는 정보가 조금씩 다르지만 이들의 포맷을 통합하여 관리하는 것이 로그 수집기, 로그 분석기와 호환성이 좋을것이라고 생각했습니다. 이러한 이유로 다음의 &lt;b&gt;통합 로깅 json 구조&lt;/b&gt;를 적용하고자 했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738299577236&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;timestamp&quot;: &quot;2025-01-30T14:54:45.345Z&quot;,
    &quot;event_id&quot;: &quot;999e8887-e77b-66d3-a456-426614174000&quot;,
    &quot;log_type&quot;: &quot;access&quot;,  // &quot;access&quot; | &quot;error&quot; | &quot;security&quot; |
    &quot;level&quot;: &quot;INFO&quot;,       // &quot;INFO&quot; | &quot;WARNING&quot; | &quot;ERROR&quot;
    &quot;method&quot;: &quot;GET&quot;,
    &quot;path&quot;: &quot;/api/v1/products&quot;,
    &quot;status_code&quot;: 200,
    &quot;client_ip&quot;: &quot;192.168.1.1&quot;,
    &quot;user_agent&quot;: &quot;Mozilla/5.0&quot;,
    &quot;time_taken_ms&quot;: 250,
    &quot;exception&quot;: null,           // 에러 로그용 필드 (error)
    &quot;error_message&quot;: null,       // 에러 로그용 필드 (error)
    &quot;stack_trace&quot;: null,         // stacktrace 용 필드
}&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;u&gt;*request 와 response 에 대한 자세한 정보는 민감한 정보를 담을 수 있기 때문에 기록하지 않습니다.&amp;nbsp;&lt;/u&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;FastAPI 로깅 적용&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.&amp;nbsp; 라이브러리 선택&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;logging 만 이용하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;python 에는 로그를 위해 내장된 &lt;b&gt;&lt;a href=&quot;https://docs.python.org/3/library/logging.html#module-logging&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;logging 라이브러리&lt;/a&gt;&lt;/b&gt;가 존재합니다. python 언어에 내장된 라이브러리이기 때문에 굉장히 표준적인 선택이라고 할 수 있겠습니다. 하지만, 어플리케이션을 빌드하는 과정에서 다른 외부 라이브러리가 logging 모듈을 건드렸을때 그에 따른 영향을 받을 가능성도 존재합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;python-json-logger 과 함께 이용하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;python 에 내장되진 않았지만 별1.8k 를 받은 &lt;b&gt;&lt;a href=&quot;https://github.com/madzak/python-json-logger&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;python-json-logger 라이브러리&lt;/a&gt;&lt;/b&gt; 역시 존재합니다. 기존 python logging 라이브러리에서 json 을 출력할때 더 편하게 할 수 있도록 도와주는 기능이 포함되어 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;라이브러리 선택: 결론&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;python-json-logger 공식문서에 제시된 예제를 통해 둘을 비교해보겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;python-json-logger 라이브러리를 통해 얻을 수 있는 이점은 json.dumps() 를 호출하지 않아도 된다는 것입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 message 라는 필드가 반드시 딸려나오고, 이를 없애기 위해서는 커스텀 설정을 해야한다는 점이 단점이있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738676257426&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import logging
import json
from pythonjsonlogger.json import JsonFormatter

plain_logger = logging.getLogger(&quot;plain-logger&quot;)
json_logger = logging.getLogger(&quot;json-logger&quot;)

plain_logger.setLevel(logging.INFO)
json_logger.setLevel(logging.INFO)

plain_logger_handler = logging.StreamHandler()
json_logger_handler = logging.StreamHandler()

# formatter 설정시 차이 발생
plain_logger_handler.setFormatter(logging.Formatter(&quot;%(message)s&quot;))
json_logger_handler.setFormatter(JsonFormatter())

plain_logger.addHandler(plain_logger_handler)
json_logger.addHandler(json_logger_handler)


# json 형식 출력시 차이 발생
my_dict = {
	&quot;message&quot;:&quot;Logging only using logger!&quot;,
    &quot;more_data&quot;:True
}
plain_logger.info(json.dumps(my_dict))
# {&quot;message&quot;: &quot;Logging only using logger!&quot;, &quot;more_data&quot;: true}

json_logger.info(&quot;Logging using pythonjsonlogger!&quot;, extra={&quot;more_data&quot;: True})
# {&quot;message&quot;: &quot;Logging using pythonjsonlogger!&quot;, &quot;more_data&quot;: true}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 python 유저들은 logging 과 json 에는 익숙하겠지만, &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;python-json-logger 라이브러리에 대해서 별도의 러닝 커브가 존재할것이라는 점, 그리고 이점보다 단점이 복잡성을 더 크게 증가시킨다는 점에서 logging 라이브러리만 이용하는 방향을 정했습다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.&amp;nbsp; Middleware 와 ExceptionHandler 역할 분리&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;FastAPI Reqeust_Response flow.png&quot; data-origin-width=&quot;2236&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xVUf1/btsL3l46g4U/KPY1GVolagDM97JbuJjdi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xVUf1/btsL3l46g4U/KPY1GVolagDM97JbuJjdi0/img.png&quot; data-alt=&quot;FastAPI 요청 흐름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xVUf1/btsL3l46g4U/KPY1GVolagDM97JbuJjdi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxVUf1%2FbtsL3l46g4U%2FKPY1GVolagDM97JbuJjdi0%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;2236&quot; height=&quot;626&quot; data-filename=&quot;FastAPI Reqeust_Response flow.png&quot; data-origin-width=&quot;2236&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FastAPI 요청 흐름&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI 요청 흐름을 보면 Middleware 와 ExceptionHandler 의 역할을 고민하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로깅을 Middleware 에서 처리하는게 일반적인데, ExceptionHandler 에서 예외에 대한 정보 없이 응답만 전달하게 된다면 ErrorLog 를 남길 수 없게됩니다. 따라서 ExceptionHandler 에서 Exception 정보를 그대로 Middleware 로 넘겨야겠다! 로 처음에는 방향을 정했었는데, 그럴경우 굳이 ExceptionHandler 가 있을 필요가 없고, Middleware 가 로깅의 역할 뿐만 아니라 Exception 을 처리하는 역할까지 맡아야해서 굉장히 복잡해지는 문제가 있습니다.&lt;/p&gt;
&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;그래서 ExceptionHandler 는 기존과 동일하게 Exception 에 따라 적절한 response 를 반환하는 역할을 하되 request.state 에 에러에 대한 정보를 넘겨주는 방법을 사용하게 되었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 환경에 따른 로그 변경&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 표준로그(json) 를 출력하기 위해 고민을 했습니다. 하지만 각 개발자들이 본인의 로컬 개발환경에서도 json 로그가 출력되는게 좋을까요? 경우에 따라서는 Yes 라고 할 수도 있겠지만, uvicorn 과 python 은 이미 훌륭한 기본 로그 시스템을 보유하고 있습니다.&lt;/p&gt;
&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_1738677790671&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if USE_JSON_LOGGING:
    setup_request_logger()      # request-logger를 JSON 포맷으로 설정
    disable_uvicorn_logs()      # uvicorn.access 로그 비활성화
    app.add_middleware(JsonRequestLoggerMiddleware)
else:
    # Uvicorn의 기본 로그 사용
    pass&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style2&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;결론: 구현코드&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 middleware 를 구현하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 구현사항에는 결정적인 오류와 개선 사항이 있습니다. 자세한 이야기는 다음 글에서 이어서 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740895625513&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import logging
import traceback
import time
import uuid
import json

from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware

from app.logger.logger_setup import REQUEST_LOGGER_NAME
from app.exception import ErrorCode, MyCustomException
from app.util.env import get_bool_from_env

logger = logging.getLogger(REQUEST_LOGGER_NAME)

STACKTRACE_LOGGING_ENABLED = get_bool_from_env(&quot;IS_STACKTRACE_LOGGING&quot;, True)


class JsonRequestLoggerMiddleware(BaseHTTPMiddleware):
    def __init__(self, app: FastAPI):
        super().__init__(app)
    async def dispatch(self, request: Request, call_next):
        start_time = time.time()
        event_id = str(uuid.uuid4())

        log_data = {
            &quot;timestamp&quot;: time.strftime(&quot;%Y-%m-%dT%H:%M:%S.%fZ&quot;, time.gmtime()),
            &quot;event_id&quot;: event_id,
            &quot;method&quot;: request.method,
            &quot;path&quot;: request.url.path,
            &quot;client_ip&quot;: request.client.host if request.client else &quot;unknown&quot;,
            &quot;user_agent&quot;: request.headers.get(&quot;user-agent&quot;),
        }

        try:
            response = await call_next(request)
            status_code = response.status_code
            log_type = &quot;access&quot;

        except Exception as e: 
            status_code = 500 
            log_type = &quot;error&quot;
            log_data['error_code'] = &quot;UNEXPECTED_ERROR&quot; # Or some default code
            log_data['exception'] = type(e).__name__
            log_data['error_message'] = str(e) # Log the exception message as fallback
            log_data['stack_trace'] = traceback.format_exc().splitlines()

            application_exception = MyCustomException(ErrorCode.UNEXPECTED)
            response = application_exception.to_error_response()


        error_info = getattr(request.state, &quot;error_info&quot;, None)  # Use getattr

        if error_info:  # exception handler 에서 넘기는 error_info 정보 확인

            status_code = error_info.get(&quot;http_status&quot;, getattr(ErrorCode.UNEXPECTED, &quot;http_status&quot;, 500)) # Use status code from error_info, Default 500
            log_type = &quot;error&quot; if status_code &amp;gt;= 400 and status_code not in [401, 403] else 'security'

            log_data.update({  # error_info 를 log data 에 추가
                &quot;error_code&quot;: error_info.get(&quot;code&quot;),
                &quot;error_message&quot;: error_info.get(&quot;message&quot;),
                &quot;stack_trace&quot;: error_info.get(&quot;stack_trace&quot;) if (STACKTRACE_LOGGING_ENABLED or error_info.get(&quot;code&quot;) == ErrorCode.UNEXPECTED.code) else None,
            })


        log_level = &quot;ERROR&quot; if status_code &amp;gt;= 400 else &quot;INFO&quot;

        log_data['time_taken_ms'] = int((time.time() - start_time) * 1000)
        log_data['log_type'] = log_type
        log_data['level'] = log_level
        log_data['status_code'] = status_code
        log_data['db_query_time_ms'] = getattr(request.state, &quot;db_query_time_ms&quot;, None)

        # https://docs.python.org/3/library/logging.html#logging.getLevelName 의 Changed in version 3.4 에 따르면
        # logging.getLevelName(str) 은 유지되긴 하지만 실수였다고 한다. 이를 최대한 사용하지 않기 위해 이런 방법을 이용헀다.
        log_level_int: int = logging.getLevelNamesMapping()[log_level] 
        logger.log(log_level_int,json.dumps(log_data, ensure_ascii=False).encode('utf-8'))

        return response&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;a href=&quot;https://github.com/timothy-jeong/fastapi-logging-example/tree/main&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;fastapi-logging-example&lt;/a&gt; 프로젝트로 정리해두었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;p data-ke-size=&quot;size16&quot;&gt;API 로그에 대하여: &lt;a href=&quot;https://www.moesif.com/blog/api-analytics/api-strategy/API-Logs/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;API Logs: Everything You Need to Know&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 로그 가이드: &lt;a href=&quot;https://www.merge.dev/blog/api-logs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;A&amp;nbsp;guide&amp;nbsp;to&amp;nbsp;API&amp;nbsp;logs&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;python logging: &lt;a href=&quot;https://docs.python.org/3/library/logging.html#module-logging&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.python.org/3/library/logging.html#module-logging&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;python josn logger: &lt;a href=&quot;https://nhairs.github.io/python-json-logger/latest/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://nhairs.github.io/python-json-logger/latest/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI Middleware: &lt;a href=&quot;https://fastapi.tiangolo.com/tutorial/middleware/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://fastapi.tiangolo.com/tutorial/middleware/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI ExceptionHandler: &lt;a href=&quot;https://fastapi.tiangolo.com/tutorial/handling-errors/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://fastapi.tiangolo.com/tutorial/handling-errors/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AQLAlchemy 비동기 연결 이벤트: &lt;a href=&quot;https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#examples-of-event-listeners-with-async-engines-sessions-sessionmakers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Examples&amp;nbsp;of&amp;nbsp;Event&amp;nbsp;Listeners&amp;nbsp;with&amp;nbsp;Async&amp;nbsp;Engines&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>탐구 생활/FastAPI Log</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/66</guid>
      <comments>https://probehub.tistory.com/66#entry66comment</comments>
      <pubDate>Thu, 30 Jan 2025 22:03:56 +0900</pubDate>
    </item>
    <item>
      <title>FastAPI log (2) -AWS EKS Fargate, 왜 Fluent Bit 인가?</title>
      <link>https://probehub.tistory.com/65</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1723010688780.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJ8jua/btsL4cy7JhM/fswR4HltNFKlOoWwZvVJk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJ8jua/btsL4cy7JhM/fswR4HltNFKlOoWwZvVJk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJ8jua/btsL4cy7JhM/fswR4HltNFKlOoWwZvVJk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJ8jua%2FbtsL4cy7JhM%2FfswR4HltNFKlOoWwZvVJk1%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;640&quot; height=&quot;320&quot; data-filename=&quot;1723010688780.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 시리즈는 AWS EKS &amp;amp; FastAPI 환경에서 로그를 적용하는 과정을 다루고 있습니다. 전체 시리즈는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://probehub.tistory.com/manage/newpost/64&quot;&gt;&lt;span&gt;FastAPI&amp;nbsp;log&amp;nbsp;(1)&amp;nbsp;-&amp;nbsp;AWS&amp;nbsp;EKS&amp;nbsp;Fargate&amp;nbsp;환경에서&amp;nbsp;log&amp;nbsp;를&amp;nbsp;외부시스템에&amp;nbsp;보내기&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://probehub.tistory.com/65&quot;&gt;&lt;span&gt;FastAPI log (2) -AWS EKS Fargate, 왜 Fluent Bit 인가?&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://probehub.tistory.com/66&quot;&gt;FastAPI&amp;nbsp;log&amp;nbsp;(3)&amp;nbsp;-&amp;nbsp;설계,&amp;nbsp;구현&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://probehub.tistory.com/75&quot;&gt;FastAPI log (4) - 개선하기&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&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;&quot;&lt;a href=&quot;https://probehub.tistory.com/64&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AWS EKS Fargate 환경에서 log 를 외부시스템에 보내기&lt;/a&gt;&quot;&lt;/b&gt; &lt;span&gt;에서 Fargate 내부에 Fluent Bit 에이전트가 기본적으로 설치되어 있으며, AWS가 제공하는 설정 방식에 따라 CloudWatch 등으로 로그를 전송할 수 있음을 설명했다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그러나 클라우드 환경에서 관측 데이터(telemetry data)를 수집하여 Log Storage로 보낼 수 있는 다양한 옵션이 존재하는데, 왜 AWS는 Fluent Bit을 선택하여 Fargate에 포함시켰을까?&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;1. 범용 로그 수집기 비교&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;일반적으로 사용되는 로그 수집기들의 성능을 비교한 자료(&lt;a href=&quot;https://medium.com/ibm-cloud/log-collectors-performance-benchmarking-8c5218a08fea&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021년 벤치마크&lt;/a&gt;)를 요약하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-pm-slice=&quot;3 3 []&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.fluentbit.io/manual/about/what-is-fluent-bit&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;&lt;b&gt;Fluent Bit&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;span&gt;: 적은 CPU 자원으로도 높은 효율을 보이며, 높은 부하에서도 성능 저하가 크지 않음. 단, 대용량 로그 처리 시 메모리 사용량이 증가함.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.fluentd.org/architecture&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;&lt;b&gt;Fluentd&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;span&gt;: 대량의 로그를 안정적으로 처리하지만, CPU와 메모리 사용량이 상대적으로 높음.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vector.dev/docs/about/vector/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;&lt;b&gt;Vector&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;span&gt;: 가장 뛰어난 로그 처리 성능을 보였지만, CPU 사용량이 Fluent Bit보다 2~3배 많음.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Fluentd는 Fluent Bit 및 Vector와 비교했을 때 특별한 장점이 없기 때문에 고려 대상에서 제외할 수 있다. 결국 &lt;/span&gt;&lt;span&gt;&lt;b&gt;Fluent Bit과 Vector 중 하나를 선택해야 하는데&lt;/b&gt;&lt;/span&gt;&lt;span&gt;, 메모리를 더 많이 활용할 경우 Fluent Bit을, CPU를 더 많이 활용할 경우 Vector를 선택하는 것이 합리적이다.&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;a href=&quot;https://www.elastic.co/guide/en/logstash/current/introduction.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;&lt;b&gt;LogStash&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;span&gt;와 &lt;/span&gt;&lt;a href=&quot;https://grafana.com/docs/loki/latest/send-data/promtail/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;&lt;b&gt;Promtail&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;span&gt;은 AWS와의 통합이 부족하여 비교군에서 제외되었으며, &lt;/span&gt;&lt;a href=&quot;https://www.rsyslog.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;&lt;b&gt;Rsyslog&lt;/b&gt;&lt;/span&gt;&lt;/a&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Fargate에서 CPU와 메모리의 특징&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;앞서 &lt;/span&gt;&lt;span&gt;&lt;b&gt;메모리를 더 많이 활용할 경우 Fluent Bit을, CPU를 더 많이 활용할 경우 Vector를 선택하는 것이 적절&lt;/b&gt;&lt;/span&gt;&lt;span&gt;하다는 가설을 세웠다. 이를 검증하기 위해 AWS EKS Fargate의 리소스 할당 방식을 살펴보자.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;Fargate의 리소스 할당 방식&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;AWS Fargate는 EC2 기반이 아닌 서버리스 컨테이너 환경으로, 인스턴스를 직접 관리하지 않는다. 대신, &lt;/span&gt;&lt;span&gt;&lt;b&gt;컨테이너별로 고정된 CPU와 메모리를 할당하는 방식&lt;/b&gt;&lt;/span&gt;&lt;span&gt;이 적용된다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CPU (vCPU)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Fargate에서는 컨테이너에 할당할 수 있는 vCPU를 &lt;b&gt;정해진 옵션에서 선택&lt;/b&gt;해야 함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CPU는 Burstable 방식이 아니며, 설정된 값까지만 사용 가능&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;과부하 시 추가적인 CPU 자원이 자동으로 할당되지 않음&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모리 (RAM)&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;/li&gt;
&lt;li&gt;&lt;b&gt;메모리가 초과되면 OOM(Out of Memory) Kill 발생&lt;/b&gt; &amp;rarr; 프로세스 강제 종료.&lt;/li&gt;
&lt;li&gt;하지만 CPU와 다르게 &lt;b&gt;사용 가능한 메모리를 미리 충분히 할당할 수 있음&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-sheets-baot=&quot;1&quot; data-sheets-root=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;할당 가능한 vCPU&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;할당 가능한 메모리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.25 vCPU&lt;/td&gt;
&lt;td&gt;0.5GB ~ 2GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.5 vCPU&lt;/td&gt;
&lt;td&gt;1GB ~ 4GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 vCPU&lt;/td&gt;
&lt;td&gt;2GB ~ 8GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 vCPU&lt;/td&gt;
&lt;td&gt;4GB ~ 16GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 vCPU&lt;/td&gt;
&lt;td&gt;8GB ~ 30GB&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Fargate는 &lt;b&gt;CPU 사용량을 필요 이상으로 증가시키는 것이 불리&lt;/b&gt;하다. (Burstable X)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메모리는 미리 할당해놓으면 활용 가능&lt;/b&gt;, 하지만 초과하면 프로세스가 죽는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;즉, CPU를 많이 쓰는 Vector는 제약을 받을 가능성이 크고, Fluent Bit처럼 메모리를 활용하는 방식이 유리할 수 있다&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;span&gt;따라서 AWS가 Fargate 환경에서 Fluent Bit을 기본 로깅 솔루션으로 선택한 이유는 CPU 사용량을 최소화하면서도 안정적으로 로그를 수집할 수 있기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&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;b&gt;logs hipper 비교&lt;/b&gt;:&amp;nbsp; &lt;a href=&quot;https://betterstack.com/community/guides/logging/log-shippers-explained/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;The Top 7 Log Shippers and How to Choose One&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;l&lt;b&gt;og collector 벤치마크&lt;/b&gt;: &lt;a href=&quot;https://medium.com/ibm-cloud/log-collectors-performance-benchmarking-8c5218a08fea&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Who is the winner &amp;mdash; Comparing Vector, Fluent Bit, Fluentd performance&lt;/a&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>탐구 생활/FastAPI Log</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/65</guid>
      <comments>https://probehub.tistory.com/65#entry65comment</comments>
      <pubDate>Thu, 30 Jan 2025 22:02:56 +0900</pubDate>
    </item>
    <item>
      <title>FastAPI log (1) - AWS EKS Fargate 환경에서 log 를 외부시스템에 보내기</title>
      <link>https://probehub.tistory.com/64</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 시리즈는 AWS EKS &amp;amp; FastAPI 환경에서 로그를 적용하는 과정을 다루고 있습니다. 전체 시리즈는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://probehub.tistory.com/manage/newpost/64&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;FastAPI&amp;nbsp;log&amp;nbsp;(1)&amp;nbsp;-&amp;nbsp;AWS&amp;nbsp;EKS&amp;nbsp;Fargate&amp;nbsp;환경에서&amp;nbsp;log&amp;nbsp;를&amp;nbsp;외부시스템에&amp;nbsp;보내기&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://probehub.tistory.com/65&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;FastAPI log (2) -AWS EKS Fargate, 왜 Fluent Bit 인가?&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://probehub.tistory.com/66&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;FastAPI&amp;nbsp;log&amp;nbsp;(3)&amp;nbsp;-&amp;nbsp;설계,&amp;nbsp;구현&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://probehub.tistory.com/75&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;FastAPI log (4) - 개선하기&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&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;기존 모놀리식 서비스를 마이크로서비스 아키텍처로 전환하면서 AWS EKS(Fargate) 기반으로 운영하는 방안을 선택했습니다. 이 과정에서 Java &amp;amp; Spring 기반 서비스를 Python &amp;amp; FastAPI로 변경하는 것뿐만 아니라, EKS Kubernetes 환경에서 운영하는 방법을 익히는 것도 중요한 과제였지요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그중 하나가 &lt;/span&gt;&lt;span&gt;&lt;b&gt;로그 수집(logging)&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 이었는데요. Fargate 환경에서는 &lt;/span&gt;&lt;span&gt;&lt;b&gt;DaemonSet을 사용할 수 없기 때문에&lt;/b&gt;&lt;/span&gt;&lt;span&gt;, 기존 방식대로 로그 수집을 설정할 수 없었다. 이에 따라 AWS가 제공하는 Fluent Bit 기반의 로그 수집 방식을 적용해야 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;AWS EKS Fargate의 로깅 구조&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;AWS-EKS-Fargate-Dataplane.png&quot; data-origin-width=&quot;2022&quot; data-origin-height=&quot;1006&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/banZOG/btsL3yijE9N/fH6ikXkt2ze9rmRVDKcVa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/banZOG/btsL3yijE9N/fH6ikXkt2ze9rmRVDKcVa0/img.png&quot; data-alt=&quot;Fargate Dataplane&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/banZOG/btsL3yijE9N/fH6ikXkt2ze9rmRVDKcVa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbanZOG%2FbtsL3yijE9N%2FfH6ikXkt2ze9rmRVDKcVa0%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;640&quot; height=&quot;318&quot; data-filename=&quot;AWS-EKS-Fargate-Dataplane.png&quot; data-origin-width=&quot;2022&quot; data-origin-height=&quot;1006&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Fargate Dataplane&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;AWS EKS를 Fargate로 운영할 때,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;Dataplane 내부에는 이미 Fluent Bit 에이전트가 설치&lt;/b&gt;&lt;/span&gt;&lt;span&gt;되어 있다. 이를 활용하면 추가적인 로그 수집용 에이전트 없이 AWS CloudWatch 또는 S3와 같은 외부 서비스로 로그를 보낼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;EKS Fargate 환경에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;aws-logging&lt;/b&gt;&lt;/span&gt;&lt;span&gt;이라는 ConfigMap을 사용하여 로그를 관리합니다. 따라서, 로그를 수집하고 외부로 전송하려면 이 ConfigMap을 올바르게 설정해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;AWS EKS Fargate 로깅 설정 방법&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. IAM role 정책 추가&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;EKS 운영에는 여러 IAM Role이 필요하지만, 로깅을 위해서는 &lt;/span&gt;&lt;span&gt;&lt;b&gt;Pod Execution Role&lt;/b&gt;&lt;/span&gt;&lt;span&gt;에 다음과 같은 정책을 추가해야 합니다. 이 정책은 CloudWatch에 로그를 전송할 수 있도록 해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738238023967&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;Version&quot;: &quot;2012-10-17&quot;,
&quot;Statement&quot;: [
    {
        &quot;Effect&quot;: &quot;Allow&quot;,
        &quot;Action&quot;: [
            &quot;logs:CreateLogGroup&quot;,
            &quot;logs:CreateLogStream&quot;,
            &quot;logs:PutLogEvents&quot;,
            &quot;logs:DescribeLogGroups&quot;,
            &quot;logs:DescribeLogStreams&quot;
        ],
        &quot;Resource&quot;: &quot;*&quot;
    }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;AWS CLI 또는 AWS Console을 사용하여 Pod Execution Role에 위 정책을 적용합니다.&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-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;2. EKS Namespace 및 ConfigMap 설정&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;AWS에서 제공하는 표준 스크립트를 사용하여 Fluent Bit을 설정할 수 있습니다. 이 설정을 통해 &lt;/span&gt;&lt;span&gt;&lt;b&gt;CloudWatch&lt;/b&gt;&lt;/span&gt;&lt;span&gt;로 로그를 전송하며, 필요에 따라 &lt;/span&gt;&lt;span&gt;&lt;b&gt;S3 또는 외부 서비스로 변경&lt;/b&gt;&lt;/span&gt;&lt;span&gt;할 수도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다음과 같이 &lt;/span&gt;&lt;span&gt;aws-logging.yaml&lt;/span&gt;&lt;span&gt; 파일을 생성합니다:&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738238209898&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kind: Namespace
apiVersion: v1
metadata:
  name: aws-observability # 이름 변경되면 안됨
  labels:
    aws-observability: enabled # 꼭 필요!
---    
kind: ConfigMap
apiVersion: v1
metadata:
  name: aws-logging # 이름 변경되면 안됨
  namespace: aws-observability
data:
  flb_log_cw: &quot;true&quot;  # Fluent Bit 자체 생산 log
  filters.conf: |
    [FILTER]
        Name parser
        Match *
        Key_name log
        Parser crio
    [FILTER]
        Name kubernetes
        Match kube.*
        Merge_Log On
        Keep_Log Off
        Buffer_Size 0
        Kube_Meta_Cache_TTL 300s
  output.conf: |
    [OUTPUT]
        Name cloudwatch_logs
        Match kube.*
        region {aws_region}
        log_group_name {log_group_name} # cloudwatch log group 이름
        log_stream_prefix from-fluent-bit- # 로그 prefix
        log_retention_days 60 # 로그 보존기간
        auto_create_group true
  parsers.conf: |
    [PARSER]
        Name crio
        Format Regex
        Regex ^(?&amp;lt;time&amp;gt;[^ ]+) (?&amp;lt;stream&amp;gt;stdout|stderr) (?&amp;lt;logtag&amp;gt;P|F) (?&amp;lt;log&amp;gt;.*)$
        Time_Key    time
        Time_Format %Y-%m-%dT%H:%M:%S.%L%z&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;위 설정을 적용하려면 다음 명령을 실행합니다:&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1738238272070&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl apply -f aws-logging.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;참고: AWS Fargate에서 제공하는 Fluent Bit은 &lt;/span&gt;&lt;span&gt;&lt;b&gt;[FILTER], [OUTPUT], [PARSER]&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 설정만 지원합니다. [SERVICE] 및 [INPUT] 설정은 사용할 수 없습니다.&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-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;3. 로그 수집 적용하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 올라와있는 pod 이 없다면 이제부터 Deployment 를 올리고 pod 을 올리면 자동으로 logging 이 적용됩니다. 하지만 이미 pod 이 올라와 있다면 pod 을 재시작하는 방법을 통해 aws-logging config map 을 참조하게 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738238744589&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl rollout restart deployment -n &amp;lt;namespace&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&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;&lt;span&gt;이제 AWS EKS Fargate 환경에서 로그 수집이 정상적으로 설정되었네요. 적용이 완료된 후, AWS CloudWatch 콘솔에서 로그 그룹을 확인하여 로그가 정상적으로 전송되고 있는지 검토할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Fluent Bit 공식 문서&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;a href=&quot;https://docs.fluentbit.io/manual/about/what-is-fluent-bit&quot;&gt;&lt;span&gt;What is Fluent Bit?&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;AWS EKS Fargate 로깅 매뉴얼&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/fargate-logging.html&quot;&gt;&lt;span&gt;EKS Fargate Logging&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>탐구 생활/FastAPI Log</category>
      <category>aws eks fargate fluent bit</category>
      <category>aws eks fargate logging</category>
      <category>aws fluent bit</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/64</guid>
      <comments>https://probehub.tistory.com/64#entry64comment</comments>
      <pubDate>Thu, 30 Jan 2025 21:06:09 +0900</pubDate>
    </item>
    <item>
      <title>FastAPI 의존성 주입, 코드를 까보자</title>
      <link>https://probehub.tistory.com/63</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;fastapi-di.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7Brjk/btsK6TIH1Me/sHDPnHhI5TkL0guCweMn11/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7Brjk/btsK6TIH1Me/sHDPnHhI5TkL0guCweMn11/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7Brjk/btsK6TIH1Me/sHDPnHhI5TkL0guCweMn11/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7Brjk%2FbtsK6TIH1Me%2FsHDPnHhI5TkL0guCweMn11%2Fimg.webp&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;350&quot; height=&quot;350&quot; data-filename=&quot;fastapi-di.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://probehub.tistory.com/62&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글&lt;/a&gt;에서 FastAPI 가 제공하는 의존성 주입 기능을 알아보았다. 하지만 그정도는 어떻게 쓰는지 알아보는 정도이고 아직 풀리지 않는 궁금증이 많다.&lt;/p&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;어떻게 Callable 만 받아들이는가?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;서브-의존성 을 구현하는 방법은 무엇인가?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;왜 FastAPI 경로 동작 함수(path operation function)에서만 동작하는가?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 궁금증을 풀기 위해서는 직접 코드를 까보는게 최고다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&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;fastapi.param_functions.Depends&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 까본 결과 직접 FastAPI 경로 작업 함수(path opration function)에서 참조하는 Depends 는 use_caches 기능이 있었다!&amp;nbsp; 그리고 내부적으로 params.Depenndes 를 호출하여 파라미터를 그대로 전달하는 동작을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1733359189354&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def Depends(  # noqa: N802
    dependency: Annotated[
        Optional[Callable[..., Any]],
        Doc(
            &quot;&quot;&quot;
            A &quot;dependable&quot; callable (like a function).

            Don't call it directly, FastAPI will call it for you, just pass the object
            directly.
            &quot;&quot;&quot;
        ),
    ] = None,
    *,
    use_cache: Annotated[
        bool,
        Doc(
            &quot;&quot;&quot;
            By default, after a dependency is called the first time in a request, if
            the dependency is declared again for the rest of the request (for example
            if the dependency is needed by several dependencies), the value will be
            re-used for the rest of the request.

            Set `use_cache` to `False` to disable this behavior and ensure the
            dependency is called again (if declared more than once) in the same request.
            &quot;&quot;&quot;
        ),
    ] = True,
) -&amp;gt; Any:

    return params.Depends(dependency=dependency, use_cache=use_cache)&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;API 작성 팁&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;typing_extension.py 의 Doc 이라는 클래스를 통해 더 명시적으로 파라미터에 대한 설명을 작성할 수 있다는 것이다.&lt;/li&gt;
&lt;li&gt;함수 인수(function parameter) 에 * 를 명시한 이후의 인수는 무조건 키워드 인수(keyword parameter) 로 구성된다. &lt;a href=&quot;https://docs.python.org/3/tutorial/controlflow.html#special-parameters&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;파이썬 공식 문서&lt;/a&gt;를 읽어보면 더 다양한 팁을 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 작석 팁을 배운것 좋지만 뭔가 아쉽다. Depends 가 호출하는 params.Depends 를 파고 들어가보자, 특히 use_caches 를 사용하면 어디에 캐시가 되고 어떨때 사용하는게 좋은지도 궁금해진다.&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;b&gt;fastapi.params.Depndes&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잔뜩 기대를 안고 열어본 fastapi.params.Depndes 는 단순한 class 였다. __repr__&lt;/p&gt;
&lt;pre id=&quot;code_1733359212650&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Depends:
    def __init__(
        self, dependency: Optional[Callable[..., Any]] = None, *, use_cache: bool = True
    ):
        self.dependency = dependency
        self.use_cache = use_cache

    def __repr__(self) -&amp;gt; str:
        attr = getattr(self.dependency, &quot;__name__&quot;, type(self.dependency).__name__)
        cache = &quot;&quot; if self.use_cache else &quot;, use_cache=False&quot;
        return f&quot;{self.__class__.__name__}({attr}{cache})&quot;&lt;/code&gt;&lt;/pre&gt;</description>
      <category>탐구 생활/FastAPI</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/63</guid>
      <comments>https://probehub.tistory.com/63#entry63comment</comments>
      <pubDate>Wed, 4 Dec 2024 20:47:28 +0900</pubDate>
    </item>
    <item>
      <title>FastAPI 의존성 주입, Depends 를 알아보자</title>
      <link>https://probehub.tistory.com/62</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;fastapi-di.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAjCLT/btsK8l4QFSc/UKFnfkyQv8SR8GhTnnz1oK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAjCLT/btsK8l4QFSc/UKFnfkyQv8SR8GhTnnz1oK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAjCLT/btsK8l4QFSc/UKFnfkyQv8SR8GhTnnz1oK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAjCLT%2FbtsK8l4QFSc%2FUKFnfkyQv8SR8GhTnnz1oK%2Fimg.webp&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;350&quot; height=&quot;350&quot; data-filename=&quot;fastapi-di.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 을 쓰다가 FastAPI 로 전환하면서 많은 것들이 의문이었지만 그중 가장 큰 것은 &quot;Python 은 빌드되는게 아닌데 의존성 주입을 사용할 수 있나?&quot; 였다. 그리고 그런 나에게 보란듯이 FastAPI 공식 문서에는 &lt;a href=&quot;https://fastapi.tiangolo.com/ko/tutorial/dependencies/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;의존성 주입 파트&lt;/a&gt;가 있다.&amp;nbsp; 이 글은 공식문서를 정독하고 정리하는 글 정도가 되겠다. 이미 어느정도 Depends 를 알고 있고, Depends 의 내부동작을 파악하고 싶다면 &lt;a href=&quot;https://probehub.tistory.com/63&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;다음 글&lt;/a&gt;이 도움이 될 수도 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;div class=&quot;container&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;FastAPI 의존성 주입(Dependency Injection) 정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI는 의존성 주입(Dependency Injection, DI)을 통해 코드의 재사용성, 유지보수성, 테스트 용이성을 향상시킨다.&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;basic-dependencies&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 기본 의존성 주입&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI에서 기본적인 의존성을 정의하고 사용하는 방법은 다음과 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.1. 의존성 함수 정의하기&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;from fastapi import Depends, FastAPI

app = FastAPI()

def get_db():
    db = connect_to_db()
    try:
        yield db
    finally:
        db.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에서는 데이터베이스 세션을 관리하는 의존성 함수를 정의했다. &lt;code&gt;yield&lt;/code&gt;를 사용하여 세션을 반환하고, 요청이 완료된 후 세션을 닫는 동작을 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.2. 의존성 주입 사용하기&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;from fastapi import Depends, FastAPI
from sqlalchemy.orm import Session

app = FastAPI()

@app.get(&quot;/items/&quot;)
def read_items(db: Session = Depends(get_db)):
    items = db.query(Item).all()
    return items
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로 작업 함수(path operation function)에서 &lt;code&gt;Depends&lt;/code&gt;를 사용하여 &lt;code&gt;get_db&lt;/code&gt; 의존성을 주입받는다. 이를 통해 데이터베이스 세션을 쉽게 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.3. 기본 사용방법 정리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 나는 여기까지만 파악했을 때 혼란스러웠다. 이게 뭐지? 하는 기분이었다. 그런데 놀랍게도 의존성으로 주입되는 대상이 호출 가능한 (Callable) 객체라는 점에서 Spring 과 유사점이 있다. 다만 다른 점이 있다면 FastAPI 의 의존성은 경로 작업 함수(FastAPI 인스턴스로 정의된 경로)에서만 유효하다는 점이다.&lt;/p&gt;
&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; yield&amp;nbsp; 를 사용하고 있다. 의존성 주입시 yield 를 이용하여 리소스를 안전하게 관리할 수 있다는 것도 장점이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 id=&quot;classes-as-dependencies&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 클래스를 사용한 의존성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스를 사용하여 의존성을 정의하면 상태를 유지하거나 여러 메서드를 포함할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;from fastapi import Depends, FastAPI

app = FastAPI()

class CommonQueryParams:
    def __init__(self, q: str = None, limit: int = 10):
        self.q = q
        self.limit = limit

@app.get(&quot;/items/&quot;)
def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
    return {&quot;q&quot;: commons.q, &quot;limit&quot;: commons.limit}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에서는 &lt;code&gt;CommonQueryParams&lt;/code&gt; 클래스를 의존성으로 사용하여 공통 쿼리 매개변수를 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.1 클래스를 사용한 의존성 정리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python 에서 클래스는 Callable 하다는 것을 명심하자. Java 도 클래스를 new 키워드로 호출하면 기본 생성자가 호출되지 않는가? Python 도 똑같다. CommonQueryParams 클래스의 __init__ 함수가 호출되는것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 id=&quot;sub-dependencies&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 서브-의존성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성은 다른 의존성을 포함할 수 있습니다. 이를 서브-의존성이라고 한다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from fastapi import Depends, FastAPI, HTTPException, Header
from datetime import datetime
from typing import Optional

app = FastAPI()


def get_token_header(authorization: Optional[str] = Header(None)):
    &quot;&quot;&quot;
    Authorization 헤더에서 토큰을 추출하고 검증합니다.
    헤더 형식은 'Bearer &amp;lt;token&amp;gt;'이어야 합니다.
    &quot;&quot;&quot;
    if authorization is None:
        raise HTTPException(status_code=400, detail=&quot;Authorization header missing&quot;)
    
    try:
        scheme, token = authorization.split()
        if scheme.lower() != &quot;bearer&quot;:
            raise HTTPException(status_code=400, detail=&quot;Invalid authentication scheme&quot;)
    except ValueError:
        raise HTTPException(status_code=400, detail=&quot;Invalid Authorization header format&quot;)
    
    if token != &quot;expected_token&quot;:
        raise HTTPException(status_code=400, detail=&quot;Invalid Token&quot;)
    
    return datetime.now()


def get_current_user(now: datetime = Depends(get_token_header)):
    &quot;&quot;&quot;
    현재 사용자를 반환합니다. 여기서는 단순히 요청 시각을 반환합니다.
    &quot;&quot;&quot;
    return {&quot;time&quot;: f&quot;{now}&quot;}


@app.get(&quot;/users/me&quot;)
def read_current_user(current_user: dict = Depends(get_current_user)):
    &quot;&quot;&quot;
    현재 사용자의 정보를 반환하는 엔드포인트입니다.
    &quot;&quot;&quot;
    return current_user&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;code&gt;get_current_user&lt;/code&gt;는 &lt;code&gt;get_token_header&lt;/code&gt;를 서브-의존성으로 사용하여 토큰 검증 후 현재 시간을 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1 서브-의존성 정리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; API 문서를 보자, header 로 Authorization 을 요구함을 알 수 있다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;요구 헤더.png&quot; data-origin-width=&quot;2934&quot; data-origin-height=&quot;1390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kUySB/btsK7C0z3yt/JdKKLgrGjajPqbkkMGHYYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kUySB/btsK7C0z3yt/JdKKLgrGjajPqbkkMGHYYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kUySB/btsK7C0z3yt/JdKKLgrGjajPqbkkMGHYYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkUySB%2FbtsK7C0z3yt%2FJdKKLgrGjajPqbkkMGHYYK%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;600&quot; height=&quot;284&quot; data-filename=&quot;요구 헤더.png&quot; data-origin-width=&quot;2934&quot; data-origin-height=&quot;1390&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 요청을 보내보자&lt;/p&gt;
&lt;pre id=&quot;code_1733311930572&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -X 'GET' \
  'http://127.0.0.1:8000/users/me' \
  -H 'accept: application/json' \
  -H 'Authorization: bearer expected_token'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 처리됨을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 서브-의존성을 통핸 의존성 체인 구성은 결국 의존성 Stack(가정이다...) 의 최상단에 있는 의존성부터 순차적으로 처리해나가고 결국 API 에서는 최상단에서 요청하는 값을 요구하게 된다. 그리고 각 의존성 함수가 수행되면서 자신을 호출한 호출자에게 반환값을 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이정도면 의존성 체이닝이라고 부를 수도 있지 않을까?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 id=&quot;global-dependencies&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 전역 의존성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역 의존성은 애플리케이션 전체에 걸쳐 모든 경로 연산자에 적용되는 의존성이다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;from fastapi import Depends, FastAPI

app = FastAPI()

def verify_token(token: str):
    if token != &quot;secret-token&quot;:
        raise HTTPException(status_code=400, detail=&quot;Invalid Token&quot;)
    return token

app = FastAPI(dependencies=[Depends(verify_token)])

@app.get(&quot;/protected-route/&quot;)
def protected_route():
    return {&quot;message&quot;: &quot;Access granted&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에서는 &lt;code&gt;verify_token&lt;/code&gt; 의존성을 전역 의존성으로 설정하여 모든 경로 연산자에 적용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 전역 의존성은 FastAPI 구현체 혹은 APIRouter 구현체에 적용할 수 있다.&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;4.1 전역의존성 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역 의존성의 역할이 미들웨어와 다른점이 무엇인지 고민이 생기는 지점이다. 그래서 GTP 에게 물어봤다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;전역의존성vs미들웨어.png&quot; data-origin-width=&quot;1990&quot; data-origin-height=&quot;1190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bECdyd/btsK7bPyRPC/KiC0JKlulcazpIHxjMQrv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bECdyd/btsK7bPyRPC/KiC0JKlulcazpIHxjMQrv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bECdyd/btsK7bPyRPC/KiC0JKlulcazpIHxjMQrv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbECdyd%2FbtsK7bPyRPC%2FKiC0JKlulcazpIHxjMQrv1%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;600&quot; height=&quot;359&quot; data-filename=&quot;전역의존성vs미들웨어.png&quot; data-origin-width=&quot;1990&quot; data-origin-height=&quot;1190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;</description>
      <category>탐구 생활/FastAPI</category>
      <category>FastAPI</category>
      <category>fastapi 의존성</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/62</guid>
      <comments>https://probehub.tistory.com/62#entry62comment</comments>
      <pubDate>Wed, 4 Dec 2024 20:05:21 +0900</pubDate>
    </item>
    <item>
      <title>오픈소스 초보자의 FastAPI 기여하기</title>
      <link>https://probehub.tistory.com/60</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 FastAPI 로 전환을 하기에 &lt;a href=&quot;https://fastapi.tiangolo.com/learn/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;FastAPI Document&lt;/a&gt; 를 보면서 공부중이었다. 한국어로 번역된 문서도 있는반면 영어 그대로인 문서들도 있었다. 왜일까? 하고 찾아보던중 &lt;a href=&quot;https://github.com/fastapi/fastapi&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;FastAPI Github&lt;/a&gt;&amp;nbsp;에서 번역활동이 진행중이며, &lt;a href=&quot;https://github.com/fastapi/fastapi/discussions/3167&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;한국인 번역&lt;/a&gt; 참여자는 16명 정도라는 것을 알게 되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대학생때 수업 대부분을 영어로 들었고 점수도 나쁘지 않았다는 자신감에 그 번역, 내가 하면 되는거 아닌가 하는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 공부도 하고, 2) 비록 번역이지만 오픈소스에 기여도 해보고, 3) 한국어로 된 자료가 늘어나면 FastAPI 가 한국에 더 뿌리내릴수도 있고, 1석 3조의 전략이었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;첫번째 PR 을 위한 노력&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 걸림돌이 하나 있었다. 오픈소스에 RP을 날리는 방법을 모른다. 그동안 Spring 코드 까본다고 Spring 소스 clone 만 해봤지 그 소스코드에 내가 무언가를 해본적은 없었다. 그래서 아래와 같이 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 구글에 &quot;How to contribute open source project in github&quot; 검색하고 관련 글 하나 읽기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 내가 기여하려고 하는 프로젝트의 기여 규칙을 찾는다. (아마 90% 이상의 관련 글이 해당 프로젝트의 &quot;룰&quot;을 준수하라고 했을 것이다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 그래서 FastAPI 에서 한국어 번역에 관한 규칙을 찾았다. 정형화된 커밋 메시지의 패턴을 찾아내고, 관련 Discussion 에서 원래 활동하시던 번역 기여자분들의 규칙을 찾았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) 이제는 Fork 를 하고 내가 번역하고자 하는 문서를 번역하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PR 을 다 해보고 찾은 정보인데 오픈소스에 기여하는 연습을 해볼 수 있는 &lt;a href=&quot;https://github.com/firstcontributions/first-contributions&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;repository&lt;/a&gt; 가 있다. (부디 다른 초심자분들은 미리 찾아서 도움을 받을 수 있길 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 노력해서 올린 &lt;a href=&quot;https://github.com/fastapi/fastapi/pull/12937&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;첫번째 PR&lt;/a&gt; 은 내가 스스로 Close 했다. 의욕이 너무 앞서서 번역한 2개의 문서를 하나의 PR 에 묶어버린 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 다시 작업을 했고, &lt;a href=&quot;https://github.com/fastapi/fastapi/pull/12940&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;두번째 PR&lt;/a&gt; 은 리뷰를 받고 최종적으로 Merge 까지 될 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째 PR 의 리뷰어분께서 찾아주신 오류는 2개의 사소한 오류였다. 번역 규칙을 찾아보고 적용하고자 했던 노력이 빛을 발한것 가아 뿌듯했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;리뷰1.png&quot; data-origin-width=&quot;963&quot; data-origin-height=&quot;838&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMJODy/btsK4Ptbktk/KSGDPncof7ilPerH1Zn9jK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMJODy/btsK4Ptbktk/KSGDPncof7ilPerH1Zn9jK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMJODy/btsK4Ptbktk/KSGDPncof7ilPerH1Zn9jK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMJODy%2FbtsK4Ptbktk%2FKSGDPncof7ilPerH1Zn9jK%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;700&quot; height=&quot;609&quot; data-filename=&quot;리뷰1.png&quot; data-origin-width=&quot;963&quot; data-origin-height=&quot;838&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;첫번째 Review 를 위한 노력&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째 PR 이 Merge 가 된 이후 &lt;a href=&quot;https://github.com/fastapi/fastapi/pull/12968&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;세번째 PR&lt;/a&gt; 을 올렸다. 내가 평소에 중요하게 생각하는 테스트에 대한 내용이어서 즐겁게 번역했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 지난번엔 PR 요청을 보내고 7일 뒤에 리뷰를 받았었는데, 이번엔 14일이 다 지나도록 리뷰가 안되는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떡할지 고민하다가 다른 한국어 번역 기여자 분이 PR 올린게 있어서 봤다. 이분의 PR 도 아직 리뷰를 받지 못하고 있었다. 그러다 문뜩 Discuss 에서 봤던 말이 떠올랐다. '한국어 번역을 리뷰할 수 있는 자격은 굳이 따지자면 한국어 네이티브 정도입니다. 활발히 리뷰해주세요' 이런 내용이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 나도 리뷰 할 수 있나? 하는 생각이 들어서 변경된 파일(Files changed) 버튼을 눌렀다. 역시나 리뷰가 가능했다. 마침 나의 두번째 PR 을 리뷰해주셨던 분이었기에 정성스럽게 리뷰해야겠다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;첫 리뷰.png&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;801&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bykwI6/btsK8wen57N/k4TLbqPMe5Uxkpfxu9rZfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bykwI6/btsK8wen57N/k4TLbqPMe5Uxkpfxu9rZfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bykwI6/btsK8wen57N/k4TLbqPMe5Uxkpfxu9rZfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbykwI6%2FbtsK8wen57N%2Fk4TLbqPMe5Uxkpfxu9rZfK%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;700&quot; height=&quot;563&quot; data-filename=&quot;첫 리뷰.png&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;801&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번역 규칙 준수는 문제가 없었지만 번역이 애매한 용어가 나왔다. 나는 관련 영어문서에서 유사한 단어가 있는지 찾아보고 해당 문서를 번역한 기존 한국어 문서에서는 어떻게 다뤘는지 파악한 뒤 리뷰를 할 수 있었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;첫번째 오픈소스 기여를 해본 소감&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;- 번역이라고 할지라도 PR 이나 리뷰를 하면서 공부에 많이 도움이 된다. 특히 리뷰같은 경우는 기존 영어문서, 한글문서를 모두 읽어야 의미있는 리뷰가 가능했기에 더욱 값진 경험이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 오픈소스를 주로 관리하는 사람이 있는것 같다. 거의 bot 마냥 PR 들에 태그를 붙이는 사람이 있는데...열정이 대단하다.&lt;/p&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 이라는 플랫폼을 다시 보게된것 같다. 이제는 관심있는 Repository 에 별표시를 하고 주기적으로 보게되었다. 세상엔 멋진 프로젝트가 많다.&lt;/p&gt;</description>
      <category>탐구 생활/FastAPI</category>
      <category>fastapi 기여하기</category>
      <category>fastapi 오픈소스</category>
      <category>oss contribute</category>
      <category>oss 기여</category>
      <category>오픈소스 기여</category>
      <category>오픈소스 컨트리뷰트</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/60</guid>
      <comments>https://probehub.tistory.com/60#entry60comment</comments>
      <pubDate>Tue, 3 Dec 2024 22:21:21 +0900</pubDate>
    </item>
    <item>
      <title>FastAPI: fastapi-permissions 를 이용한 접근제어</title>
      <link>https://probehub.tistory.com/58</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;FastAPI security.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ejZAiD/btsK4hVI7S3/fWuk0ptOkmM1DguuOZLVjk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ejZAiD/btsK4hVI7S3/fWuk0ptOkmM1DguuOZLVjk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ejZAiD/btsK4hVI7S3/fWuk0ptOkmM1DguuOZLVjk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FejZAiD%2FbtsK4hVI7S3%2FfWuk0ptOkmM1DguuOZLVjk%2Fimg.webp&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;350&quot; height=&quot;350&quot; data-filename=&quot;FastAPI security.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;fastapi-permissions&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fastapi-permissions는 Pyramid 프레임워크의 &lt;b&gt;&lt;a href=&quot;https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/security.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Pyraid Securiy&lt;/a&gt;&amp;nbsp;&lt;/b&gt;기능에서 영감을 받아 만들어진&amp;nbsp;&lt;b&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://github.com/holgi/fastapi-permissions&quot;&gt;라이브러리&lt;/a&gt;&lt;/b&gt;이다. 2024년 12월 기준으로 마지막 업데이트가 4년 전이어서 최신 FastAPI와의 호환성에 의문이 들지만, 접근 방식이 흥미로워 살펴보게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fastapi-permissions 가 갖는 주요 철학과 개념, 내구 동작 방식을 살펴본다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 주요 철학&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fastapi-permissions 가 FastAPI security 와는 다른 점은 scope 로 접근을 제어하는게 아니라 &lt;b&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;style5&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 주요 개념&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI에는 기본적으로 제공되지 않는 4가지 새로운 개념이 도입되었다.&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;Resource (리소스)&lt;/b&gt;: 관리 대상 엔티티, Access Control List 를 제공한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ACL (Access Control List, &lt;b&gt;접근 제어 리스트&lt;/b&gt;)&lt;/b&gt;: 리소스별 권한 규칙 집합, &lt;b&gt;(액션, 주체, 권한) 들의 집합&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Principals (주체)&lt;/b&gt;: 사용자 및 그룹, 정의에 제한을 두지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Permissions (권한)&lt;/b&gt;: 리소스에서 수행할 수 있는 작업, 정의에 제한을 두지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요개념들의 이름과 역할만 들어서 어떤식으로 접근제어를 하는지 감이 잡힌다. &quot;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;특정&amp;nbsp;&lt;/span&gt;리소스&lt;span style=&quot;text-align: left;&quot;&gt;의&amp;nbsp;&lt;/span&gt;권한 규칙&lt;span style=&quot;text-align: left;&quot;&gt;이 있을때,&amp;nbsp;&lt;/span&gt;접근 주체&lt;span style=&quot;text-align: left;&quot;&gt;가&lt;/span&gt;&amp;nbsp;권한&lt;span style=&quot;text-align: left;&quot;&gt;을 가지고 있는지 검증한다.&quot;&amp;nbsp;&lt;/span&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.1. Resource &amp;amp; ACL&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예시에서 관리대상 엔티티(Resource)는 User 이다. User 클래스에 __acl__을 구현하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1733043810060&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from typing import List
from fastapi_permissions import Allow, Authenticated

# 사용자 데이터 모델 (ACL 포함)
class UserInDB:
    def __init__(self, id: int, email: str, full_name: str, roles: List[str]):
        self.id = id
        self.email = email
        self.full_name = full_name
        self.roles = roles

    def __acl__(self):
        return [
            (Allow, Authenticated, &quot;view&quot;),
            (Allow, f&quot;user:{self.id}&quot;, (&quot;edit&quot;, &quot;delete&quot;)),
            (Allow, &quot;role:admin&quot;, (&quot;view&quot;, &quot;edit&quot;, &quot;delete&quot;)),
        ]
    
user_db: List[UserInDB] = [
    UserInDB(id=i, email=f&quot;mail{i}@gmail.com&quot;, full_name=f&quot;name_{i}&quot;, roles=[&quot;user&quot;]) for i in range(10)
]&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.2. Principl &amp;amp; Permissions&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현된 접근 제어 리스트(acl) 이 반환하는 tuple 중 하나를 &lt;b&gt;접근 제어 항목(Access Control Item)&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;(액션, 주체(Principal), 권한(Permission)), &lt;/b&gt;따라서 (Allow, Authenticated, &quot;view&quot;) 라는 접근 제어 항목은 어떤 방식으로든 Authenticated 되었다고 판단된 주체는 &quot;view&quot; 권한이 허가(Allow) 된다는 것을 의미한다.&lt;/p&gt;
&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;액션:&lt;/b&gt;&amp;nbsp;Allow 혹은 Deny 가 가능하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주체: &lt;/b&gt;라이브러리에서 이미 정해놓은 EveryOne, Authenticated 를 제외한 나머지는 개발자가 정의하여 넣을 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;권한: &lt;/b&gt;라이브러리에서 정해진게 없다. 개발자가 원하는대로 설정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.3 에제 코드&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서상 주요객체와 함수를 설명해야겠지만 개인적으로 새로운 내용을 학습하고나서 그게 구현된 코드를 보는게 도움이 되었다. 그래서 이상의 개념들을 활용해서 간단한 API 와 접근제어를 구현하였다. 유저 정보를 DB 에서 읽어와서 pydantic model 로 변환하여 반환하는 것처럼 동작하는 GET 엔드포인트 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 직접 python 파일에 넣고 필요한 의존성을 설치한 뒤&amp;nbsp; http://localhost:8000/users/1 등을 해보면 유저의 정보가 조회되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1733094830438&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from typing import List, Optional

from fastapi import FastAPI, HTTPException, status
from fastapi_permissions import Allow, Authenticated, configure_permissions, Everyone
from pydantic import BaseModel


# 사용자 데이터 모델 (ACL 포함)
class UserInDB:
    def __init__(self, id: int, email: str, full_name: str, roles: List[str]):
        self.id = id
        self.email = email
        self.full_name = full_name
        self.roles = roles

    def __acl__(self):
        return [
            (Allow, Authenticated, &quot;view&quot;),
            (Allow, f&quot;user:{self.id}&quot;, (&quot;edit&quot;, &quot;delete&quot;)),
            (Allow, &quot;role:admin&quot;, (&quot;view&quot;, &quot;edit&quot;, &quot;delete&quot;)),
        ]

# 사용자 읽기 모델 (Pydantic)
class UserRead(BaseModel):
    id: int
    email: str
    full_name: str
    roles: List[str]

# 데이터베이스 시뮬레이션
user_db: List[UserInDB] = [
    UserInDB(id=i, email=f&quot;mail{i}@gmail.com&quot;, full_name=f&quot;name_{i}&quot;, roles=[&quot;user&quot;]) for i in range(10)
]

# 사용자 인증 및 주체(principals) 생성
def get_active_principals(user_id: int) -&amp;gt; List[str]:
    # 데이터베이스에서 사용자 조회
    user = next((u for u in user_db if u.id == user_id), None)
    if user:
        principals = [Everyone, Authenticated, f&quot;user:{user.id}&quot;] + [f&quot;role:{role}&quot; for role in user.roles]
        principals.extend(getattr(user, &quot;principals&quot;, []))
    else:
        principals = [Everyone]

    return principals

# 특정 사용자 리소스 반환
def get_user_resource(user_id: int) -&amp;gt; Optional[UserInDB]:
    user = next((u for u in user_db if u.id == user_id), None)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=&quot;User not found&quot;,
        )
    return user

# 권한 검사 시스템 설정
Permission = configure_permissions(get_active_principals)

# FastAPI 앱 초기화
app = FastAPI()

# 사용자 정보 조회 엔드포인트
@app.get(&quot;/users/{user_id}&quot;, response_model=UserRead)
async def get_user(
    user: UserInDB = Permission(&quot;view&quot;, get_user_resource),
):
    return UserRead(**user.__dict__)&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;get_active_principals(user_id: int)는&amp;nbsp; user_id 가 user_db 리스트에 있으면 user 정보를 활용해 principal 을 생성해 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;get_user_resource(user_id: int) 는 user_id 가 user_db 리스트에 있으면 user 정보를 반환한다.&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;그리고 아직 뭔지 모르는 Permission 이라는 객체는 configure_permissions(get_active_principals) 를 통해 생성되더니, 엔드포인트 파라미터에서는 객체 자체를 주입하고 &quot;view&quot; (요구되는 permission 일 것이다) 와 get_user_resource 를 이용해 구현체를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적절한 유저 정보를 반환한다는 사실과, get_active_principals 가 EveyOne 만 반환하게 바꾸면 엔드포인트가 403을 반환하다는 사실에 기반하여&amp;nbsp; &lt;b&gt;어떤식으로든&lt;/b&gt; Permission 객체는 user_id 를 읽어와서 get_user_resource 와 get_active_principals 를 호출하여 접근 제어와 유저정보 반환을 모두 수행한다는 것을 추측할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 직관적인 흐름이 그려졌다. 이제 세부적으로 파고들어가서 어떻게 이런 일이 가능한건지 알아보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. fastapi-permission 내부동작 파헤치기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 동작을 세부적으로 파고들기에 앞서 결론부터 요약하자면 configure_permissions 함수를 통해 만들어낸 Permissions 이라는 객체는 내부적으로 has_permission 을 호출하여 접근주체(principal) 자원(resource)에 접근권한(permission) 이 있는지 체크하는 것으로 동작한다. 이때 fastapi 에서 제공하는 Depends 를 활용하여 동작을 추상화한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_fastapi-permissions 흐름 요약.png&quot; data-origin-width=&quot;1794&quot; data-origin-height=&quot;1043&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyTRL7/btsLEX4Hebl/DQsWkzsWiKjHfpYIhFabG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyTRL7/btsLEX4Hebl/DQsWkzsWiKjHfpYIhFabG1/img.png&quot; data-alt=&quot;fastapi-permissions 동작 요약&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyTRL7/btsLEX4Hebl/DQsWkzsWiKjHfpYIhFabG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyTRL7%2FbtsLEX4Hebl%2FDQsWkzsWiKjHfpYIhFabG1%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;1794&quot; height=&quot;1043&quot; data-filename=&quot;edited_fastapi-permissions 흐름 요약.png&quot; data-origin-width=&quot;1794&quot; data-origin-height=&quot;1043&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;fastapi-permissions 동작 요약&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 fastapi-permissions 라이브러리 README.md 에서 언급한 함수와 객체 위주로 코드를 까보자&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1. 주요 객체와 함수&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;has_permissions(&lt;/b&gt;principals, permission, resource&lt;b&gt;):&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주어진 사용자(principals)가 특정 자원(resource) 에 대한 권한(permission)이 있는지 검사하여 True/False 를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;list_permissions(&lt;/b&gt;princiapals, resource&lt;b&gt;):&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주어진 사용자(principals)가 특정 자원(resource) 에 대해 가질 수 있는 모든&amp;nbsp; 권한(permission)을 반환다. dict를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Permission:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI 경로(Path Operation)에서 Depends 를 사용하여 권한 검사를 간단히 처리할 수 있도록 도와주는 의존성 객체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 이미 &lt;b&gt;Depends 로 감싸져있기 때문에&lt;/b&gt; 별도로 Depneds 를 설정할 필요는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명시적으로는 confiture_permissions() 를 이용하여 생성하고, 내부적으로는 permission_dependency_factory() 가 생성한 함수를 사용하여 동작한다.&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;size18&quot;&gt;&lt;b&gt;permission_dependency_factory(&lt;/b&gt;permission, resource, active_principals_func, permission_exception&lt;b&gt;):&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 검사를 위한 FastAPI 의존성(dependency)을 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 principals 와 resource 를 가져와 has_permissions() 를 호출하여 권한을 검사한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자(principals) 이 접근 권한이 없다고 판단되면 permission_exception 을 일으킨다. (default 값 변경가능)&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;size18&quot;&gt;&lt;b&gt;configure_permissions(&lt;/b&gt;active_principals_func, permission_execption&lt;b&gt;):&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;permission_dependency_factory 를 단순화한 버전이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략적으로 살펴봤을때 중요한 역할을 하는 함수는 permission_dependency_factory() 와 configure_permissions() 으로 보인다. 이들을 중점적으로 살펴보자&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.2. permission_dependency_factory():&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스코드를 까보자&lt;/p&gt;
&lt;pre id=&quot;code_1733097245966&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def permission_dependency_factory(
    permission: str,
    resource: Any,
    active_principals_func: Any,
    permission_exception: HTTPException,
):
    if callable(resource):
        dependable_resource = Depends(resource)
    else:
        dependable_resource = Depends(lambda: resource)

    # to get the caller signature right, we need to add only the resource and
    # user dependable in the definition
    # the permission itself is available through the outer function scope
    def permission_dependency(
        resource=dependable_resource, principals=active_principals_func
    ):
        if has_permission(principals, permission, resource):
            return resource
        raise permission_exception

    return Depends(permission_dependency)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검사할 권한(permission) 과 대상 자원(resource)를 전달받아서 의존성 주입을 받도록 만들고, 전달받은 접근자 정보를 얻는 방법(active_principals_func)를 활용하여 has_permission 으로&amp;nbsp; 권한을 검사하는 permission_dependency 라는 함수를 정의하여 반환한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위의 예제코드에서 get_user_resource 가 의도한대로 동작할 수 있었던 이유는 FastAPI 엔드포인트에 의존성 (Depends) 으로 설정되어 주입됐기 때문이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 active_principals_func 는 어떻게 그게 가능했을까? 이 함수는 FastAPI 의존성(Depends) 으로 선언되어 있지 않다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 비밀은 다음에 자세히 살펴볼 configure_permissions() 함수에 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.3. &lt;/b&gt;&lt;b&gt;configure_permissions():&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;confiture_permissions() 는 permission_dedendency_factory 를 쉽게 사용하는 편의 함수정도로 얘기했지만 사실 중요한 차이가 있다. 바로 &lt;b&gt;configure_permissions() 를 이용해야 active_principals_func 에 의존성(Depends)이 설정&lt;/b&gt;된다는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1733097340515&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def configure_permissions(
    active_principals_func: Any,
    permission_exception: HTTPException = permission_exception,
):

    active_principals_func = Depends(active_principals_func)

    return functools.partial(
        permission_dependency_factory,
        active_principals_func=active_principals_func,
        permission_exception=permission_exception,
    )&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;pre id=&quot;code_1733098478009&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 권한 검사 시스템 설정, 이때 넘긴 get_active_principals 함수는 FastAPI 의존성으로 설정된다.
# Permission 객체는 permission_dependency_factory() 를 functuools.partial 에 의해 호출하는 callable 객체이다.
# functuools.partial 덕분에 외부 클로저를 활용할 수 있어서 Permission 객체에는 permission: str, resource: Any 만 전달하면 된다.
Permission = configure_permissions(get_active_principals)

app = FastAPI()

# 사용자 정보 조회 엔드포인트
@app.get(&quot;/users/{user_id}&quot;, response_model=UserRead)
async def get_user(
    user: UserInDB = Permission(&quot;view&quot;, get_user_resource),
):
	# Permission 을 호출함으로써 get_user_resource 는 FastAPI 의존성으로 설정되었다.
    # 내부적으로 get_active_principals 함수와 get_user_resource 함수가 돌아가고 
    # has_permission 을 통해 &quot;view&quot; 권한이 있는지 검사한다.
    return UserRead(**user.__dict__)&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>탐구 생활/FastAPI</category>
      <category>FastAPI</category>
      <category>fastapi security</category>
      <category>fastapi 접근제어</category>
      <category>fastapi-permissions</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/58</guid>
      <comments>https://probehub.tistory.com/58#entry58comment</comments>
      <pubDate>Sun, 1 Dec 2024 17:58:16 +0900</pubDate>
    </item>
    <item>
      <title>MSA 주요 패턴: 중앙화된 로그 집계 패턴</title>
      <link>https://probehub.tistory.com/57</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 중앙화된 로그 집계 패턴이란?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이크로서비스 아키텍처(MSA)는 독립적인 서비스로 구성되기 때문에 서비스별로 로그를 개별적으로 수집하면 문제를 추적하거나 디버깅하는 과정에서 복잡성과 비효율이 증가하기 마련이다. &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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 왜 중앙화된 로그 집계가 필요한가?&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.1. MSA 환경에서의 로그 관리의 어려움&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1&lt;/b&gt; &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;MSA 환경에서는 수십에서 수백 개의 마이크로서비스가 각각 독립적으로 로그를 생성한다.&lt;/li&gt;
&lt;li&gt;이러한 로그를 각 서비스에서 확인하려면 많이 번거로워서 에러 발생 원인을 빠르게 찾기 어려워진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2&lt;/b&gt; &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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3&lt;/b&gt; &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;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.2. 중앙화된 로그 집계의 장점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;b&gt;빠른 문제 진단&lt;/b&gt;: 모든 서비스의 로그를 한곳에서 조회할 수 있으므로, 장애 원인을 신속히 파악할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관된 로그 분석&lt;/b&gt;: 중앙화된 로그 시스템은 공통 포맷으로 데이터를 통합하여 검색 및 분석이 용이하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장성 및 모니터링&lt;/b&gt;: 로그를 수집하고 분석하여 실시간 모니터링 및 알림 설정이 가능하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;규제 준수 및 감사&lt;/b&gt;: 로그 데이터를 중앙에서 관리하면 보안 및 규제 준수 요건을 충족하기가 더 쉽다..&lt;/li&gt;
&lt;/ul&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;b&gt;3. 중앙화된 로그 집계 아키텍처&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1. 구성 요소&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1&lt;/b&gt; &lt;b&gt;로그 수집기&lt;/b&gt;: 각 서비스의 로그를 수집하여 중앙 로그 시스템으로 전송. (eg. Filebeat, Fluentd, Logstash)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2&lt;/b&gt; &lt;b&gt;중앙 로그 저장소&lt;/b&gt;: 수집된 로그를 저장하는 장소. (eg. Elasticsearch, Amazon S3)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3&lt;/b&gt; &lt;b&gt;로그 분석 및 시각화 도구: &lt;/b&gt;저장된 로그를 분석하고 시각화. (eg. Kibana, Grafana, Datadog)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4&lt;/b&gt; &lt;b&gt;알림 및 모니터링: &lt;/b&gt;특정 조건(에러 발생, 트래픽 급증 등)을 감지하여 알림을 발송. (eg.PagerDuty, Slack)&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;b&gt;4. 중앙화된 로그 집계 구현 시 고려사항&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 성능&lt;/b&gt;: 로그 수집기가 과도한 부하를 유발하지 않도록 설계, 로그 전송 속도와 중앙 저장소의 처리 속도를 최적화.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 보안&lt;/b&gt;:&amp;nbsp; 로그 데이터에 민감한 정보(예: 사용자 데이터, 인증 토큰)가 포함되지 않도록 마스킹 또는 필터링, 로그 저장소에 접근 제어.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3&lt;/b&gt;. 데이터 보존 정책: 로그 데이터를 무기한 저장하면 비용이 증가하므로, 적절한 보존 기간 설정(예: 30일, 90일).&lt;/p&gt;</description>
      <category>기초 지식/MSA</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/57</guid>
      <comments>https://probehub.tistory.com/57#entry57comment</comments>
      <pubDate>Wed, 27 Nov 2024 00:40:29 +0900</pubDate>
    </item>
    <item>
      <title>MSA 전환을 위한 서비스 분리 전략</title>
      <link>https://probehub.tistory.com/56</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 Monolith 로 개발해오던 프로젝트들을 MSA 로 옮겨간다고 한다. 내가 맡아서 개발해온 프로젝트의 도메인 지식은 내가 가장 잘 알고 있다보니 이 프로젝트 만큼은 내가 도메인 분리를 맡게 될 것 같다...그래서 공부중이다.&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;b&gt;서비스 분리 전략 3가지 Key Point&lt;/b&gt;&lt;/h3&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;li&gt;서비스 분리를 깔끔하게 나눠주는 마법같은 공식은 없다. (이 부분이 너무 슬펐다.)&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;&lt;b&gt;서비스 분리 8가지 원칙&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;첫 번째 원칙: 작고 분리가 쉬운 서비스로 워밍업&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간단한 서비스를 분리하며 역량을 내재화.&lt;/li&gt;
&lt;li&gt;신규 개발 기능이나 내부 의존성이 적고, 중요도가 낮은 기능부터 시작.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Pilot 서비스 분리의 기준&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내부 의존성이 낮은 기능(Core 의존성, 데이터 의존성 적음).&lt;/li&gt;
&lt;li&gt;장애 발생 시 시스템 전체에 영향이 적은 기능.&lt;/li&gt;
&lt;li&gt;Fallback 처리 가능한 기능.&lt;/li&gt;
&lt;li&gt;테이블 크기가 작고 데이터가 적은 기능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;두 번째 원칙: 핵심 기능의 분리 원칙&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;워밍업 이후 핵심 기능 분리를 고려.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;핵심 기능 분리의 어려움&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결합도가 높고, 도메인 경계가 명확하지 않음.&lt;/li&gt;
&lt;/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;도메인 정의: 이벤트 스토밍으로 Sub-Domain 및 Bounded Context 발견.&lt;/li&gt;
&lt;li&gt;의존성 분석: 정적 분석 도구(Structure101, Stan4J 등) 활용.&lt;/li&gt;
&lt;li&gt;조직 기반 분리: 비즈니스 팀 구조와 일치하는 서비스 경계를 설정.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;세 번째 원칙: 데이터 분리의 원칙&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 간 결합도를 최소화하기 위해 데이터베이스 분리가 필수.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Anti-Pattern&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;/li&gt;
&lt;li&gt;서비스별 독립적인 데이터 소유.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;네 번째 원칙: 분리 대상 선정의 원칙&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;분리 대상 선정 질문&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 문제가 되는 기능은 무엇인가?&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;&lt;b&gt;분리 대상 선정 방법&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 커밋 히스토리 분석.&lt;/li&gt;
&lt;li&gt;프로젝트 로드맵 기반으로 선정.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;다섯 번째 원칙: 코드의 재사용 vs 재개발&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기존 코드 재사용&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기술 부채가 많아 비효율적일 가능성.&lt;/li&gt;
&lt;li&gt;오래된 기술 스택으로 Boilerplate Code가 많음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재개발의 장점&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인 요구사항 재정의 가능.&lt;/li&gt;
&lt;li&gt;기술 부채 해결 및 새로운 기술 스택 도입.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;여섯 번째 원칙: 진화적인 서비스 분리&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;서비스 크기 결정의 원칙&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Go Macro, then Micro: 먼저 크게 분리, 이후 필요에 따라 세분화.&lt;/li&gt;
&lt;li&gt;지나치게 작은 서비스는 인프라 복잡도 증가.&lt;/li&gt;
&lt;li&gt;Two Pizza Team, 독립 배포 가능성, 2주 내 재작성 가능 수준.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;일곱 번째 원칙: 반복/점진적 분리&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Strangler Pattern&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;점진적으로 Monolith를 대체.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;여덟 번째 원칙: MSA와 신기술 도입&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MSA는 혁신과 실험을 가능하게 하지만, 초기 도입 시 신기술 적용은 신중해야 함.&lt;/li&gt;
&lt;li&gt;충분한 역량 내재화 후 점진적으로 신기술 도입.&lt;/li&gt;
&lt;/ul&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;b&gt;결론&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA 전환은 단순히 기술적인 변화가 아니라, 비즈니스와 기술을 통합적으로 고려해야 하는 전략적 과정입니다. 단계적이고 점진적인 접근, 도메인 중심 설계, 데이터 분리, 그리고 팀 구조와의 연계를 통해 성공적인 MSA 전환을 이룰 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고자료: 샘 뉴먼의 Building MicroServices (책)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고자료: monolith into microservice (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://martinfowler.com/articles/break-monolith-into-microservices.html&quot;&gt;블로그&lt;/a&gt;)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고자료: Straingler pattern (&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://akfpartners.com/growth-blog/strangler-pattern-dos-and-donts&quot;&gt;블로그&lt;/a&gt;)&lt;/p&gt;</description>
      <category>기초 지식/MSA</category>
      <category>msa 분리 원칙</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/56</guid>
      <comments>https://probehub.tistory.com/56#entry56comment</comments>
      <pubDate>Tue, 26 Nov 2024 23:18:59 +0900</pubDate>
    </item>
    <item>
      <title>Terraform AWS: VPC 만들기</title>
      <link>https://probehub.tistory.com/54</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 을 이용하여 로컬 리소스를 다루는것도 의미가 있겠으나, 내가 Terraform 을 알아보기 시작한 이유는 클라우드 인프라를 재사용가능한 효율적인 형태로 다루고 싶기 때문이었다.&lt;/p&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 의 기본 리소스중 하나인 VPC 를 만들어보겠다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;AWS VPC&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 지금부터 만들려고하는 AWS VPC 에 대한 개념과 이를 구성하는 세부 요소에 대해서 알아보고 왜 많고 많은 AWS 인프라 중에서 VPC 를 먼저 만들려고하는지 알아보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPC(Virtual Private Cloud) 란 AWS 클라우드 내에서 자신의 네트워크를 &quot;논리적&quot; 으로 격리하여 정의할 수 있는 가상 네트워크이다. 인터넷에 노출되면 안되는 리소스를 격리하여 보안에 필수적이며, 보안 그룹과&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/vpc-network-acls.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; 네트워크 ACL&lt;/a&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;VPC 는 가용 영역(AZ)이 아닌 Region 에 속하므로 여러 가용 영역에 걸쳐 네트워크를 확장할 수도 있다.&lt;/p&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 리소스의 네트워크를 먼저 정의하는 것은 필수 과정일 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;VPC의 구성 요소&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;AWS VPC.png&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;462&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btmRcn/btsKU70xzrZ/i1OLnTWPX2dfY8z52nN2Xk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btmRcn/btsKU70xzrZ/i1OLnTWPX2dfY8z52nN2Xk/img.png&quot; data-alt=&quot;간단하게 구성된 VPC&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btmRcn/btsKU70xzrZ/i1OLnTWPX2dfY8z52nN2Xk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtmRcn%2FbtsKU70xzrZ%2Fi1OLnTWPX2dfY8z52nN2Xk%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;700&quot; height=&quot;306&quot; data-filename=&quot;AWS VPC.png&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;462&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;간단하게 구성된 VPC&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. &lt;b&gt;서브넷 (Subnet)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브넷은 VPC 내에서 IP 주소 범위를 더 세분화한 논리적 단위이다. VPC는 여러 서브넷으로 나뉠 수 있으며, 각 서브넷은 특정 가용 영역에 연결된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;특징&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;퍼블릭 서브넷: 인터넷 게이트웨이와 연결되어 외부 인터넷과 통신 가능.&lt;/li&gt;
&lt;li&gt;프라이빗 서브넷: NAT 게이트웨이 등을 통해 간접적으로 인터넷에 액세스 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;용도&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;퍼블릭 서브넷: 웹 서버, 로드 밸런서와 같이 외부 인터넷과 통신이 필요한 리소스 배치.&lt;/li&gt;
&lt;li&gt;프라이빗 서브넷: 데이터베이스, 애플리케이션 서버와 같이 내부적으로만 접근 가능한 리소스 배치.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 라우팅 테이블 (Route Table)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우팅 테이블은 네트워크 트래픽의 경로를 정의하는 규칙의 집합이다. 서브넷은 반드시 라우팅 테이블과 연결되어야 하며, 이를 통해 트래픽이 어디로 전달될지 결정된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;구성&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 라우트: local로 설정된 라우트는 VPC 내의 모든 서브넷 간 통신을 허용.&lt;/li&gt;
&lt;li&gt;추가 라우트: 특정 목적지(CIDR)로 트래픽을 보내기 위해 IGW, NGW, VPN, Direct Connect 등을 지정.&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;퍼블릭 라우팅 테이블: 트래픽이 인터넷 게이트웨이(IGW)를 통해 외부 인터넷으로 이동하도록 설정.&lt;/li&gt;
&lt;li&gt;프라이빗 라우팅 테이블: 트래픽이 NAT 게이트웨이(NGW)를 통해 인터넷에 간접적으로 연결되도록 설정.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 인터넷 게이트웨이 (Internet Gateway, IGW)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷 게이트웨이는 VPC와 외부 인터넷 간의 통신을 가능하게 하는 네트워크 컴포넌트이다.&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;VPC당 하나의 IGW만 연결 가능.&lt;/li&gt;
&lt;li&gt;퍼블릭 서브넷과 연결된 리소스가 인터넷에서 액세스 가능하도록 지원.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;용도&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;퍼블릭 서브넷의 리소스(예: EC2 인스턴스)가 인터넷과 양방향으로 통신할 수 있도록 설정.&lt;/li&gt;
&lt;li&gt;일반적으로 웹 서버, API Gateway 등 외부와 직접 통신해야 하는 리소스에서 사용.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. &lt;b&gt;NAT 게이트웨이 (Network Address Translation Gateway, NGW)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NAT 게이트웨이는 프라이빗 서브넷의 리소스가 인터넷과 통신할 수 있도록 하지만, 외부에서 프라이빗 리소스를 직접적으로 액세스하지 못하도록 하는 역할을 한다.&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;퍼블릭 서브넷에 배치되며, Elastic IP를 사용.&lt;/li&gt;
&lt;li&gt;프라이빗 서브넷에서 오는 요청을 NAT 게이트웨이가 대행하여 외부와 통신.&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;프라이빗 서브넷의 리소스(예: 데이터베이스, 애플리케이션 서버)가 소프트웨어 업데이트나 외부 API 호출을 위해 인터넷과 통신할 때 사용.&lt;/li&gt;
&lt;li&gt;외부로부터의 직접적인 트래픽은 차단.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Terraform 으로 VPC 만들기&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. AWS CLI 준비&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근본적인 질문이 있다. Terraform 은 내 컴퓨터에서 돌아가고 결국 AWS 에서 제공하는 API 를 호출할텐데 이러한 API 호출에 대한 인증,인가를 어떻게 이뤄지는 걸까? Terraform 안에 AWS credential 정보를 넣어야 하는걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 Terraform 스크립트 안에 AWS credential 정보를 넣을 수도 있을 것이다. 하지만 그렇게 한다면 과연 이 스크립트를 팀내에서, 혹은 외부와 공유할 수 있을까?&lt;/p&gt;
&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://aws.amazon.com/ko/cli/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AWS CLI&lt;/a&gt; 이다. AWS CLI 를 이용하면 터미널에서 AWS API 를 이용하여 클라우드 인프라를 생성, 삭제, 조회 할 수 있으며, 무엇보다 aws configure 명령어를 통해 지금부터 요청을 보내려고하는 IAM role 을 정해줄 수 있다.&lt;/p&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 action 에서는 별도 설치없이 AWC CLI 를 이용할 수 있는 등 CI/CD 에서 자주 이용하게 된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 기본 Terraform 구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 변수 없이, 그저 static 하게 main.tf 하나로도&amp;nbsp; AWS VPC와 subent 을 public 과 private 로 나누고 라우트 테이블을 분리하여 인터넷 게이트웨이와 네트워크 게이트웨이로 라우팅하도록 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.tf 는 다음과 같다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1732458337797&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;provider &quot;aws&quot; {
  region = &quot;ap-northeast-2&quot;
}

# VPC 생성
resource &quot;aws_vpc&quot; &quot;my_vpc&quot; {
  cidr_block           = &quot;10.0.0.0/16&quot;
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = &quot;my-vpc&quot;
  }
}

# 인터넷 게이트웨이 생성
resource &quot;aws_internet_gateway&quot; &quot;my_igw&quot; {
  vpc_id = aws_vpc.my_vpc.id
  tags = {
    Name = &quot;my-igw&quot;
  }
}

# 퍼블릭 서브넷 생성
resource &quot;aws_subnet&quot; &quot;public_subnets&quot; {
  count = 3
  vpc_id                  = aws_vpc.my_vpc.id
  cidr_block              = cidrsubnet(&quot;10.0.0.0/16&quot;, 4, count.index) # /20 서브넷 생성
  availability_zone       = [&quot;ap-northeast-2a&quot;, &quot;ap-northeast-2b&quot;, &quot;ap-northeast-2c&quot;][count.index]
  map_public_ip_on_launch = true
  tags = {
    Name = &quot;public-subnet-${count.index + 1}&quot;
  }
}

# 프라이빗 서브넷 생성
resource &quot;aws_subnet&quot; &quot;private_subnets&quot; {
  count = 3
  vpc_id            = aws_vpc.my_vpc.id
  cidr_block        = cidrsubnet(&quot;10.0.0.0/16&quot;, 4, count.index + 3) # /20 서브넷 생성
  availability_zone = [&quot;ap-northeast-2a&quot;, &quot;ap-northeast-2b&quot;, &quot;ap-northeast-2c&quot;][count.index]
  tags = {
    Name = &quot;private-subnet-${count.index + 1}&quot;
  }
}

# NAT 게이트웨이 생성
resource &quot;aws_eip&quot; &quot;ngw_eip&quot; {
  domain = &quot;vpc&quot;
  tags = {
    Name = &quot;ngw-eip&quot;
  }
}

resource &quot;aws_nat_gateway&quot; &quot;nat_gateway&quot; {
  allocation_id = aws_eip.ngw_eip.id
  subnet_id     = aws_subnet.public_subnets[0].id # 첫 번째 퍼블릭 서브넷에 배치
  tags = {
    Name = &quot;nat-gateway&quot;
  }
}

# 퍼블릭 라우팅 테이블 생성
resource &quot;aws_route_table&quot; &quot;public_route_table&quot; {
  vpc_id = aws_vpc.my_vpc.id
  tags = {
    Name = &quot;public-route-table&quot;
  }
}

# 퍼블릭 라우팅 테이블에 IGW 추가
resource &quot;aws_route&quot; &quot;public_route&quot; {
  route_table_id         = aws_route_table.public_route_table.id
  destination_cidr_block = &quot;0.0.0.0/0&quot;
  gateway_id             = aws_internet_gateway.my_igw.id
}

# 퍼블릭 서브넷과 퍼블릭 라우팅 테이블 연결
resource &quot;aws_route_table_association&quot; &quot;public_associations&quot; {
  count          = 3
  subnet_id      = aws_subnet.public_subnets[count.index].id
  route_table_id = aws_route_table.public_route_table.id
}

# 프라이빗 라우팅 테이블 생성
resource &quot;aws_route_table&quot; &quot;private_route_table&quot; {
  vpc_id = aws_vpc.my_vpc.id
  tags = {
    Name = &quot;private-route-table&quot;
  }
}

# 프라이빗 라우팅 테이블에 NAT 게이트웨이 추가
resource &quot;aws_route&quot; &quot;private_route&quot; {
  route_table_id         = aws_route_table.private_route_table.id
  destination_cidr_block = &quot;0.0.0.0/0&quot;
  nat_gateway_id         = aws_nat_gateway.nat_gateway.id
}

# 프라이빗 서브넷과 프라이빗 라우팅 테이블 연결
resource &quot;aws_route_table_association&quot; &quot;private_associations&quot; {
  count          = 3
  subnet_id      = aws_subnet.private_subnets[count.index].id
  route_table_id = aws_route_table.private_route_table.id
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. static한 방식의 문제점, cidr 중복&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 VPC 리소스를 생성하면 결정적인 문제가 생긴다. 우선 재사용이 불가능하다는 것이다. 왜일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS CLI 가 연결되어 있다면 이런 커맨드를 입력해보자&lt;/p&gt;
&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. VPC 조회&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732456337888&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;aws ec2 describe-vpcs --query &quot;Vpcs[*].[VpcId, CidrBlock, Tags[?Key=='Name'].Value | [0]]&quot; --output table&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_vpc-table.png&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ljEKf/btsKTxfcnJh/0KtdLyMCwNumeAA6IJs77k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ljEKf/btsKTxfcnJh/0KtdLyMCwNumeAA6IJs77k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ljEKf/btsKTxfcnJh/0KtdLyMCwNumeAA6IJs77k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FljEKf%2FbtsKTxfcnJh%2F0KtdLyMCwNumeAA6IJs77k%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;650&quot; height=&quot;162&quot; data-filename=&quot;edited_vpc-table.png&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 결과를 반환한다. 원래 AWS 에서 기본으로 제공해주던 VPC 외에 위의 스크립트 때문에 생성된 my-vpc 가 보인다. 이 vpc 의 id를 이용하여 subent 들을 조회할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 특정 VPC 의 subent 조회&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새롭게 생성한 my-vpc 의 subent 들을 조회해보겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1732456115355&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;aws ec2 describe-subnets \
    --filters &quot;Name=vpc-id,Values={VPC_ID}&quot; \
    --query &quot;Subnets[*].[SubnetId, AvailabilityZone, MapPublicIpOnLaunch, CidrBlock]&quot; \
    --output table&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_subent-table.png&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPqIDQ/btsKUvViAl4/1nJdMVf3twBFqvOZP5x8i1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPqIDQ/btsKUvViAl4/1nJdMVf3twBFqvOZP5x8i1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPqIDQ/btsKUvViAl4/1nJdMVf3twBFqvOZP5x8i1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPqIDQ%2FbtsKUvViAl4%2F1nJdMVf3twBFqvOZP5x8i1%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;650&quot; height=&quot;204&quot; data-filename=&quot;edited_subent-table.png&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제점이 보이는가? 바로 위 스크립트에 명시했던 subent 이 규칙에 따라 생성된것이 확인된다. subent 리소스를 생성한 규칙을 다시 보겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1732456826319&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;resource &quot;aws_subnet&quot; &quot;public_subnets&quot; {
  count = 3
  vpc_id                  = aws_vpc.my_vpc.id
  cidr_block              = cidrsubnet(&quot;10.0.0.0/16&quot;, 4, count.index) # /20 서브넷 생성
  availability_zone       = [&quot;ap-northeast-2a&quot;, &quot;ap-northeast-2b&quot;, &quot;ap-northeast-2c&quot;][count.index]
  map_public_ip_on_launch = true
  tags = {
    Name = &quot;public-subnet-${count.index + 1}&quot;
  }
}

resource &quot;aws_subnet&quot; &quot;private_subnets&quot; {
  count = 3
  vpc_id            = aws_vpc.my_vpc.id
  cidr_block        = cidrsubnet(&quot;10.0.0.0/16&quot;, 4, count.index + 3) # /20 서브넷 생성
  availability_zone = [&quot;ap-northeast-2a&quot;, &quot;ap-northeast-2b&quot;, &quot;ap-northeast-2c&quot;][count.index]
  tags = {
    Name = &quot;private-subnet-${count.index + 1}&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;a href=&quot;https://developer.hashicorp.com/terraform/language/functions/cidrsubnet&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;cidrsubent&lt;/a&gt; 이다. cidrsubent 함수는 CIDR 블록을 분할 및 하위 네트워크 생성에 사용된다. 여기서는 VPC 의 cidr_block 을 &quot;10.0.0.0/16&quot; 로 정했기 때문에 이를 기준으로 새로운 하위 네트워크를 생성하도록 하였다. 즉, 다음에 &lt;b&gt;다른 Terraform 작업 공간에서 이 스크립트를 다시 이용할때 겹치지 않는 cidr 을 subent 에 부여한다는 것을 보장할 수도 없으며 무엇보다 VPC 의 cidr_block 이 겹치는 문제&lt;/b&gt;가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1 cidr 중복 문제의 해결&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 vpc 와 subent 의 cidr 을 읽어와서 처리하면 해결될 수 있는 문제이다. main.tf 제일 윗 부분을 이렇게 바꾸로&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1732459797916&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 기존 VPC 확인
data &quot;aws_vpcs&quot; &quot;existing_vpcs&quot; {}

# 기존 VPC CIDR 블록 가져오기
data &quot;aws_vpc&quot; &quot;vpc_details&quot; {
  count = length(data.aws_vpcs.existing_vpcs.ids)
  id    = data.aws_vpcs.existing_vpcs.ids[count.index]
}

# 기존 서브넷 CIDR 가져오기
data &quot;aws_subnets&quot; &quot;existing_subnets&quot; {}

data &quot;aws_subnet&quot; &quot;subnet_details&quot; {
  count = length(data.aws_subnets.existing_subnets.ids)
  id    = data.aws_subnets.existing_subnets.ids[count.index]
}

# 동적 CIDR 계산
locals {
  base_cidr      = &quot;10.0.0.0/8&quot; # CIDR 블록 기본 값
  existing_cidrs = concat(
    [for vpc in data.aws_vpc.vpc_details : vpc.cidr_block],
    [for subnet in data.aws_subnet.subnet_details : subnet.cidr_block]
  )
  new_vpc_cidr = cidrsubnet(local.base_cidr, 8, length(local.existing_cidrs)) # CIDR 범위 /16~ 사용
}&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_vpc 와 aws_subent 을 생성하는 블록을 변경하자&lt;/p&gt;
&lt;pre id=&quot;code_1732459867915&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;resource &quot;aws_vpc&quot; &quot;my_vpc&quot; {
  cidr_block           = local.new_vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = &quot;my-vpc&quot;
  }
}

# 퍼블릭 서브넷 생성
resource &quot;aws_subnet&quot; &quot;public_subnets&quot; {
  count = 3
  vpc_id                  = aws_vpc.my_vpc.id
  cidr_block              = cidrsubnet(local.new_vpc_cidr, 4, count.index) # /20 서브넷 생성
  availability_zone       = [&quot;ap-northeast-2a&quot;, &quot;ap-northeast-2b&quot;, &quot;ap-northeast-2c&quot;][count.index]
  map_public_ip_on_launch = true
  tags = {
    Name = &quot;public-subnet-${count.index + 1}&quot;
  }
}

# 프라이빗 서브넷 생성
resource &quot;aws_subnet&quot; &quot;private_subnets&quot; {
  count = 3
  vpc_id            = aws_vpc.my_vpc.id
  cidr_block        = cidrsubnet(local.new_vpc_cidr, 4, count.index + 3) # /20 서브넷 생성
  availability_zone = [&quot;ap-northeast-2a&quot;, &quot;ap-northeast-2b&quot;, &quot;ap-northeast-2c&quot;][count.index]
  tags = {
    Name = &quot;private-subnet-${count.index + 1}&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 수정된 파일은 이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. static&amp;nbsp; 한 방식의 문제점, resoucre 이름 및 개수&lt;/b&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;&lt;b&gt;4.1 variable 을 이용한 해결&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1732460519628&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# variable.tf
variable &quot;availability_zones&quot; {
  description = &quot;Availability zones to use for subnets&quot;
  type        = list(string)
  default     = [&quot;ap-northeast-2a&quot;, &quot;ap-northeast-2b&quot;, &quot;ap-northeast-2c&quot;]
}

variable &quot;resource_name_prefix&quot; {
  description = &quot;Prefix for resource names&quot;
  type        = string
  default     = &quot;my&quot;
}

variable &quot;base_cidr&quot; {
  description = &quot;Base CIDR for VPC&quot;
  type        = string
  default     = &quot;10.0.0.0/16&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1732460537266&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# main.tf

# 추가
resource &quot;random_string&quot; &quot;resource_suffix&quot; {
  length  = 6
  upper   = false
  special = false
}

# ... 기존 스크립트&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;이상의 스크립트를 이용하면 이렇게 유연하게 VPC 를 생성할 수 있겠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;AWS VPC-2.png&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;471&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bi1AeR/btsKUwmjjol/OKdiP56MWJl83RuFPcDAfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bi1AeR/btsKUwmjjol/OKdiP56MWJl83RuFPcDAfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bi1AeR/btsKUwmjjol/OKdiP56MWJl83RuFPcDAfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbi1AeR%2FbtsKUwmjjol%2FOKdiP56MWJl83RuFPcDAfk%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;700&quot; height=&quot;228&quot; data-filename=&quot;AWS VPC-2.png&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;471&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련코드는 깃헙 (&lt;a href=&quot;https://github.com/jts8257/terraform-aws/tree/main/vpc-section&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/jts8257/terraform-aws/tree/main/vpc-section&lt;/a&gt;) 에 올려두었다.&lt;/p&gt;</description>
      <category>탐구 생활/Terraform</category>
      <category>aws vpc terraform</category>
      <category>terraform vpc</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/54</guid>
      <comments>https://probehub.tistory.com/54#entry54comment</comments>
      <pubDate>Sun, 24 Nov 2024 23:20:46 +0900</pubDate>
    </item>
    <item>
      <title>Terraform 기초: 리소스 생명 주기 및 데이터 소스 관리하기</title>
      <link>https://probehub.tistory.com/51</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 리소스 생명 주기 (Lifecycle Rules)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform은 리소스를 업데이트할 때 기존 리소스를 삭제(destroy)하고 새로운 리소스를 생성(create)하는 &lt;b&gt;immutable 인프라&lt;/b&gt; 접근 방식을 따릅니다. 그러나 모든 경우에 이러한 동작이 적합하지 않을 수 있습니다. 이를 제어하려면 &lt;b&gt;lifecycle rules&lt;/b&gt;를 활용할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.1 &lt;code&gt;create_before_destroy&lt;/code&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 리소스를 삭제하기 전에 새로운 리소스를 먼저 생성한다. 어떤 리소스는 다운 타임을 최소화하기 위해 이러한 조치가 필요할 것이다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;resource &quot;aws_instance&quot; &quot;example&quot; {
  ami           = &quot;ami-123456&quot;
  instance_type = &quot;t2.micro&quot;

  lifecycle {
    create_before_destroy = true
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.2 &lt;code&gt;prevent_destroy&lt;/code&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스가 삭제되지 않도록 보호한다, 이때 보호되는것은 변경에 의한 삭제이지 main.tf 등 문서에서 리소스가 사라졌을때 삭제되는 것까지지 방지하는것은 아니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;resource &quot;aws_s3_bucket&quot; &quot;example&quot; {
  bucket = &quot;example-bucket&quot;

  lifecycle {
    prevent_destroy = true
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.3. &lt;code&gt;ignore_changes&lt;/code&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 속성 변경을 무시한다. 예를 들어, 태그 변경을 Terraform에서 무시하도록 설정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;resource &quot;aws_instance&quot; &quot;web&quot; {
  ami           = &quot;ami-123456&quot;
  instance_type = &quot;t2.micro&quot;

  lifecycle {
    ignore_changes = [
      tags
    ]
  }
}&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;주로 외부 시스템이나 사용자에 의해 변경될 수 있는 속성을 terraform 이 덮어쓰지 않도록 할 때 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 위의 예시처럼 EC2 인스턴스의 태그나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- S3 의 ACL 처럼 시스템이나 사용자에 의해 변경될 수 있는 것들&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 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ignore_changes&lt;/b&gt; 가 유용하지만 Terraform 이 바라보는 상태와 실제 인프라의 상태가 달라지게 되므로 나중에 둘을 동기화할때 추가작업이 필요해진다. 따라서 신중히 사용해야한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. Meta Arguments: Count&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform에서 동일한 리소스를 여러 개 생성하려면 &lt;code&gt;count&lt;/code&gt; 메타 인수를 사용한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.1 기본 사용법&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 로컬 파일을 3개 생성하는 예제이다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;local_file&quot; &quot;example&quot; {
  count    = 3
  filename = &quot;/root/file-${count.index}.txt&quot;
  content  = &quot;This is file ${count.index}&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;code&gt;/root/file-0.txt&lt;/code&gt;, &lt;code&gt;/root/file-1.txt&lt;/code&gt;, &lt;code&gt;/root/file-2.txt&lt;/code&gt;가 생성된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.2 Count와 리스트&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 파일 이름을 사용하려면 리스트 변수를 활용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;variable &quot;filenames&quot; {
  default = [&quot;file-a.txt&quot;, &quot;file-b.txt&quot;, &quot;file-c.txt&quot;]
}

resource &quot;local_file&quot; &quot;example&quot; {
  count    = length(var.filenames)
  filename = &quot;/root/${var.filenames[count.index]}&quot;
  content  = &quot;This is ${var.filenames[count.index]}&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스트 길이가 변경되면 자동으로 리소스 개수가 조정된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. Data Sources&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform은 &lt;b&gt;외부에서 생성된 리소스&lt;/b&gt;를 읽고 활용하기 위해 &lt;b&gt;Data Sources&lt;/b&gt;를 사용한다. 이때 외부라 하면 현재 terraform 파일에서 관리되지 않는 것을 총칭한다. 외부 클라우드에서 제공되는 데이터 소스일 수도 있으며, 내가 작업중인 컴퓨터의 다른 파일일 수도 있겠다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1 기본 사용법&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 로컬 파일의 내용을 읽는 예시이다. example.txt 의 내용을 읽어서 new_example.txt 에 넣는다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;data &quot;local_file&quot; &quot;example&quot; {
  filename = &quot;/root/example.txt&quot;
}

resource &quot;local_file&quot; &quot;new_file&quot; {
  filename = &quot;/root/new_example.txt&quot;
  content  = data.local_file.example.content
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.2 Data Sources vs Managed Resources&lt;/b&gt;&lt;/h4&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;b&gt;Managed Resources&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;&lt;b&gt;Data Sources&lt;/b&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Terraform에서 리소스를 생성, 업데이트, 삭제&lt;/td&gt;
&lt;td&gt;외부 리소스에서 데이터만 읽기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resource&lt;/code&gt; 키워드를 사용&lt;/td&gt;
&lt;td&gt;&lt;code&gt;data&lt;/code&gt; 키워드를 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terraform이 리소스의 상태를 관리&lt;/td&gt;
&lt;td&gt;Terraform 외부의 리소스를 참조&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;위의 내용을 활용하여 Terraform을 더욱 효율적으로 관리해보자!  &lt;/p&gt;</description>
      <category>탐구 생활/Terraform</category>
      <category>Terraform</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/51</guid>
      <comments>https://probehub.tistory.com/51#entry51comment</comments>
      <pubDate>Fri, 22 Nov 2024 19:47:31 +0900</pubDate>
    </item>
    <item>
      <title>Terraform 기초: commands</title>
      <link>https://probehub.tistory.com/50</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Terraform-comamnds.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rML9a/btsKQHPENwC/ivw3JRVbXkQDuXKeGR37KK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rML9a/btsKQHPENwC/ivw3JRVbXkQDuXKeGR37KK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rML9a/btsKQHPENwC/ivw3JRVbXkQDuXKeGR37KK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrML9a%2FbtsKQHPENwC%2Fivw3JRVbXkQDuXKeGR37KK%2Fimg.webp&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;350&quot; height=&quot;350&quot; data-filename=&quot;Terraform-comamnds.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform는 인프라를 코드로 관리하기 위한 강력한 도구이다. Terraform을 효과적으로 사용하려면 다양한 명령어를 이해하고 활용하는 것이 중요하므로 Terraform에서 자주 사용되는 명령어들과 그 활용 사례에 대해 살펴보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 Terraform 명령어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. &lt;code&gt;terraform init&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;terraform init&lt;/code&gt;&lt;/b&gt;은 Terraform 프로젝트를 초기화하는 데 사용된다. 이 명령은 구성 디렉토리를 준비하고 필요한 provider 플러그인을 다운로드한다. Terraform 작업의 첫 단계로 &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;h3 data-ke-size=&quot;size23&quot;&gt;2. &lt;code&gt;terraform validate&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;terraform validate&lt;/code&gt;&lt;/b&gt;는 Terraform 구성 파일의 &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;성공적인 유효성 검사:결과: &quot;Success! The configuration is valid.&quot;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;terraform validate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;오류 발생 시:오류가 발생한 줄과 수정 방법이 제공된다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;Error: Unsupported argument&lt;/code&gt;&lt;/li&gt;
&lt;/ul&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;3. &lt;code&gt;terraform fmt&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;terraform fmt&lt;/code&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;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 포맷이 통일되어 있지 않은 tf 문서가 있는 작업공간에서 terraform fmt 를 실행하면 일관된 포맷으로 만들어준다.&lt;/p&gt;
&lt;pre id=&quot;code_1732190847140&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;resource &quot;local_file&quot; &quot;pet&quot; {
filename = var.filename
content  = &quot;${var.content} - ${random_pet.my-pet.id}&quot;
}

resource &quot;random_pet&quot; &quot;my-pet&quot; {
prefix    = var.prefix
            separator = var.separator
                    length    = var.length
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;resource &quot;local_file&quot; &quot;pet&quot; {
  filename = var.filename
  content  = &quot;${var.content} - ${random_pet.my-pet.id}&quot;
}

resource &quot;random_pet&quot; &quot;my-pet&quot; {
  prefix    = var.prefix
  separator = var.separator
  length    = var.length
}&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;이때 파일 실행 결과 std out 에 수정된 파일 목록이 출력되며, 변경 사항은 저장된다.&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;4. &lt;code&gt;terraform plan&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;terraform plan&lt;/code&gt;&lt;/b&gt;은 실행 계획을 생성하여 Terraform이 구성 파일에 정의된 변경 사항을 실제 인프라에 적용하기 전에 어떤 작업이 수행될지 확인할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변경 사항이 없다면: &quot;No changes. Infrastructure is up-to-date.&quot;&lt;/li&gt;
&lt;li&gt;변경 사항이 있다면, 추가/수정/삭제할 리소스 목록이 출력된다.&lt;/li&gt;
&lt;/ul&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;5. &lt;code&gt;terraform apply&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;terraform apply&lt;/code&gt;&lt;/b&gt;는 &lt;code&gt;terraform plan&lt;/code&gt;에서 생성된 실행 계획을 실제 인프라에 &lt;b&gt;적용&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령 실행 후 확인 프롬프트에서 &lt;code&gt;yes&lt;/code&gt;를 입력하면 Terraform이 실행 계획을 실행한다.&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;6. &lt;code&gt;terraform show&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;terraform show&lt;/code&gt;&lt;/b&gt;는 현재 인프라 상태를 표시한다. 이 명령은 Terraform의 상태 파일을 읽어 리소스의 속성을 출력한다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;terraform show&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JSON 형식으로 출력하려면:
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;terraform show -json&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&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;7. &lt;code&gt;terraform output&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;terraform output&lt;/code&gt;&lt;/b&gt;은 구성 파일에서 정의된 &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;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;terraform output&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;특정 변수의 값 표시:
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;terraform output &amp;lt;variable_name&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&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;8. &lt;code&gt;terraform refresh&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;terraform refresh&lt;/code&gt;&lt;/b&gt;는 실제 인프라 상태를 &lt;b&gt;동기화&lt;/b&gt;하고 Terraform의 상태 파일을 업데이트합니다. 수동으로 외부에서 리소스가 변경된 경우 유용하다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;terraform refresh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령은 &lt;code&gt;terraform plan&lt;/code&gt; 및 &lt;code&gt;terraform apply&lt;/code&gt; 실행 시 자동으로 수행되지만, &lt;code&gt;-refresh=false&lt;/code&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;9. &lt;code&gt;terraform graph&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;terraform graph&lt;/code&gt;&lt;/b&gt;는 Terraform 구성 파일 또는 실행 계획에 정의된 &lt;b&gt;의존성 그래프&lt;/b&gt;를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 fmt 에서 사용된 파일을 예시로 사용해보면 사람이 해석하기에는 다소 난해한 DOT 형식의 텍스트를 출력한다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1732191073864&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;➜  terraform-files terraform graph
digraph {
        compound = &quot;true&quot;
        newrank = &quot;true&quot;
        subgraph &quot;root&quot; {
                &quot;[root] local_file.pet (expand)&quot; [label = &quot;local_file.pet&quot;, shape = &quot;box&quot;]
                &quot;[root] provider[\&quot;registry.terraform.io/hashicorp/local\&quot;]&quot; [label = &quot;provider[\&quot;registry.terraform.io/hashicorp/local\&quot;]&quot;, shape = &quot;diamond&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;이러한 텍스트를 그래프 시각화 도구에 파이프라인으로 연결하면 시각화된 그래프를 볼 수 있다. mac 과 linux 에서는 Graphviz 를 이용할 수 있겠다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;terraform graph | dot -Tpng &amp;gt; graph.png&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래가 그 결과인데, 그다지 보기 좋진 않다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;graph.png&quot; data-origin-width=&quot;1559&quot; data-origin-height=&quot;443&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vMi8A/btsKQAwfuGQ/mFTnNkXIWIdadhxVy8amV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vMi8A/btsKQAwfuGQ/mFTnNkXIWIdadhxVy8amV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vMi8A/btsKQAwfuGQ/mFTnNkXIWIdadhxVy8amV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvMi8A%2FbtsKQAwfuGQ%2FmFTnNkXIWIdadhxVy8amV0%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;1559&quot; height=&quot;443&quot; data-filename=&quot;graph.png&quot; data-origin-width=&quot;1559&quot; data-origin-height=&quot;443&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;10. &lt;code&gt;terraform providers&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;terraform providers&lt;/code&gt;&lt;/b&gt;는 현재 구성 파일에서 사용하는 &lt;b&gt;provider 목록&lt;/b&gt;을 표시한다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;terraform providers&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;필요한 provider 플러그인을 다른 디렉토리에 복사하려면 &lt;code&gt;mirror&lt;/code&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;11. &lt;code&gt;terraform state&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 상태 파일을 직접 수정하는 대신, &lt;b&gt;&lt;code&gt;terraform state&lt;/code&gt;&lt;/b&gt; 명령어를 사용하여 안전하게 상태 파일을 관리할 수 있다. 이 명령은 상태 파일에서 리소스를 이동하거나 수정하는 데 사용된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Terraform 명령어 사용 팁&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 협업 환경에서의 Remote State&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 환경에서는 &lt;b&gt;Remote State&lt;/b&gt;를 사용하여 상태 파일을 공유하는 것이 중요하다. Terraform 상태 파일을 원격 스토리지(예: AWS S3, Terraform Cloud)에 저장하면, 모든 팀원이 항상 최신 상태를 유지할 수 있다.&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;p data-ke-size=&quot;size16&quot;&gt;Terraform 명령어를 CI/CD 파이프라인에 통합하면 인프라 배포를 자동화할 수 있다. &lt;code&gt;terraform init&lt;/code&gt;, &lt;code&gt;plan&lt;/code&gt;, &lt;code&gt;apply&lt;/code&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;3. &lt;code&gt;-refresh=false&lt;/code&gt;로 성능 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 인프라에서는 &lt;code&gt;-refresh=false&lt;/code&gt; 옵션을 사용하여 불필요한 상태 동기화를 줄이고 실행 속도를 개선할 수 있다.&lt;/p&gt;</description>
      <category>탐구 생활/Terraform</category>
      <category>Terraform</category>
      <category>terraform command</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/50</guid>
      <comments>https://probehub.tistory.com/50#entry50comment</comments>
      <pubDate>Thu, 21 Nov 2024 21:03:39 +0900</pubDate>
    </item>
    <item>
      <title>Terraform 기초: State</title>
      <link>https://probehub.tistory.com/49</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Terraform-state-thumb.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K3b6X/btsKSGhcP5n/ElfbIDvJRDKL7FAWS0Qot1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K3b6X/btsKSGhcP5n/ElfbIDvJRDKL7FAWS0Qot1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K3b6X/btsKSGhcP5n/ElfbIDvJRDKL7FAWS0Qot1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK3b6X%2FbtsKSGhcP5n%2FElfbIDvJRDKL7FAWS0Qot1%2Fimg.webp&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;350&quot; height=&quot;350&quot; data-filename=&quot;Terraform-state-thumb.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform를 사용할 때 중요한 개념 중 하나는 &lt;b&gt;State&lt;/b&gt; 이다. 이번 글에서는 Terraform의 State의 역할과 중요성, 그리고 실무에서의 활용 방안에 대해 살펴보겠다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Terraform State란 무엇인가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform의 State 파일은 &lt;b&gt;리소스 구성과 실제 인프라 간의 매핑 정보를 저장&lt;/b&gt;하는 역할을 한다. 이는 Terraform이 실행 계획을 생성하고, 구성 파일과 실제 인프라 간의 &lt;b&gt;불일치(Drift)&lt;/b&gt;를 식별하는 데 사용된다. 쉽게 말해, Terraform State 파일은 실제 세상에 존재하는 모든 리소스의 청사진(blueprint)이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 State 는 terraform apply 가 되는순간 &lt;code&gt;terraform.tfstate&lt;/code&gt; 파일에 json 형태로 저장이 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Terraform State의 주요 역할&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 리소스 매핑 및 의존성 관리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 은 리소스를 생성할 때 해당 리소스의 ID를 State에 기록한다. 이 ID는 로컬 파일 리소스, 클라우드 리소스, 또는 논리적 리소스와 같은 모든 리소스를 &lt;b&gt;유일하게 식별&lt;/b&gt;할 수 있는 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뿐만 아니라, State는 리소스 간의 의존성 정보를 포함하고 있다. 예를 들어:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;random_pet&quot; &quot;my_pet&quot; {
 prefix = &quot;Mrs&quot;
 separator = &quot;.&quot;
 length = 1
}

resource &quot;local_file&quot; &quot;pet&quot; {
  content  = random_pet.my_pet.id
  filename = &quot;pet_name.txt&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;code&gt;local_file.pet&lt;/code&gt;은 &lt;code&gt;random_pet.my_pet&lt;/code&gt; 에 의존한다. 따라서 &lt;code&gt;random_pet&lt;/code&gt; 이 생성된 이후에만 &lt;code&gt;local_file&lt;/code&gt; 이 생성된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 삭제 순서 제어&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform에서 리소스를 삭제할 때도 State 파일의 의존성 정보가 중요하다. 예를 들어, 위의 예제에서 &lt;code&gt;random_pet&lt;/code&gt; 과 &lt;code&gt;local_file&lt;/code&gt; 을 모두 삭제한다고 가정해보자. Terraform은 &lt;b&gt;State 파일에 기록된 의존성 정보를 기반으로&lt;/b&gt; &lt;code&gt;local_file&lt;/code&gt; 을 먼저 삭제한 후 &lt;code&gt;random_pet&lt;/code&gt; 을 삭제한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 성능 최적화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform은 수백 개의 리소스를 관리할 수 있으며, 이 중 일부는 여러 클라우드 제공업체에 분산될 수 있다. 매번 모든 리소스 상태를 확인하려면 많은 시간이 소요된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform State는 이러한 리소스 상태를 &lt;b&gt;캐시&lt;/b&gt;로 저장하여 &lt;b&gt;실행 속도를 대폭 개선&lt;/b&gt;하는 역할을 수행한다. 예를 들어, -refresh=false 플래그를 사용하면 Terraform은 State 파일만 참조하고 실시간 상태 갱신을 건너뛰게 된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;terraform plan -refresh=false&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 협업 지원&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 프로젝트에서는 로컬 State 파일(terraform.tfstate)을 사용하는 것이 문제없을 것이다. 하지만 &lt;b&gt;팀 협업&lt;/b&gt; 환경에서는 로컬 State 파일 사용이 비효율적이며 예측 불가능한 에러를 초래할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;Remote State&lt;/b&gt;를 사용하는 것이 권장된다. Remote State는 State 파일을 원격 저장소에 저장함으로써 팀원들이 항상 최신 상태를 공유할 수 있도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform에서 지원하는 Remote State 백엔드의 예는 다음과 같다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS S3&lt;/li&gt;
&lt;li&gt;Google Cloud Storage&lt;/li&gt;
&lt;li&gt;Azure Blob Storage&lt;/li&gt;
&lt;li&gt;Terraform Cloud&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;Terraform State의 보안 고려사항&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform State 파일은 &lt;b&gt;JSON 형식&lt;/b&gt;으로 저장되며, 모든 리소스 속성 값이 포함된다. 여기에는 다음과 같은 &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;클라우드 리소스의 IP 주소&lt;/li&gt;
&lt;li&gt;SSH 키&lt;/li&gt;
&lt;li&gt;데이터베이스 초기 비밀번호&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 로컬 State 파일은 항상 &lt;b&gt;안전한 스토리지&lt;/b&gt;에 저장해야 하며, 민감한 데이터가 유출되지 않도록 주의해야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Git 저장소에 State 파일을 저장하지 말 것&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 구성 파일은 Git과 같은 분산 버전 관리 시스템에 저장하는 것이 일반적이다. 하지만 State 파일은 예외다. 민감한 정보를 포함하고 있으므로, State 파일은 반드시 &lt;b&gt;원격 백엔드&lt;/b&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;Terraform State 수정 주의사항&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;State 파일은 Terraform의 내부 용도로 설계되었으며, &lt;b&gt;직접 수정해서는 안 된다&lt;/b&gt;. 수정이 필요한 경우 terraform state 명령어를 사용해야 한다.&lt;/p&gt;</description>
      <category>탐구 생활/Terraform</category>
      <category>Terraform</category>
      <category>terraform state</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/49</guid>
      <comments>https://probehub.tistory.com/49#entry49comment</comments>
      <pubDate>Thu, 21 Nov 2024 20:56:14 +0900</pubDate>
    </item>
    <item>
      <title>MSA 주요 패턴: 사가(Saga) 패턴</title>
      <link>https://probehub.tistory.com/48</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;MSA-saga-pattern.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chLQPv/btsKQL4ohFz/maFVftiOks1jcEFwDqCfgK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chLQPv/btsKQL4ohFz/maFVftiOks1jcEFwDqCfgK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chLQPv/btsKQL4ohFz/maFVftiOks1jcEFwDqCfgK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchLQPv%2FbtsKQL4ohFz%2FmaFVftiOks1jcEFwDqCfgK%2Fimg.webp&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;350&quot; height=&quot;350&quot; data-filename=&quot;MSA-saga-pattern.webp&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 id=&quot;1.-Saga-패턴&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. Saga 패턴&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;모놀로식 아키텍처에서 단일 DB 를 사용하고 있다면 트랜잭션의 원자성과 일관성을 DBMS 수준에서 보장을 해준다. 따라서 데이터의 일관성을 유지하는게 어렵지 않다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;106&quot; data-ke-size=&quot;size16&quot;&gt;하지만 MSA 에서 여러 서비스의 서로 다른 DBMS 를 거쳐 영속화 해야하는 것은 흔한 일이다. 때문에 데이터 일관성 문제가 발생한다. 이러한 문제를 해결하기 위해 &lt;b&gt;&amp;ldquo;서로다른 DBMS 의 트랜잭션을 논리적으로 묶을 필요&amp;rdquo;&lt;/b&gt;가 있었고 이를 위해 등장한 설계 패턴이 Saga 패턴이다. (마치 트랜잭션이 전파되는 것이 이야기가 전파되는것과 같다고 하여 Saga 라고 부른다)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;106&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;사가패턴-논리적 트랜잭션.png&quot; data-origin-width=&quot;1828&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRJk4h/btsKQfEXrd0/yTPflNRAaKo7Ykj2W3QnTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRJk4h/btsKQfEXrd0/yTPflNRAaKo7Ykj2W3QnTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRJk4h/btsKQfEXrd0/yTPflNRAaKo7Ykj2W3QnTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRJk4h%2FbtsKQfEXrd0%2FyTPflNRAaKo7Ykj2W3QnTk%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;605&quot; height=&quot;331&quot; data-filename=&quot;사가패턴-논리적 트랜잭션.png&quot; data-origin-width=&quot;1828&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;106&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;106&quot; data-ke-size=&quot;size16&quot;&gt;Saga 패턴에서 트랜잭션을 논리적으로 묶는 주체는 어플리케이션이다. 위와 같이 트랜잭션이 실패했을 때 &lt;b&gt;보상 트랜잭션(cancel)&lt;/b&gt; 을 호출 어플리케이션에 &lt;b&gt;전달&lt;/b&gt;하고 트랜잭션 롤백 등 지정된 행위를 하게 만든다. 이 과정에서 일시적으로 데이터 정합이 깨져있을 수 있으나 &lt;b&gt;&amp;ldquo;결과적 정합&amp;rdquo;&lt;/b&gt;을 보장한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;106&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;106&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;위 그림은 단순히 트랜잭션 신호 전달만 도식화했을 뿐이다. 실제로 트랜잭션 성공(TransactionComplete) 혹은 실패(Cancel) 은 메시지 브로커를 통한 이벤트로 전달된다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;106&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;435&quot; data-ke-size=&quot;size16&quot;&gt;이러한 saga 패턴은 코레오그래피 기반 사가(Choreography-based Saga)와 오케스트레이션 기반 사가(Orchestration-based Saga) 등이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 id=&quot;1.1-코레오그래피-기반-사가(Choreography-based-Saga)&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;536&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 코레오그래피 기반 사가(Choreography-based Saga)&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;579&quot; data-ke-size=&quot;size16&quot;&gt;코레오그래피 기반 사가는 아래의 그림과 같이 각 서비스간 메시지 브로커로 &lt;b&gt;직접 결합된 형태&lt;/b&gt;이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;사가패턴-코레오그래피.png&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;1090&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/meow3/btsKPD7jzcB/HezcbdJjLHiYnQfvrR8Jkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/meow3/btsKPD7jzcB/HezcbdJjLHiYnQfvrR8Jkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/meow3/btsKPD7jzcB/HezcbdJjLHiYnQfvrR8Jkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmeow3%2FbtsKPD7jzcB%2FHezcbdJjLHiYnQfvrR8Jkk%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;650&quot; height=&quot;385&quot; data-filename=&quot;사가패턴-코레오그래피.png&quot; data-origin-width=&quot;1840&quot; data-origin-height=&quot;1090&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;994&quot; data-ke-size=&quot;size16&quot;&gt;서비스와 서비스를 메시지 브로커가 직접 중계하기 때문에 구현이 쉽다. 하지만,&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;681&quot; data-ke-size=&quot;size16&quot;&gt;트랜잭션 Saga 참가자가 많을 수록 트랜잭션 흐름이 복잡해지고 마이크로 &lt;b&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;style2&quot; /&gt;
&lt;h3 id=&quot;1.2--오케스트레이션-기반-사가(Orchestration-based-Saga)&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;812&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 오케스트레이션 기반 사가(Orchestration-based Saga)&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;681&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;오케스트레이션 기반 사가는 중앙 집중된 Saga Orchestration 이 saga 참여자들에게 어떤 로컬 트랜잭션에 참여해야 하는지 알려주는 형태이다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;사가패턴-오케스트레이션.png&quot; data-origin-width=&quot;1834&quot; data-origin-height=&quot;1110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bx1aBR/btsKRP51Diu/Rf6fO8xFEuRMm0RqD4aB5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bx1aBR/btsKRP51Diu/Rf6fO8xFEuRMm0RqD4aB5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bx1aBR/btsKRP51Diu/Rf6fO8xFEuRMm0RqD4aB5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbx1aBR%2FbtsKRP51Diu%2FRf6fO8xFEuRMm0RqD4aB5K%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;650&quot; height=&quot;393&quot; data-filename=&quot;사가패턴-오케스트레이션.png&quot; data-origin-width=&quot;1834&quot; data-origin-height=&quot;1110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 흐름이 명확해지고 서비스간 결합도를 낮출 수 있다는 장점이 있지만,&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;994&quot; data-ke-size=&quot;size16&quot;&gt;오케스트레이터(Orchestrator) 가 실패했을 때 모든 트랜잭션 연계가 끊길 수 있다는 SPOF 문제가 있으며 서비스간 결합도는 낮아지지만 서비스와 오케스트레이터간 결합도가 높아진다는 단점있다. 이러한 이유로 유연성과 확장성이 낮아질 수 이다는 위험이 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 id=&quot;1.3-Correlation-ID&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1143&quot; data-ke-size=&quot;size23&quot;&gt;4. 보장되지 않은 영속화 순서, CorrelationID&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;아래와 같은 DB 를 가지고 있고 BFF 로 MSA 를 호출하는 환경을 생각해보자&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_BFF 예시 ERD.png&quot; data-origin-width=&quot;1686&quot; data-origin-height=&quot;786&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HKcwr/btsKRhBMz2r/EPwYhIXs4kRqxBOqX1WGP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HKcwr/btsKRhBMz2r/EPwYhIXs4kRqxBOqX1WGP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HKcwr/btsKRhBMz2r/EPwYhIXs4kRqxBOqX1WGP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHKcwr%2FbtsKRhBMz2r%2FEPwYhIXs4kRqxBOqX1WGP0%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;650&quot; height=&quot;303&quot; data-filename=&quot;edited_BFF 예시 ERD.png&quot; data-origin-width=&quot;1686&quot; data-origin-height=&quot;786&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_BFF 모델.png&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;575&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEzZHD/btsKRtWrJ1z/hGr0jDRmvDvCXeij7KQI81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEzZHD/btsKRtWrJ1z/hGr0jDRmvDvCXeij7KQI81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEzZHD/btsKRtWrJ1z/hGr0jDRmvDvCXeij7KQI81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEzZHD%2FbtsKRtWrJ1z%2FhGr0jDRmvDvCXeij7KQI81%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;650&quot; height=&quot;366&quot; data-filename=&quot;edited_BFF 모델.png&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;575&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;BFF 는 &lt;/span&gt;&lt;b&gt;A,B,C 를 생성&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;할 수 있는 요청을 모두 받았고 이들을 각기 분리하여 A 서비스, B 서비스, C 서비스로 전달하였다. 이때 A, B, C 의 ERD 는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;BFF 는 각 서비스의 순서를 모르게(결합도가 낮은) 설계 되었기 때문에 동시에 A, B, C 서비스를 호출하였고, 각각의 A, B, C 의 영속화 순서는 보장되지 않았다. 그 결과 &lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;&lt;b&gt;C &amp;rarr; B &amp;rarr; A 순서로 영속화&lt;/b&gt;가 되었다. &lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;B 는 A의 id 를 알아야하고 C는 B 의 id 를 알아야 하지만 B 와 C 는 fk 가 누락된 상황이다.&lt;/span&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;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;각 서비스는 본인들의 영속화 업무가 끝나자 TransactionComplete 이벤트를 발행한다. 그 순서는 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Saga Event Sequence.png&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;810&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bI260R/btsKRtWrLDj/jWQ4Z4zw0dbAaBOYPPI93k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bI260R/btsKRtWrLDj/jWQ4Z4zw0dbAaBOYPPI93k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI260R/btsKRtWrLDj/jWQ4Z4zw0dbAaBOYPPI93k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbI260R%2FbtsKRtWrLDj%2FjWQ4Z4zw0dbAaBOYPPI93k%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;650&quot; height=&quot;405&quot; data-filename=&quot;Saga Event Sequence.png&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;810&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발생 이벤트의 payload 에는 생성된 엔티티의 id 만 담겨있다고 생각하면 B 는 A 가 생성되었다는 이벤트를 구독하여 확인했지만 &lt;b&gt;도대체 이 A 를 어떤 B 와 연관시켜야 할지 알 지 못하며 C 역시 마찬가지다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1694&quot; data-ke-size=&quot;size16&quot;&gt;어떻게 해야할까? BFF 가 모든 paylaod 를 A 에게 넘기고 A는 영속화후 남는 payload 로 B 를 만들고, 다시 같은 작업을 B 는 C에 대해 반복해야할까? 이렇게 설계된다면 서비스간 결합도가 굉장히 높은 상태이므로 좋은 설계라고 볼 수 없다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1694&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1840&quot; data-ke-size=&quot;size16&quot;&gt;이때 등장하는게 &lt;b&gt;correlationID&lt;/b&gt;란 개념이다. BFF 에서(혹은 API Gateway 에서) &lt;b&gt;각 요청을 고유하게 구분할 수 있는id 를 생성&lt;/b&gt;하여 각 서비스의 생성 요청에 함께 보내는 것이다. 그리고 각 서비스는 이벤트를 발행할때 생성된 엔티티의 id 와 이 유니크한 id 를 함께 payload 에 넣고 발행한다면 상호 결합도는 낮추면서 DB 의존 관계를 명확하게 유지할 수 있게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1840&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1840&quot; data-ke-size=&quot;size16&quot;&gt;이러한 &lt;b&gt;correlationID &lt;/b&gt;를 적용하여 위의 이벤트 발행에 payload 를 붙여서 보여주면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Saga Event Sequence-v2.png&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;810&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJL6qO/btsKQt3ZoNj/2hFhkCeQH5kJbL1N0t4X0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJL6qO/btsKQt3ZoNj/2hFhkCeQH5kJbL1N0t4X0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJL6qO/btsKQt3ZoNj/2hFhkCeQH5kJbL1N0t4X0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJL6qO%2FbtsKQt3ZoNj%2F2hFhkCeQH5kJbL1N0t4X0K%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;650&quot; height=&quot;405&quot; data-filename=&quot;Saga Event Sequence-v2.png&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;810&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1840&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1840&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. 보장되지 않은 영속화 순서 ,&amp;nbsp; 아직 메시지를 받아들일 준비가 안됨...&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 반대로 영속화 순서가 A -&amp;gt; B -&amp;gt; C 순서라고 생각해보자. 그리고 이런 경우에는 A 가 TransactionComplete이벤트를 발행해도 B 는 이 메시지를 구독하고도 처리할 준비가 되지 않았을 것이다. 이것은 C역시 마찬가지다.&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.1 Kafka 에서 준비되지 않은 TransactionComplte 메시지 처리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트를 소비했으나 연관관계를 매핑해줄 엔티티가 아직 없는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Kafka 는 메시지의 오프셋을 고의로 커밋하지 않는 것으로 메시지를 유지할 수 있다. 그리고 N 초 뒤에 다시 메시지를 소비한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이벤트를 Dead Letter Queue 에 저장하고, 엔티티 B 가 영속화 되었을때 재처리하도록 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Fallback Storage 를 이용하여 TransactionComplete 이벤트를 임시 저장소(Redis, DB) 에 저장한 뒤 B 가 영속화된 뒤 재처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python 코드로 보면 이렇다.&lt;/p&gt;
&lt;pre id=&quot;code_1732104685805&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from kafka import KafkaConsumer, KafkaProducer
import json
import time

consumer = KafkaConsumer(
    'transaction-events',
    group_id='service_b',
    bootstrap_servers='localhost:9092',
    enable_auto_commit=False,  # 오프셋 자동 커밋 비활성화
    value_deserializer=lambda v: json.loads(v.decode('utf-8'))
)

# 임시 저장소 (Redis나 DB로 대체 가능)
temp_storage = {}

def handle_event(event):
    correlation_id = event['correlation_id']
    a_id = event['a_id']

    # B 엔티티가 아직 없는 경우
    b_entity = find_b_entity_by_correlation_id(correlation_id)
    if not b_entity:
        print(f&quot;B entity not found for Correlation ID {correlation_id}. Retrying...&quot;)
        temp_storage[correlation_id] = event  # 이벤트를 임시 저장
        time.sleep(5)  # 대기 후 재처리
        return False

    # B 엔티티 업데이트
    b_entity.a_id = a_id
    save_b_entity(b_entity)
    print(f&quot;Updated B entity with A ID {a_id}&quot;)
    return True

for message in consumer:
    event = message.value
    success = handle_event(event)

    # 성공적으로 처리된 경우만 오프셋 커밋
    if success:
        consumer.commit()&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기초 지식/MSA</category>
      <category>MSA</category>
      <category>msa saga pattern</category>
      <category>msa 사가 패턴</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/48</guid>
      <comments>https://probehub.tistory.com/48#entry48comment</comments>
      <pubDate>Wed, 20 Nov 2024 20:59:37 +0900</pubDate>
    </item>
    <item>
      <title>MSA 주요 패턴: 서비스 디스커버리(Service Discovery) 패턴</title>
      <link>https://probehub.tistory.com/47</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;MSA-ServiceDiscovery.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u3YfR/btsKPfeu6hV/UvpKLncmAW8JVm9aLEuUd0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u3YfR/btsKPfeu6hV/UvpKLncmAW8JVm9aLEuUd0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u3YfR/btsKPfeu6hV/UvpKLncmAW8JVm9aLEuUd0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu3YfR%2FbtsKPfeu6hV%2FUvpKLncmAW8JVm9aLEuUd0%2Fimg.webp&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;350&quot; height=&quot;350&quot; data-filename=&quot;MSA-ServiceDiscovery.webp&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 id=&quot;1.-서비스-디스커버리-패턴&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 서비스 디스커버리 패턴&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;MSA 에서는 언제든 새로운 원격 서버 인스턴스(pod) 가 추가되거나 제거될 수 있으므로 그 개수와 물리적 주소가 고정되어 있지 않기 때문에 클라이언트가 물리적인 위치를 몰라도 서비스를 호출할 수 있도록 하는 것이 중요하다.&lt;br /&gt;&lt;b&gt;즉 &amp;ldquo;물리적인 주소&amp;rdquo;가 아니라 &amp;ldquo;논리적인 주소&amp;rdquo; 로 서버 인스턴스를 찾을수 있어야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 id=&quot;1.1-클라이언트-사이드-서비스-디스커버리&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;196&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.1 클라이언트 사이드 서비스 디스커버리&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;221&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트 사이드 서비스 디스커버리의 구성은 아래와 같다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;221&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;클라이언트 사이드 디스커버리.png&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bit6kg/btsKQwzJgWT/zVm6UIoucsfOckeZ4L3qqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bit6kg/btsKQwzJgWT/zVm6UIoucsfOckeZ4L3qqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bit6kg/btsKQwzJgWT/zVm6UIoucsfOckeZ4L3qqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbit6kg%2FbtsKQwzJgWT%2FzVm6UIoucsfOckeZ4L3qqK%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;650&quot; height=&quot;321&quot; data-filename=&quot;클라이언트 사이드 디스커버리.png&quot; data-origin-width=&quot;2414&quot; data-origin-height=&quot;1194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;221&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;221&quot; data-ke-size=&quot;size16&quot;&gt;마이크로 서비스의 인스턴스들(pod)의 물리적인 주소를 Service Registry 라는 곳에 저장한다. 이때 인스턴들은 주기적으로 Hearthbeat 를 Service Registry 로 전송하여 Service Registry가 인스턴스 상태를 체크할 수 있도록 한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;412&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트는 직접 Service Registry 에 접근하여 주소 목록을 조회하고, 그 주소중 한 곳으로 요청을 전송한다. 이때 클라이언트에서 직접 로드밸런서를 구현하여 적절히 트래픽이 분산될 수 있도록 한다.&lt;/p&gt;
&lt;h4 id=&quot;클라이언트-사이드-서비스-디스커버리의-장애-대응&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;531&quot; data-ke-size=&quot;size20&quot;&gt;클라이언트 사이드 서비스 디스커버리의 장애 대응&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;559&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Service Registry 장애 대응]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;585&quot; data-ke-size=&quot;size16&quot;&gt;어떠한 이유에서 Service Registry 가 다운된 상황에서 클라이언트가 주소를 요청하는 경우에 문제가 발생할 수 있다. 이러한 이유로 클라이언트는 Service Registry 가 반환한 주소 목록을 일정 기간동안 캐시를 해두는 전략을 사용하여 장애 대응 시간을 벌 수 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;585&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;745&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Instance 장애 대응]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;763&quot; data-ke-size=&quot;size16&quot;&gt;Service Registry 가 반환한 주소 목록을 캐싱해뒀을 때 Service Registry 에서는 장애가 있는 Instance 라고 판단하여 주소 목록에서 제외했으나 클라이언트가 계속 요청을 보내는 경우가 있을 수 있다. 이런 경우에 기존 캐시에서 해당 Instance 주소를 삭제하고 다른 주소에 요청을 보내는 등의 로직이 클라이언트에서 적절히 구현되어야 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h4 id=&quot;1.2-서버-사이드-서비스-디스커버리&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;972&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.2 서버 사이드 서비스 디스커버리&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;994&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트 사이드 서비스 디스커버리의 구성은 아래와 같다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;994&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;서버 사이드 디스커버리.png&quot; data-origin-width=&quot;2378&quot; data-origin-height=&quot;1174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCqCMt/btsKRxR4Rkx/94pyNoLs0sEfQPNiFiDuQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCqCMt/btsKRxR4Rkx/94pyNoLs0sEfQPNiFiDuQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCqCMt/btsKRxR4Rkx/94pyNoLs0sEfQPNiFiDuQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCqCMt%2FbtsKRxR4Rkx%2F94pyNoLs0sEfQPNiFiDuQk%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;2378&quot; height=&quot;1174&quot; data-filename=&quot;서버 사이드 디스커버리.png&quot; data-origin-width=&quot;2378&quot; data-origin-height=&quot;1174&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;994&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;994&quot; data-ke-size=&quot;size16&quot;&gt;마이크로 서비스의 인스턴스들(pod)의 물리적인 주소를 Service Registry 라는 곳에 저장한다. 이때 인스턴들은 주기적으로 Hearthbeat 를 Service Registry 로 전송하여 Service Registry가 인스턴스 상태를 체크할 수 있도록 한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1185&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트는 서버에서 구현된 로드밸랜서 주소로 요청을 보내고, 로드밸런서는 클라이언트 요청에 적절한 인스턴스 주소를 찾아서 요청을 처리한다.&lt;/p&gt;
&lt;h4 id=&quot;서버-사이드-서비스-디스커버리-장애-대응&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1265&quot; data-ke-size=&quot;size20&quot;&gt;서버 사이드 서비스 디스커버리 장애 대응&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1289&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Service Registry 장애 대응]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1315&quot; data-ke-size=&quot;size16&quot;&gt;어떠한 이유에서 Service Registry 가 다운된 상황에서 LB가 주소를 요청하는 경우에 문제가 발생할 수 있다. 이러한 이유로 LB는 Service Registry 가 반환한 주소 목록을 일정 기간동안 캐시를 해두는 전략을 사용하여 장애 대응 시간을 벌 수 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1315&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1469&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Instance 장애 대응]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1487&quot; data-ke-size=&quot;size16&quot;&gt;Service Registry 가 반환한 주소 목록을 캐싱해뒀을 때 Service Registry 에서는 장애가 있는 Instance 라고 판단하여 주소 목록에서 제외했으나 LB가 계속 요청을 보내는 경우가 있을 수 있다. 이런 경우에 기존 캐시에서 해당 Instance 주소를 삭제하고 다른 주소에 요청을 보내는 등의 로직이 LB에 적절히 구현되어야 한다.&lt;/p&gt;
&lt;h2 id=&quot;1.3-K8s-에서-Service-Discovery&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1689&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1.3 K8s 에서 Service Discovery&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1719&quot; data-ke-size=&quot;size16&quot;&gt;K8s 에서 Service 란 Service Object 를 말하는 개념이다. &lt;a href=&quot;https://homes-dev-team.atlassian.net/wiki/spaces/HD/pages/15630378/MSA+Kubernetes+Container+Orchestration#4.2-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC%EC%97%90-%EA%B4%80%ED%95%9C-object-%EB%93%A4&quot;&gt;K8s 네트워크 Object&lt;/a&gt; 에서 언급한것과 같이 Service Object는 클러스터에서 실행중인 pod 에 접근할 수 있는 방법을 정의한 것이다. 따라서 클라이언트 코드는 가변적인 pod 의 물리 주소 대신 Service Object 를 통해 더 안정적인 네트워크 엔드포인트를 제공 받는다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1719&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1929&quot; data-ke-size=&quot;size16&quot;&gt;이러한 사실에 기반하여 K8s 의 Service Object 자체가 서비스 디스커버리 패턴을 구현한 구현체라고 생각할 수 있겠다. 이때 Service Discovery 의 역할은 K8s 마스터를 구성하는 요소 중 etcd 와 Controller Manager 와 API Server 를 통해 구현되었다.&lt;/p&gt;</description>
      <category>기초 지식/MSA</category>
      <category>MSA</category>
      <category>서비스 디스커버리</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/47</guid>
      <comments>https://probehub.tistory.com/47#entry47comment</comments>
      <pubDate>Wed, 20 Nov 2024 20:45:30 +0900</pubDate>
    </item>
    <item>
      <title>Kubernetes 기본구조</title>
      <link>https://probehub.tistory.com/46</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;K8s_docker_support_MSA.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JJ0jK/btsKPyea2gC/eyrqCz1TxQ80K8JKUBgi41/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JJ0jK/btsKPyea2gC/eyrqCz1TxQ80K8JKUBgi41/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JJ0jK/btsKPyea2gC/eyrqCz1TxQ80K8JKUBgi41/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJJ0jK%2FbtsKPyea2gC%2FeyrqCz1TxQ80K8JKUBgi41%2Fimg.webp&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;350&quot; height=&quot;350&quot; data-filename=&quot;K8s_docker_support_MSA.webp&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 id=&quot;1.-Kbuernetes(K8s)-란?&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. Kbuernetes(K8s) 란?&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;MSA에서는 각 서비스가 컨테이너로 패키징되며, 하나의 서비스에 대해 N개의 컨테이너가 존재할 수 있습니다. 수십 개의 서비스만 있어도 수백 개 이상의 컨테이너를 관리해야 합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;126&quot; data-ke-size=&quot;size16&quot;&gt;컨테이너 관리에는 다음과 같은 요구 사항이 있습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&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;b&gt;스케일링(scale-out, scale-in)&lt;/b&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 style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;337&quot; data-ke-size=&quot;size16&quot;&gt;이러한 복잡성을 사람이 수동으로 처리하는 것은 비효율적이며, 이를 자동화하기 위해 &lt;b&gt;컨테이너 오케스트레이션&lt;/b&gt;이 필요합니다. Kubernetes(K8s)는 이러한 요구를 충족시키는 대표적인 오케스트레이션 도구로, 대규모 컨테이너 관리와 자동화를 지원합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 id=&quot;2-K8s-의-철학&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;482&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. K8s 의 철학&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;494&quot; data-ke-size=&quot;size16&quot;&gt;K8s 는 다음과 같은 철학에 기초하여 설계, 구현되었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Immutable Infrastructure
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;클라우드 환경에서는 컴퓨터 리소스는 자유롭게 생성, 폐기, 확장할 수 있다. 따라서 인프라에 변경이 생기면 기존 인프라에 변경 사항을 적용하는게 아니라 기존 인프라를 파기하고 다시 만들도록 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Declarative Configuration
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;K8s 관리자는 인프라가 원하는 상태(Desired State)에 도달하기 위해 필요한 모든 과정을 선언적으로 명시해야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Self Healing
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;K8s 는 실시간으로 클러스터의 현재 상태(Current State) 를 바라보면서 Desired State 와 차이가 발생하면 이를 자동으로 복구한다.&lt;/li&gt;
&lt;/ul&gt;
&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;h3 id=&quot;3-K8s-의-구조&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;891&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. K8s 의 구조&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 id=&quot;3.1-구조-개괄&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;903&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1 구조 개괄&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;914&quot; data-ke-size=&quot;size16&quot;&gt;K8s 의 구조를 단순하게 보면 &lt;b&gt;&amp;ldquo;Master에 API 서버와 상태 저장소(etcd)를 두고 어플리케이션은 노드의 포드(pod) 에 배포되며, Master 가 각 노드의 에이전트(kubelet)와 통신하는 구조&amp;rdquo;&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;K8s구조-개괄.png&quot; data-origin-width=&quot;2576&quot; data-origin-height=&quot;812&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b85X0q/btsKQJS2TzQ/QWFDxtHRhBYb0jySQgloP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b85X0q/btsKQJS2TzQ/QWFDxtHRhBYb0jySQgloP1/img.png&quot; data-alt=&quot;K8s 구조 개괄&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b85X0q/btsKQJS2TzQ/QWFDxtHRhBYb0jySQgloP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb85X0q%2FbtsKQJS2TzQ%2FQWFDxtHRhBYb0jySQgloP1%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;650&quot; height=&quot;205&quot; data-filename=&quot;K8s구조-개괄.png&quot; data-origin-width=&quot;2576&quot; data-origin-height=&quot;812&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;K8s 구조 개괄&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h4 id=&quot;3.2-마스터&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1043&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.2 마스터&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1052&quot; data-ke-size=&quot;size16&quot;&gt;K8s 의 마스터는 클러스터를 관리하는 역할을 합니다. 즉 노드들의 상태를 파악하고 어떤 컨테이너를 어떤 노드에서 가동할지 선택합니다. 이때 분산 저장소에 클러스터의 정보를 저장하고 조회하게 됩니다. 이러한 기능을 위해 각각의 노들들은 상호 직접적으로 소통하는게 아니라 오로지 마스터를 통해서만 소통이 가능하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1232&quot; data-ke-size=&quot;size16&quot;&gt;이러한 마스터의 주요 구성 요소로는 API Server, Scheduler, Controller Manager, etcd 가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;K8s구조-마스터.png&quot; data-origin-width=&quot;2652&quot; data-origin-height=&quot;1236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ec9R2S/btsKQ70tTto/SG05TWMq8GpPxTHmRAiD7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ec9R2S/btsKQ70tTto/SG05TWMq8GpPxTHmRAiD7k/img.png&quot; data-alt=&quot;K8s master&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ec9R2S/btsKQ70tTto/SG05TWMq8GpPxTHmRAiD7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fec9R2S%2FbtsKQ70tTto%2FSG05TWMq8GpPxTHmRAiD7k%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;650&quot; height=&quot;303&quot; data-filename=&quot;K8s구조-마스터.png&quot; data-origin-width=&quot;2652&quot; data-origin-height=&quot;1236&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;K8s master&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 id=&quot;3.2.1-API-Server&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1312&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.2.1 API Server&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1330&quot; data-ke-size=&quot;size16&quot;&gt;모든 요청을 처리하는 마스터의 핵심 구성 요소입니다. 검증된 K8s 관리자, 노드의 요청만 처리해야 하므로 인증 및 인가 기능을 갖추고 있습니다.&lt;/p&gt;
&lt;h4 id=&quot;3.2.2-Scheduler&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1415&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.2.2 Scheduler&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1432&quot; data-ke-size=&quot;size16&quot;&gt;pod(컨테이너) 가 어떤 노드에서 가동할지 결정하는 구성 요소입니다.&lt;/p&gt;
&lt;h4 id=&quot;3.2.3-Controller-Manager&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1475&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.2.3 Controller Manager&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1501&quot; data-ke-size=&quot;size16&quot;&gt;K8s 클러스터의 상태를 실시간으로 검사하면서 Current State 와 Desired State 를 비교하고 Desired State 가 되도록 유지합니다.&lt;/p&gt;
&lt;h4 id=&quot;3.2.4-etcd&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1594&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.2.4 etcd&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1606&quot; data-ke-size=&quot;size16&quot;&gt;분산 key-value 저장소로 클러스터의 모든 설정, 상태 데이터를 저장합니다. Scheduler, Controlelr Manager 는 API Server 를 통해 etcd 에서 클러스터의 정보를 조회하고 갱신합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h4 id=&quot;3.3-노드(node)&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1732&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.3 노드(node)&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1746&quot; data-ke-size=&quot;size16&quot;&gt;노드는 pod(컨테이너) 가 직접적으로 가동되는 서버입니다. 다수의 노드로 클러스터를 구성합니다. 하나의 노드도 다양한 구성요소가 존재하는데 대표적인것은 kubelet 과 Kube proxy , 그리고 pod 입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1746&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;K8s구조-노드.png&quot; data-origin-width=&quot;2208&quot; data-origin-height=&quot;1214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Eh5b7/btsKPgYPQTb/7gqPi5Z4EarkpCi5v8kQU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Eh5b7/btsKPgYPQTb/7gqPi5Z4EarkpCi5v8kQU1/img.png&quot; data-alt=&quot;K8s 노드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Eh5b7/btsKPgYPQTb/7gqPi5Z4EarkpCi5v8kQU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEh5b7%2FbtsKPgYPQTb%2F7gqPi5Z4EarkpCi5v8kQU1%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;650&quot; height=&quot;357&quot; data-filename=&quot;K8s구조-노드.png&quot; data-origin-width=&quot;2208&quot; data-origin-height=&quot;1214&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;K8s 노드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 id=&quot;3.3.1-kubelet&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1872&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.3.1 kubelet&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1887&quot; data-ke-size=&quot;size16&quot;&gt;실제로 마스터의 요청을 받아 pod(컨테이너) 를 생성하는 주체입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1928&quot; data-ke-size=&quot;size16&quot;&gt;pod(컨테이너) 의 상태를 감시하는 역할도 수행하여 pod 의 상태를 API Server 로 전송합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1989&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 id=&quot;3.3.2-Kube-proxy&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1991&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.3.2 Kube proxy&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2009&quot; data-ke-size=&quot;size16&quot;&gt;각 노드에서 실행되는 네트워크 프록시입니다. 노드의 네트워크 규칙을 유지 관리하고 내부 네트워크 세션이나 클러스터 바깥에서 컨테이너로 네트워크 통신을 할 수 있도록 해줍니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2108&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 id=&quot;3.3.3-pod&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2110&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.3.3 pod&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;_K8s구조-pod.png&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d23pjE/btsKO5DdYeu/OBKOBPExFynsrMVWIAmSDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d23pjE/btsKO5DdYeu/OBKOBPExFynsrMVWIAmSDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d23pjE/btsKO5DdYeu/OBKOBPExFynsrMVWIAmSDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd23pjE%2FbtsKO5DdYeu%2FOBKOBPExFynsrMVWIAmSDk%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;350&quot; height=&quot;239&quot; data-filename=&quot;_K8s구조-pod.png&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;652&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;K8s 에서 배포할 수 있는 가장 작은 단위로, K8s 의 Object 들 중 하나입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2176&quot; data-ke-size=&quot;size16&quot;&gt;pod 는 하나 이상의 컨테이너와 스토리지 그리고 네트워크 속성 가지고 있으며, 컨테이너와 스토리지는 네트워크를 공유하며 상호 localhost 로 접근할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 id=&quot;4.-K8s-의-Object-들&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2275&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. K8s 의 Object 들&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2294&quot; data-ke-size=&quot;size16&quot;&gt;K8s 에는 대규모 분산환경에 필요한 요소들이 추상화 되어 있고 이러한 요소들을 Object 라고 부릅니다.&lt;/p&gt;
&lt;h4 id=&quot;4.1-어플리케이션-배포에-관한-Object-들&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2356&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.1 어플리케이션 배포에 관한 Object 들&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pod&lt;/li&gt;
&lt;li&gt;(pod) ReplicaSet
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;pod 여러개를 복제하여 관리하는 Object&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Deployment
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;K8s 에서 실제 배포를 진행하는 기본 단위로 배포 전략 선택, 배포 Revision, Roll Back 등의 기능을 사용하려면 이 Objet 를 이용해야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DaemonSet
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;클러스터 전체에 적용되는 pod 를 띄울때 사용하는 Controller로 DaemonSet 을 이용해 띄운 pod 는 항상 모든 node 에서 실행된다. 로그 수집이나 모니터링에 적합하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;StatefulSet
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;K8s Object 들은 대부분 Stateless 하다. Stateful 한 Volume 을 이용한 DB 등을 pod 로 만들때 사용하는 Object 이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;4.2-네트워크에-관한-object-들&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2785&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.2 네트워크에 관한 object 들&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Service&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pod 간 연결 및 pod 와 외부 네트워크를 연결 하는 object 이다.&lt;/li&gt;
&lt;li&gt;여러 pod 에 대한 L4 수준의 내부 로드밸런서의 역할을 수행하며 내부 DNS에 서비스를 등록하여 서비스 디스커버리 역할도 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;K8s_Object-Service.png&quot; data-origin-width=&quot;1132&quot; data-origin-height=&quot;744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rWuPA/btsKRw6GY6K/7K4XVTuls7Do4E9R8fkeYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rWuPA/btsKRw6GY6K/7K4XVTuls7Do4E9R8fkeYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rWuPA/btsKRw6GY6K/7K4XVTuls7Do4E9R8fkeYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrWuPA%2FbtsKRw6GY6K%2F7K4XVTuls7Do4E9R8fkeYk%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;420&quot; height=&quot;276&quot; data-filename=&quot;K8s_Object-Service.png&quot; data-origin-width=&quot;1132&quot; data-origin-height=&quot;744&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Ingress
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 트래픽을 받는 Object 로 http, https 등 L7 레벨에서 라우팅 규칙, 로드밸런싱 규칙 등을 설정한다.&lt;/li&gt;
&lt;li&gt;일반적으로 클러스터를 외부로 노출할때는 Ingress 를 이용한다.&lt;/li&gt;
&lt;/ul&gt;
&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;h3 id=&quot;5.-최종-K8s-구조&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3078&quot; data-ke-size=&quot;size23&quot;&gt;5. 최종 K8s 구조&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;이상의 K8s 구조, Object 를 종합하면 아래와 같이 그릴 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;K8s구조-전체.png&quot; data-origin-width=&quot;2844&quot; data-origin-height=&quot;1206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgoQbf/btsKQOUjG81/S3n71g9rVLd9hP8gkYnQZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgoQbf/btsKQOUjG81/S3n71g9rVLd9hP8gkYnQZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgoQbf/btsKQOUjG81/S3n71g9rVLd9hP8gkYnQZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdgoQbf%2FbtsKQOUjG81%2FS3n71g9rVLd9hP8gkYnQZk%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;650&quot; height=&quot;276&quot; data-filename=&quot;K8s구조-전체.png&quot; data-origin-width=&quot;2844&quot; data-origin-height=&quot;1206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>기초 지식/K8s</category>
      <category>k8s</category>
      <category>kubernetes</category>
      <category>msa k8s</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/46</guid>
      <comments>https://probehub.tistory.com/46#entry46comment</comments>
      <pubDate>Wed, 20 Nov 2024 20:40:16 +0900</pubDate>
    </item>
    <item>
      <title>Container와 Docker</title>
      <link>https://probehub.tistory.com/45</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;MSA-2-Docker.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bM0bCK/btsKQBtYGjs/pTPbTq1HiJagKHEcKmCEM0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bM0bCK/btsKQBtYGjs/pTPbTq1HiJagKHEcKmCEM0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bM0bCK/btsKQBtYGjs/pTPbTq1HiJagKHEcKmCEM0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbM0bCK%2FbtsKQBtYGjs%2FpTPbTq1HiJagKHEcKmCEM0%2Fimg.webp&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;350&quot; height=&quot;350&quot; data-filename=&quot;MSA-2-Docker.webp&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 id=&quot;1.-컨테이너와-도커의-개념-및-역할&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;컨테이너와 도커의 개념 및 역할&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&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: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;컨테이너는 애플리케이션과 그 애플리케이션이 실행되는 환경을 패키지화한 기술입니다. 이를 통해 개발 환경과 운영 환경의 차이로 인한 문제를 방지할 수 있습니다.&lt;/li&gt;
&lt;li&gt;컨테이너는 가상 머신(VM)과는 다르게, 호스트 OS의 커널을 공유하므로 경량화된 프로세스 격리를 제공합니다.&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: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;도커는 컨테이너 기술을 구현한 오픈소스 플랫폼으로, 컨테이너를 생성, 관리, 배포할 수 있는 도구입니다.&lt;/li&gt;
&lt;li&gt;도커는 애플리케이션 개발, 테스트, 배포를 일관된 방식으로 처리할 수 있도록 지원합니다.&lt;/li&gt;
&lt;li&gt;컨테이너 이미지를 빌드하고 실행하는 데 필요한 도구를 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2.-컨테이너의-특징과-도커를-활용한-애플리케이션-배포&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;359&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;컨테이너의 특징과 도커를 활용한 애플리케이션 배포&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&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: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;격리성&lt;/b&gt;: 각 컨테이너는 독립적으로 실행되어 다른 컨테이너나 호스트 시스템과 격리됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경량성&lt;/b&gt;: 가상 머신과 달리 호스트 OS의 커널을 공유하기 때문에 리소스 소모가 적습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이식성&lt;/b&gt;: 어디서나 동일한 환경에서 실행될 수 있도록 애플리케이션과 모든 종속성을 포함합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장성&lt;/b&gt;: 컨테이너는 빠르게 생성 및 삭제할 수 있어 대규모 트래픽에도 유연하게 대응할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;도커를 활용한 배포 프로세스&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;개발&lt;/b&gt;: 개발자는 도커 이미지를 정의하기 위해 Dockerfile을 작성합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트&lt;/b&gt;: 작성된 이미지를 도커 컨테이너로 실행하여 테스트합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배포&lt;/b&gt;: 도커 이미지를 컨테이너 레지스트리(예: Docker Hub, AWS ECR)에 저장한 뒤 운영 환경에 배포합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;운영&lt;/b&gt;: 컨테이너 오케스트레이션 도구(Kubernetes 등)를 활용해 운영 환경에서 컨테이너를 관리합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;3.-도커가-MSA-구현에-필수적인-이유&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;869&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;도커가 MSA 구현에 필수적인 이유&lt;/b&gt;&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&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: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&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: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;MSA에서는 서비스별로 독립적인 배포가 중요한데, 도커는 이를 손쉽게 구현할 수 있습니다.&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: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&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: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각 서비스가 독립된 컨테이너로 실행되기 때문에 하나의 서비스 장애가 다른 서비스에 영향을 미치지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DevOps 친화성&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;CI/CD 파이프라인에 쉽게 통합할 수 있어 빠르고 안정적인 배포 프로세스를 지원합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기초 지식/K8s</category>
      <category>MSA</category>
      <category>msa docker</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/45</guid>
      <comments>https://probehub.tistory.com/45#entry45comment</comments>
      <pubDate>Wed, 20 Nov 2024 20:23:42 +0900</pubDate>
    </item>
    <item>
      <title>MSA 개념</title>
      <link>https://probehub.tistory.com/44</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;MSA-1.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JZGUq/btsKPyyw4F0/MkggOvBiqw1X3j7Es1bIhK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JZGUq/btsKPyyw4F0/MkggOvBiqw1X3j7Es1bIhK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JZGUq/btsKPyyw4F0/MkggOvBiqw1X3j7Es1bIhK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJZGUq%2FbtsKPyyw4F0%2FMkggOvBiqw1X3j7Es1bIhK%2Fimg.webp&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;350&quot; height=&quot;350&quot; data-filename=&quot;MSA-1.webp&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 id=&quot;MSA-란?&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1&quot; data-ke-size=&quot;size23&quot;&gt;MSA 란?&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;하나의 애플리케이션을 다수의 독립적인 서비스로 구성한 아키텍처입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;50&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://martinfowler.com/articles/microservices.html&quot;&gt;마틴파울러의 블로그&lt;/a&gt;&lt;/b&gt;에 따르면 MSA 는 다음의 특징을 갖습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스들은 &lt;b&gt;각자 별도의 프로세스&lt;/b&gt;에서 실행된다.&lt;/li&gt;
&lt;li&gt;HTTP API 와 &lt;b&gt;같은 가벼운 매커니즘으로 통신&lt;/b&gt;하는 작은 애플리케이션이다.&lt;/li&gt;
&lt;li&gt;작은 서비스들은 &lt;b&gt;각자의 비즈니스 기능을 담당&lt;/b&gt;하고 &lt;b&gt;완전 자동화 된 절차에 따라 독립적으로 배포&lt;/b&gt;된다.&lt;/li&gt;
&lt;li&gt;각 서비스는 &lt;b&gt;서로 다른 프로그맹 언어&lt;/b&gt;, &lt;b&gt;프레임워크&lt;/b&gt;, &lt;b&gt;데이터 저장 기술&lt;/b&gt;을 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;275&quot; data-ke-size=&quot;size16&quot;&gt;대표적인 사례로는 Netflix 가 있으며, 국내에는 &lt;a style=&quot;color: #000000;&quot; href=&quot;https://medium.com/coupang-engineering/how-coupang-built-a-microservice-architecture-fd584fff7f2b&quot; data-renderer-mark=&quot;true&quot; data-testid=&quot;link-with-safety&quot;&gt;쿠팡&lt;/a&gt;, &lt;a style=&quot;color: #000000;&quot; href=&quot;https://www.youtube.com/watch?v=BnS6343GTkY&quot; data-renderer-mark=&quot;true&quot; data-testid=&quot;link-with-safety&quot;&gt;배달의 민족&lt;/a&gt; 등이 있습니다. MSA 의 어떤 점이 매력적이길래 이들은 MSA 를 도입한 걸까요?&lt;/p&gt;
&lt;h3 id=&quot;MSA-의-장점&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;365&quot; data-ke-size=&quot;size23&quot;&gt;MSA 의 장점&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;빠른 Delivery&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각 서비스는 독립적이므로 코드 수정 영향 범위가 작습니다.&lt;/li&gt;
&lt;li&gt;결과적으로, 고객에게 가치를 빠르게 전달할 수 있어 시장 경쟁력을 강화합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Polyglot Architecture&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각 서비스가 독립적으로 운영되므로 각 팀이 최적의 기술 스택을 선택할 수 있습니다.&lt;/li&gt;
&lt;li&gt;도메인 전문성이 증가하고, 팀의 자율성이 높아집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실험과 혁신 가능&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;코드베이스가 작아 수정으로 인한 영향 예측이 용이합니다.&lt;/li&gt;
&lt;li&gt;혁신적인 기술을 더 쉽게 도입할 수 있어서 IT 의존적인 비즈니스들은 시장역량을 강화할 수 있습니다.&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: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;특정 서비스만 Scale-out 할 수 있어 운영 효율성이 높아집니다.&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: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;서비스 단위로 기술을 변경하거나 외부 솔루션으로 교체가 용이합니다.&lt;/li&gt;
&lt;li&gt;이를 통해 &lt;a href=&quot;https://ko.wikipedia.org/wiki/NIH_%EC%A6%9D%ED%9B%84%EA%B5%B0&quot;&gt;&lt;b&gt;NIH(Not Invented Here) 증후군&lt;/b&gt;&lt;/a&gt;의 위험을 줄일 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;865&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;867&quot; data-ke-size=&quot;size16&quot;&gt;하지만 항상 기술 선택에 따른 트레이드오프가 있기 마렵입니다. MSA 는 다음과 같은 단점이 있어서 신중한 고려가 필요합니다.&lt;/p&gt;
&lt;h3 id=&quot;MSA-의-단점&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;939&quot; data-ke-size=&quot;size23&quot;&gt;MSA 의 단점&lt;span&gt;&lt;span data-vc=&quot;icon-undefined&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&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: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각 서비스가 독립적으로 실행되기 때문에 컴퓨팅 자원(중복 런타임 리소스, 네트워크)을 더 많이 사용합니다.&lt;/li&gt;
&lt;li data-renderer-start-pos=&quot;1035&quot;&gt;예: JVM 런타임 중복 실행, 마샬링/언마샬링 오버헤드 등.&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: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;모니터링 대상이 증가할 뿐만아니라 모니터링 해야하는 기술도 다양해집니다.&lt;/li&gt;
&lt;li&gt;각 서비스가 상호 의존적이기 때문에 단위 테스트 난이도가 증가합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB 트랜잭션의 모호함&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;서비스간 서로 다른 DB 를 사용하기 때문에 애초에 DB 수준에서 트랜잭션 관리가 안됩니다.&lt;/li&gt;
&lt;li&gt;데이터 일관성이 중요한 서비스에서는 MSA 도입을 신중히 도입해야합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1294&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1296&quot; data-ke-size=&quot;size16&quot;&gt;MSA는 많은 장점이 있지만, 그만큼 기술적인 어려움도 존재합니다. 이러한 어려움을 해결하기 위해 컨테이너 기술과 오케스트레이션 도구(Kubernetes) 같은 기술들이 필요합니다.&lt;/p&gt;</description>
      <category>기초 지식/MSA</category>
      <category>MSA</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/44</guid>
      <comments>https://probehub.tistory.com/44#entry44comment</comments>
      <pubDate>Wed, 20 Nov 2024 20:21:59 +0900</pubDate>
    </item>
    <item>
      <title>Terraform 기초: depends_on, output</title>
      <link>https://probehub.tistory.com/41</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;terraform-depend-output.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JkwF3/btsKOxkna57/buGTKTnhw1Wahr5znh9GIk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JkwF3/btsKOxkna57/buGTKTnhw1Wahr5znh9GIk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JkwF3/btsKOxkna57/buGTKTnhw1Wahr5znh9GIk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJkwF3%2FbtsKOxkna57%2FbuGTKTnhw1Wahr5znh9GIk%2Fimg.webp&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;350&quot; height=&quot;350&quot; data-filename=&quot;terraform-depend-output.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Terraform 의존성과 Output 활용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 알아본 Terraform 블록은 resource와 variable이 전부였다. 이번 글에서는 &lt;b&gt;output&lt;/b&gt;이라는 유용한 블록을 소개하고, &lt;b&gt;depends_on&lt;/b&gt;이라는 argument를 다룬다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. depends_on&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 리소스를 관리하다 보면 &quot;의존적인 관계&quot;에 있는 리소스들이 존재한다. AWS를 예로 들자면, 특정 VPC에 종속되어야 하는 security group은 VPC 리소스에 의존적일 수밖에 없다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.1 암시적 의존성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform은 리소스 간의 참조 관계를 분석하여 자동으로 의존성을 생성한다. 예를 들어, security group이 특정 VPC의 ID를 참조할 때, Terraform은 이를 기반으로 암시적 의존성을 설정한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;
resource &quot;aws_vpc&quot; &quot;example&quot; {
  cidr_block = &quot;10.0.0.0/16&quot;
}

resource &quot;aws_security_group&quot; &quot;example&quot; {
  vpc_id = aws_vpc.example.id
  name   = &quot;example_sg&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 `aws_security_group`은 `aws_vpc`에 암시적으로 의존한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.2 명시적 의존성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;암시적 의존성으로 충분하지 않은 경우, &lt;code&gt;depends_on&lt;/code&gt;을 사용하여 명시적으로 의존성을 설정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;
resource &quot;aws_vpc&quot; &quot;example&quot; {
  cidr_block = &quot;10.0.0.0/16&quot;
}

resource &quot;aws_security_group&quot; &quot;example&quot; {
  name   = &quot;example_sg&quot;
  depends_on = [aws_vpc.example]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 `aws_security_group`은 명시적으로 `aws_vpc`에 의존하도록 지정된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Output&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;output&lt;/code&gt; 블록은 특정 값을 출력하거나 다른 Terraform 구성에서 사용할 수 있도록 전달한다. 주로 생성된 리소스의 ID, 속성값, 또는 설정값을 화면에 출력하는 데 사용된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.1 Output 기본 사용&lt;/h4&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;
output &quot;vpc_id&quot; {
  value = aws_vpc.example.id
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform apply 후, 위 output은 생성된 VPC의 ID를 화면에 출력한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.2 다른 구성으로 전달&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Output은 Terraform 모듈 간 데이터를 전달하는 데에도 활용된다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;
output &quot;subnet_ids&quot; {
  value = aws_subnet.example.*.id
}

# 다른 Terraform 구성에서:
module &quot;network&quot; {
  source = &quot;./network_module&quot;
}

output &quot;module_subnet_ids&quot; {
  value = module.network.subnet_ids
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Output은 리소스의 결과를 공유하고, 작업 결과를 효율적으로 활용하는 데 필수적이다.&lt;/p&gt;</description>
      <category>탐구 생활/Terraform</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/41</guid>
      <comments>https://probehub.tistory.com/41#entry41comment</comments>
      <pubDate>Tue, 19 Nov 2024 21:14:12 +0900</pubDate>
    </item>
    <item>
      <title>Terraform 기초:  변수와 변수의 활용</title>
      <link>https://probehub.tistory.com/40</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;terraform-variable.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcjFwe/btsKN6glcwy/4qFrUeJay0uSwvXF055ZdK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcjFwe/btsKN6glcwy/4qFrUeJay0uSwvXF055ZdK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcjFwe/btsKN6glcwy/4qFrUeJay0uSwvXF055ZdK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcjFwe%2FbtsKN6glcwy%2F4qFrUeJay0uSwvXF055ZdK%2Fimg.webp&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;350&quot; height=&quot;350&quot; data-filename=&quot;terraform-variable.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Terraform 변수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;terraform 은 대규모 프로비저닝 관리를 위해 &lt;code&gt;변수&lt;/code&gt; 를 지원해준다. 변수는 어떻게 구성되어 있고, 어떻게 사용하는지 알아보자&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 Terraform 변수의 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수는 3개의 argument 로 구성된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;default: 변수의 기본 값을 지정한다.&lt;/li&gt;
&lt;li&gt;type: 변수의 타입 을 지정한다.&lt;/li&gt;
&lt;li&gt;description: 변수에 대한 메타데이터를 지정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 변수를 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;variable &quot;filename&quot; {
 default = &quot;pets.txt&quot;
 type = string
 description = &quot;name of file&quot;
}

variable &quot;length&quot; {
 default = 2
 type = number
 description = &quot;length of ...&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.2 Terraform 변수 타입&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform은 인프라를 코드로 관리하기 위해 다양한 데이터 타입을 지원한다. 이러한 타입을 이해하면 효율적인 인프라 정의와 관리를 할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.2.1. 기본 타입 (Primitive Types):&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;문자열 (string):&lt;/b&gt; 유니코드 문자들의 시퀀스로, 예를 들어 &quot;hello&quot;와 같다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;숫자 (number):&lt;/b&gt; 정수와 부동소수점을 모두 포함하며, 예를 들어 15나 6.283185 등이 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;불리언 (bool):&lt;/b&gt; 참(true) 또는 거짓(false) 값을 가지며, 조건문 등에 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.2.2. 복합 타입 (Complex Types):&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복합 타입은 collection type 과 structural type 으로 구분지을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;collection type 에는 list, map, set 이 있고 structural type 에는 object 와 tuple 타입이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;리스트 (list)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 타입의 값들이 순서대로 나열된 시퀀스이다. 예를 들어, [&quot;us-west-1a&quot;, &quot;us-west-1c&quot;] 가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;list 를 이용하는 방법은 다음과 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;terraform-variable-list.png&quot; data-origin-width=&quot;3342&quot; data-origin-height=&quot;808&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qnep0/btsKM6V10Ye/5vQNjc64vHd09qO43wocZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qnep0/btsKM6V10Ye/5vQNjc64vHd09qO43wocZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qnep0/btsKM6V10Ye/5vQNjc64vHd09qO43wocZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqnep0%2FbtsKM6V10Ye%2F5vQNjc64vHd09qO43wocZ0%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;3342&quot; height=&quot;808&quot; data-filename=&quot;terraform-variable-list.png&quot; data-origin-width=&quot;3342&quot; data-origin-height=&quot;808&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;맵 (map)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키-값 쌍으로 이루어진 집합으로, 각 키는 고유해야 한다. 예를 들어, {&quot;name&quot; = &quot;Mabel&quot;, &quot;age&quot; = 52} 가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;terraform-variable-map.png&quot; data-origin-width=&quot;2806&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AQN73/btsKOnhUi9V/6STgBnjFWYDL1FcK5ECkQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AQN73/btsKOnhUi9V/6STgBnjFWYDL1FcK5ECkQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AQN73/btsKOnhUi9V/6STgBnjFWYDL1FcK5ECkQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAQN73%2FbtsKOnhUi9V%2F6STgBnjFWYDL1FcK5ECkQK%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;2806&quot; height=&quot;840&quot; data-filename=&quot;terraform-variable-map.png&quot; data-origin-width=&quot;2806&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;셋 (set)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고유한 값들의 집합으로, 순서가 없으며 중복을 허용하지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;terraform-variable-set.png&quot; data-origin-width=&quot;3230&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhdkd0/btsKMFLlfS6/97FFSBnnjVRvVIKaKNyBs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhdkd0/btsKMFLlfS6/97FFSBnnjVRvVIKaKNyBs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhdkd0/btsKMFLlfS6/97FFSBnnjVRvVIKaKNyBs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhdkd0%2FbtsKMFLlfS6%2F97FFSBnnjVRvVIKaKNyBs0%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;3230&quot; height=&quot;612&quot; data-filename=&quot;terraform-variable-set.png&quot; data-origin-width=&quot;3230&quot; data-origin-height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 set 타입으로 만든 변수는 이를 수용할 수 있는 argument 가 특정되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이떄 string 타입을 가져오기 위해서는 다양한 방법을 사용할 수 있는데, 아래는 그 예시 3개를 보여준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;terraform-variable-set-solve.png&quot; data-origin-width=&quot;3322&quot; data-origin-height=&quot;790&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmVp9f/btsKN4iBWjn/SRTQEtQsi9nn0s9YdwleP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmVp9f/btsKN4iBWjn/SRTQEtQsi9nn0s9YdwleP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmVp9f/btsKN4iBWjn/SRTQEtQsi9nn0s9YdwleP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmVp9f%2FbtsKN4iBWjn%2FSRTQEtQsi9nn0s9YdwleP1%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;3322&quot; height=&quot;790&quot; data-filename=&quot;terraform-variable-set-solve.png&quot; data-origin-width=&quot;3322&quot; data-origin-height=&quot;790&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;객체(Object), 튜플(Tuple)&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;terraform-variable-object-tuple.png&quot; data-origin-width=&quot;2954&quot; data-origin-height=&quot;1472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lZjh6/btsKNe7dFCT/jxm6Gjc2g7cTnGlRuhxiKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lZjh6/btsKNe7dFCT/jxm6Gjc2g7cTnGlRuhxiKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lZjh6/btsKNe7dFCT/jxm6Gjc2g7cTnGlRuhxiKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlZjh6%2FbtsKNe7dFCT%2Fjxm6Gjc2g7cTnGlRuhxiKk%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;2954&quot; height=&quot;1472&quot; data-filename=&quot;terraform-variable-object-tuple.png&quot; data-origin-width=&quot;2954&quot; data-origin-height=&quot;1472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 특수 타입:&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;null:&lt;/b&gt; 값의 부재를 나타내며, 리소스의 특정 속성을 생략하고자 할 때 사용된다.&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;h3 data-ke-size=&quot;size23&quot;&gt;1.3 Terraform 변수 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;variable 블록을 기존과 같이 resource 가 선언된 블록에서 같이 관리해도 된다. 하지만 동일 작업공간에서 terraform 은 .tf 파일을 모두 읽어들여서 작업을 진행하므로 굳이 둘을 하나의 파일에서 관리하지 말고 main.tf 와 variable.tf 로 나눠보도록 하자&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;main.tf: 리소스를 정의하는 테라폼 파일&lt;/li&gt;
&lt;li&gt;variable.tf: 리소스에서 사용할 변수를 정의하는 테라폼 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HCL 기본기에서 다뤘듯이 argument 에 expression 을 대입할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-18 오후 10.13.44.png&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;263&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHhWxX/btsKMeAdzhQ/Xq195GwaVjCuRlgvj75wIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHhWxX/btsKMeAdzhQ/Xq195GwaVjCuRlgvj75wIk/img.png&quot; data-alt=&quot;main.tf&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHhWxX/btsKMeAdzhQ/Xq195GwaVjCuRlgvj75wIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHhWxX%2FbtsKMeAdzhQ%2FXq195GwaVjCuRlgvj75wIk%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;650&quot; height=&quot;248&quot; data-filename=&quot;스크린샷 2024-11-18 오후 10.13.44.png&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;263&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;main.tf&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-18 오후 10.13.49.png&quot; data-origin-width=&quot;707&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHV5Tn/btsKNestJsx/kmZq3xvwyTFN6QrYB8CAn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHV5Tn/btsKNestJsx/kmZq3xvwyTFN6QrYB8CAn1/img.png&quot; data-alt=&quot;variable.tf&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHV5Tn/btsKNestJsx/kmZq3xvwyTFN6QrYB8CAn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHV5Tn%2FbtsKNestJsx%2FkmZq3xvwyTFN6QrYB8CAn1%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;650&quot; height=&quot;383&quot; data-filename=&quot;스크린샷 2024-11-18 오후 10.13.49.png&quot; data-origin-width=&quot;707&quot; data-origin-height=&quot;417&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;variable.tf&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 terraform init &amp;gt; terraform paln &amp;gt; terraform apply 순서로 돌리면 작업공간에 pets.txt 가 생성되면서 랜덤한 문자열이 생성된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.4 Terraform 변수 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 이제는 variable 의 내용만 바꾸면 변경 사항을 반영할 수 있지 않을까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-18 오후 10.16.50.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RhBfX/btsKOerLv2a/3CKpaak6ed0vx43MG9bfSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RhBfX/btsKOerLv2a/3CKpaak6ed0vx43MG9bfSK/img.png&quot; data-alt=&quot;변경된 variable.tf&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RhBfX/btsKOerLv2a/3CKpaak6ed0vx43MG9bfSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRhBfX%2FbtsKOerLv2a%2F3CKpaak6ed0vx43MG9bfSK%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;650&quot; height=&quot;418&quot; data-filename=&quot;스크린샷 2024-11-18 오후 10.16.50.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;변경된 variable.tf&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;terraform paln &amp;gt; terraform apply 를 한 결과 기존 작업공간에 있던 pets.txt 는 사라지고 pets2.txt 가 생성되었다. 그리고 그 내용도 랜덤 변수 3개가 생성되는 것으로 바뀌었다.&lt;/p&gt;</description>
      <category>기초 지식/Terraform</category>
      <category>terraform variable</category>
      <category>terraform 변수</category>
      <category>테라폼 변수</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/40</guid>
      <comments>https://probehub.tistory.com/40#entry40comment</comments>
      <pubDate>Mon, 18 Nov 2024 21:25:07 +0900</pubDate>
    </item>
    <item>
      <title>Terraform 기초: 리소스 만들어보기</title>
      <link>https://probehub.tistory.com/39</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;terraform-tutorial.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qwWHL/btsKNvm2XJ3/8mj18512kHFBbkvYNHFu2k/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qwWHL/btsKNvm2XJ3/8mj18512kHFBbkvYNHFu2k/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qwWHL/btsKNvm2XJ3/8mj18512kHFBbkvYNHFu2k/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqwWHL%2FbtsKNvm2XJ3%2F8mj18512kHFBbkvYNHFu2k%2Fimg.webp&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;350&quot; height=&quot;350&quot; data-filename=&quot;terraform-tutorial.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Terraform 실습&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 스크립트는 init -&amp;gt; plan -&amp;gt; apply 명령어 순으로 적용하여 리소스를 프로비저닝 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 순서에 따라서 local device에 txt 파일을 만들어본 후 txt 파일을 업데이트 해보겠다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 스크립트 작성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 스크립트는 이전 &lt;a href=&quot;https://probehub.tistory.com/38&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Terraform:HCL 기본기&lt;/a&gt; 에서 살펴본것과 같이 Block 단위로 Resource 와 Argument 를 명시해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-18 오후 12.04.29.png&quot; data-origin-width=&quot;2138&quot; data-origin-height=&quot;870&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cO7qFP/btsKNdfdvq3/gNyVT06CvhMQAZLzPuuz41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cO7qFP/btsKNdfdvq3/gNyVT06CvhMQAZLzPuuz41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cO7qFP/btsKNdfdvq3/gNyVT06CvhMQAZLzPuuz41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcO7qFP%2FbtsKNdfdvq3%2FgNyVT06CvhMQAZLzPuuz41%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;550&quot; height=&quot;224&quot; data-filename=&quot;스크린샷 2024-11-18 오후 12.04.29.png&quot; data-origin-width=&quot;2138&quot; data-origin-height=&quot;870&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 terraform-tutorial 이라는 디렉터리를 만들고 그 공간에서 실습을 진행했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;script_file.png&quot; data-origin-width=&quot;547&quot; data-origin-height=&quot;97&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJchSn/btsKM3Ye6G1/5fxk1jOu9xCd3ZpbPjIdok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJchSn/btsKM3Ye6G1/5fxk1jOu9xCd3ZpbPjIdok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJchSn/btsKM3Ye6G1/5fxk1jOu9xCd3ZpbPjIdok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJchSn%2FbtsKM3Ye6G1%2F5fxk1jOu9xCd3ZpbPjIdok%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;550&quot; height=&quot;98&quot; data-filename=&quot;script_file.png&quot; data-origin-width=&quot;547&quot; data-origin-height=&quot;97&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2 terraform init&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;script 를 적절히 작성한 후 terraform init 을 실행하면 작업공간(terraform-tutorial) 에 .terraform 디렉터리와 .terraform.locl.hcl 파일이 생성된다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;after init.png&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAoIdU/btsKNbBFVZG/3QLSuHM92syx18XkAJzpXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAoIdU/btsKNbBFVZG/3QLSuHM92syx18XkAJzpXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAoIdU/btsKNbBFVZG/3QLSuHM92syx18XkAJzpXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAoIdU%2FbtsKNbBFVZG%2F3QLSuHM92syx18XkAJzpXk%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;650&quot; height=&quot;420&quot; data-filename=&quot;after init.png&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;434&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.1 terraform 디렉터리&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-18 오후 1.07.42.png&quot; data-origin-width=&quot;643&quot; data-origin-height=&quot;207&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvXcKc/btsKNsXUBHd/Kh5IvbKVjloLUKuC74zR40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvXcKc/btsKNsXUBHd/Kh5IvbKVjloLUKuC74zR40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvXcKc/btsKNsXUBHd/Kh5IvbKVjloLUKuC74zR40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvXcKc%2FbtsKNsXUBHd%2FKh5IvbKVjloLUKuC74zR40%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;650&quot; height=&quot;209&quot; data-filename=&quot;스크린샷 2024-11-18 오후 1.07.42.png&quot; data-origin-width=&quot;643&quot; data-origin-height=&quot;207&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.terraform 디렉터리는 Terraform 의 작업공간으로서 &lt;b&gt;&quot;프로젝트 상태&quot;&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;플러그인(Providers) 캐싱: 프로젝트에 사용되는 &lt;b&gt;Provider 들의 바이너리가 저장&lt;/b&gt;된다. 덕분에 매번 동일한 Provider 를 다운로드 받지 않아도 된다.&lt;/li&gt;
&lt;li&gt;Terraform 상태 관리: Terraform이 인프라를 프로비저닝하거나 업데이트하면서 필요한 메타데이터를 보관한다. S3 나 Terraform Cloud 같은 원격 상태를 사용할 경우 원격 상태 정보가 저장되기도 한다.&lt;/li&gt;
&lt;li&gt;Terraform 구성 파일 모듈 관리: 프로젝트에서 모듈을 사용할 경우, 다운로드된 모듈이 저장된다.&lt;/li&gt;
&lt;li&gt;Terraform 작업 설정: Terraform 실행 시 생성되는 파일이나 설정 정보가 저장된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.terraform 디렉터리를 삭제하면 다시 필요한 Provider 를 다운로드 하는 과정을 거친다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.2 terraform.lock.hcl&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-18 오후 1.07.31.png&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbB7Vp/btsKNU0Hrl8/DdIwNCWLLhdNkuooVq2Vek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbB7Vp/btsKNU0Hrl8/DdIwNCWLLhdNkuooVq2Vek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbB7Vp/btsKNU0Hrl8/DdIwNCWLLhdNkuooVq2Vek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbB7Vp%2FbtsKNU0Hrl8%2FDdIwNCWLLhdNkuooVq2Vek%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;650&quot; height=&quot;352&quot; data-filename=&quot;스크린샷 2024-11-18 오후 1.07.31.png&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.terraform.lock.hcl 파일은 Provider와 모듈의 버전을 고정하는 역할을 한다. 덕분에 실행 환경에서 동일한 버전을 사용할 수 있도록 보장된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3 terraform plan&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_after plan.png&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAZ1ip/btsKNU7wndL/q4kCCafL4BsvLVNwEtRW3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAZ1ip/btsKNU7wndL/q4kCCafL4BsvLVNwEtRW3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAZ1ip/btsKNU7wndL/q4kCCafL4BsvLVNwEtRW3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAZ1ip%2FbtsKNU7wndL%2Fq4kCCafL4BsvLVNwEtRW3K%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;650&quot; height=&quot;504&quot; data-filename=&quot;edited_after plan.png&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;terraform plan 커맨드를 입력하면, 사용자가 설정한 리소스 타입이 받아들인 argument 값을 알려주고 그에 따라 앞으로 생길 변화에 대해 리포트해준다. init 단계에서 생성된 .terraform 디렉터리와 .terraform.lock.hcl 에 영향은 없다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4 terraform apply&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_after apply.png&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;836&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TNcsG/btsKL7UWatq/lo7oA735IABLrsZCPSk3SK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TNcsG/btsKL7UWatq/lo7oA735IABLrsZCPSk3SK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TNcsG/btsKL7UWatq/lo7oA735IABLrsZCPSk3SK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTNcsG%2FbtsKL7UWatq%2Flo7oA735IABLrsZCPSk3SK%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;650&quot; height=&quot;601&quot; data-filename=&quot;edited_after apply.png&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;836&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;terraform apply 커맨드를 입력하면 plan 에 따른 리소스 생성, 변경, 삭제가 이루어진다. 그리고 apply 가 될 때 terraform.tfstate 파일이 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.1 terraform.tfstate&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;terraform.tfstate 파일은 Terraform이 관리하는 &lt;b&gt;현재 인프라의 상태&lt;/b&gt;를 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 저장된 상태 정보는 Terraform이 정의한 Desired State(원하는 상태)와 Current State(현재 상태)를 비교하는 데 사용된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2024-11-18 오후 1.36.44.png&quot; data-origin-width=&quot;1321&quot; data-origin-height=&quot;875&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/drwCGb/btsKNTgyBqh/iyz2XkYPGxQoIUOKgvext1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/drwCGb/btsKNTgyBqh/iyz2XkYPGxQoIUOKgvext1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/drwCGb/btsKNTgyBqh/iyz2XkYPGxQoIUOKgvext1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdrwCGb%2FbtsKNTgyBqh%2Fiyz2XkYPGxQoIUOKgvext1%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;850&quot; height=&quot;563&quot; data-filename=&quot;edited_스크린샷 2024-11-18 오후 1.36.44.png&quot; data-origin-width=&quot;1321&quot; data-origin-height=&quot;875&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 생성된 terraform.tfstate 파일을 확인해보면 plan 에 나왔던 argument 들이 어떤 값이 들어가 있는지, 어떤 프로바이더가 사용되었는지 등의 정보가 생성된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5 리소스 수정&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.1 스크립트 수정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;terraform 을 통해 생성된 리소스를 수정하기 위해 스크립트를 수정한다. 기존 pets.txt 의 content 를 변경했고, pets.json 이라는 파일을 추가하도록 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-18 오후 1.30.26.png&quot; data-origin-width=&quot;478&quot; data-origin-height=&quot;287&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lWn1Q/btsKNdNdwxw/fsrKkNo4KVNsRYkzO5zYy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lWn1Q/btsKNdNdwxw/fsrKkNo4KVNsRYkzO5zYy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lWn1Q/btsKNdNdwxw/fsrKkNo4KVNsRYkzO5zYy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlWn1Q%2FbtsKNdNdwxw%2FfsrKkNo4KVNsRYkzO5zYy1%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;550&quot; height=&quot;330&quot; data-filename=&quot;스크린샷 2024-11-18 오후 1.30.26.png&quot; data-origin-width=&quot;478&quot; data-origin-height=&quot;287&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.2 paln &amp;amp; apply&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 해당 작업공간에는 terraform init 이 되어 있으므로 추가로 init 할 필요는 없다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;terraform plan 커맨드를 입력하면 변경된 스크립트에 기반하여 어떤 변화가 생길지&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2024-11-18 오후 1.39.20.png&quot; data-origin-width=&quot;1424&quot; data-origin-height=&quot;848&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0rrTl/btsKMLcD4KA/Qfh9kbiixPuG324yrI1IbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0rrTl/btsKMLcD4KA/Qfh9kbiixPuG324yrI1IbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0rrTl/btsKMLcD4KA/Qfh9kbiixPuG324yrI1IbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0rrTl%2FbtsKMLcD4KA%2FQfh9kbiixPuG324yrI1IbK%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;1424&quot; height=&quot;848&quot; data-filename=&quot;edited_스크린샷 2024-11-18 오후 1.39.20.png&quot; data-origin-width=&quot;1424&quot; data-origin-height=&quot;848&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흥미로운 점은 pets.txt 파일은 change 되는게 이니라 &lt;b&gt;replaced&lt;/b&gt; 된다는 점이다. 덕분에 최종 plan 에는 2 to add, 0 to change, 1 to destroy 가 표시되었다. 이런식으로 변경된 사항만 바뀌는게 아니라아예 없애고 새롭게 만드게 하는것을 &lt;b&gt;Immutable Infrastructure&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;terraform apply 커맨드를 입력해보자, yes 를 누르면 plan 에 있던 변화가 반영된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-18 오후 1.42.57.png&quot; data-origin-width=&quot;1433&quot; data-origin-height=&quot;1001&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v54OF/btsKLhXW17Z/NKQv3pWTUOCINbR31LDJLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v54OF/btsKLhXW17Z/NKQv3pWTUOCINbR31LDJLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v54OF/btsKLhXW17Z/NKQv3pWTUOCINbR31LDJLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv54OF%2FbtsKLhXW17Z%2FNKQv3pWTUOCINbR31LDJLk%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;1433&quot; height=&quot;1001&quot; data-filename=&quot;스크린샷 2024-11-18 오후 1.42.57.png&quot; data-origin-width=&quot;1433&quot; data-origin-height=&quot;1001&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6. 리소스 삭제&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 공간에서 terraform destroy 를 입력하면 terraform 에 의해 관리되던 리소스를 destroy 하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2024-11-18 오후 3.46.44.png&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;943&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgXOKr/btsKL9FI4hJ/WIiKrt4H5hLzcjKLyvKUUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgXOKr/btsKL9FI4hJ/WIiKrt4H5hLzcjKLyvKUUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgXOKr/btsKL9FI4hJ/WIiKrt4H5hLzcjKLyvKUUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgXOKr%2FbtsKL9FI4hJ%2FWIiKrt4H5hLzcjKLyvKUUk%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;790&quot; height=&quot;943&quot; data-filename=&quot;edited_스크린샷 2024-11-18 오후 3.46.44.png&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;943&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>기초 지식/Terraform</category>
      <category>Terraform</category>
      <category>terraform tutorial</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/39</guid>
      <comments>https://probehub.tistory.com/39#entry39comment</comments>
      <pubDate>Mon, 18 Nov 2024 13:44:02 +0900</pubDate>
    </item>
    <item>
      <title>Terraform 기초: HCL 기본기</title>
      <link>https://probehub.tistory.com/38</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;DALL&amp;amp;middot;E 2024-11-18 00.12.42 - A thumbnail image for a blog post explaining the basic structure of Terraform HCL (HashiCorp Configuration Language). The image should feature a clean.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQdzFr/btsKLoPYjUF/V4A62xR54kNaY7EYMQloZk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQdzFr/btsKLoPYjUF/V4A62xR54kNaY7EYMQloZk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQdzFr/btsKLoPYjUF/V4A62xR54kNaY7EYMQloZk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQdzFr%2FbtsKLoPYjUF%2FV4A62xR54kNaY7EYMQloZk%2Fimg.webp&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;350&quot; height=&quot;350&quot; data-filename=&quot;DALL&amp;middot;E 2024-11-18 00.12.42 - A thumbnail image for a blog post explaining the basic structure of Terraform HCL (HashiCorp Configuration Language). The image should feature a clean.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무리 HCL 이 쉬운 DSL 이라곤 하지만 정확히 쓰고 읽기 위해서는 최소한의 공부가 필요하다. 우선 Terraform 을 설치하는게 우선이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Terraform 설치 (업그레이드)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 좋은 방법은 &lt;a href=&quot;https://developer.hashicorp.com/terraform/install&quot;&gt;공식 사이트의 설치 가이드&lt;/a&gt;를 보고 설치하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 mac 을 이용하고 brew 를 통해 설치, 삭제, 업그레이드를 관리하는 것이 용이하므로 brew 로 설치했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 버전 1.8.2 가 설치되어 있는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;$&amp;gt; brew list terraform
# /opt/homebrew/Cellar/terraform/1.8.2/bin/terraform

$&amp;gt; terraform version
# Terraform v1.8.2
# on darwin_arm64&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;24년 11월 17일 기준 terraform 최신 버전은 1.9.8 이므로 업그레이드가 필요했다. 이럴때 brew 가 빛을 발한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;$&amp;gt; brew update
# ==&amp;gt; Updating Homebrew...

$&amp;gt; brew upgrade terraform
# ==&amp;gt; Upgrading 1 outdated package:
# hashicorp/tap/terraform 1.8.2 -&amp;gt; 1.9.8&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. HCL 문법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에 따르면 Terraform 을 이용하기 위해 모든 HCL 문법을 디테일하게 알 필요는 없다고 한다. (그럼에도 궁금하다면 &lt;a href=&quot;https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md&quot;&gt;HCL Spec&lt;/a&gt; 을 확인하자) 그리고 HCL 이 아닌 &lt;a href=&quot;https://developer.hashicorp.com/terraform/language/syntax/json&quot;&gt;JSON 형태&lt;/a&gt;로도 동일한 기능을 수행하게 할 수 있는데, 개발자가 직접 다루기보다는 프로그래밍적으로 다룰때 유용한 형태이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.1 블록 (block)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HCL 언어는 주로 &lt;b&gt;블록(block)&lt;/b&gt;과 &lt;b&gt;인수(argument)&lt;/b&gt;로 구성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블록은 특정 리소스나 설정을 정의하는 기본 단위이다. 각 블록은 블록 유형, 라벨, 그리고 본문으로 구성된다. 예를 들어, AWS의 EC2 인스턴스를 정의하는 블록은 다음과 같다:&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;resource &quot;aws_instance&quot; &quot;example&quot; {
  ami           = &quot;ami-0c55b159cbfafe1f0&quot;
  instance_type = &quot;t2.micro&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;code&gt;resource&lt;/code&gt;는 블록 유형을, &lt;code&gt;aws_instance&lt;/code&gt;와 &lt;code&gt;example&lt;/code&gt;은 각각 리소스 유형과 로컬 이름을 나타낸다. 블록 본문에는 해당 리소스의 속성을 정의하는 인수들이 포함된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.2 인수(argument)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인수는 위의 예제에서 속성에 값을 할당하는 데 사용된다. &lt;code&gt;ami&lt;/code&gt; 와 &lt;code&gt;instnace_type&lt;/code&gt; 이 인수이며, 각각의 등호(&lt;code&gt;=&lt;/code&gt;) 를 통해 할당된다. 인수의 값은 리터럴 값, 변수, 또는 표현식으로 지정할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.3 표현식 (expression)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인수의 값에 표현식을 지정한다는 것의 예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;resource &quot;aws_subnet&quot; &quot;example&quot; {
  vpc_id = aws_vpc.example.id
  cidr_block = &quot;10.0.1.0/24&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;code&gt;vpc_id&lt;/code&gt; 에 &lt;code&gt;aws_vpc.example.id&lt;/code&gt; 를 이용하여 다른 리소스의 id 를 참조하도록 하고 있는 형태가 표현식을 인수의 값으로 지정한 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.4 주석 (Comment)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 구성 파일에서 주석은 &lt;code&gt;#&lt;/code&gt; 또는 &lt;code&gt;//&lt;/code&gt;로 시작하는 한 줄 주석과, &lt;code&gt;/* */&lt;/code&gt;로 감싸는 여러 줄 주석이 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 파일 구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 구성은 .tf 확장자를 가진 파일로 작성되며, 하나의 디렉토리에 여러 파일이 있을 경우 Terraform은 이를 모두 읽어들여 하나의 구성으로 처리한다. 따라서 관련된 리소스나 모듈을 논리적으로 분리하여 파일을 구성하는 것이 좋다.&lt;/p&gt;</description>
      <category>탐구 생활/Terraform</category>
      <category>HCL</category>
      <category>Terraform</category>
      <category>terraform hcl</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/38</guid>
      <comments>https://probehub.tistory.com/38#entry38comment</comments>
      <pubDate>Sun, 17 Nov 2024 23:26:37 +0900</pubDate>
    </item>
    <item>
      <title>Terraform 소개</title>
      <link>https://probehub.tistory.com/37</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. &lt;b&gt;Terraform&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.1 Terraform 이란?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform은 HashiCorp에서 개발한 무료 오픈소스 IAC 도구이다. &lt;a href=&quot;https://probehub.tistory.com/36&quot;&gt;IAC 개요 및 주요 도구&lt;/a&gt;에서 다룬 것과 같이 주로 &lt;b&gt;인프라 프로비저닝&lt;/b&gt;에 사용되는 툴로써, 설치 파일은 단일 바이너리로 구성돼 있어서 빠르게 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform의 가장 큰 장점 중 하나는 여러 플랫폼(프라이빗 클라우드와 퍼블릭 클라우드)을 아우르는 인프라 배포가 가능하다는 거이다. 예를 들어, 온프레미스 vSphere 클러스터부터 AWS, GCP, Azure 같은 클라우드 솔루션까지 지원한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.2 Terraform 과 HashiCorp&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HashiCorp 는 클라우드 인프라를 효율적으로 관리하고 자동화하기 위한 다양한 도구를 개발하는 회사이다. HashiCorp의 제품은 DevOps, 클라우드, 그리고 인프라 관리에 초점을 맞추고 있으며 Terrafrom 도 그들의 제품 중 하나이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. &lt;b&gt;Terraform의 핵심: Providers&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;assets.webp&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;644&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bct9Zm/btsKNtvcAda/HrjdRyGURpmZ2n3FcBZKZK/tfile.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bct9Zm/btsKNtvcAda/HrjdRyGURpmZ2n3FcBZKZK/tfile.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bct9Zm/btsKNtvcAda/HrjdRyGURpmZ2n3FcBZKZK/tfile.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbct9Zm%2FbtsKNtvcAda%2FHrjdRyGURpmZ2n3FcBZKZK%2Ftfile.webp&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;650&quot; height=&quot;204&quot; data-filename=&quot;assets.webp&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;644&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform이 이렇게 다양한 플랫폼을 지원할 수 있는 이유는 &lt;b&gt;Provider&lt;/b&gt; 덕분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Provider 는 HashiCorp 와 Terraform Community 에 의해 작성, 유지되며 외부 플랫폼을 API 형태로 생성, 관리 할 수 있도록 도와주는 역할을 한다. 대표적으로 다음과 같은 Provider 들이 있다:&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;: AWS, GCP, Azure&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네트워크 인프라&lt;/b&gt;: CloudFlare, DNS, Palo Alto Networks&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모니터링 및 데이터 관리 도구&lt;/b&gt;: DataDog, Grafana, Sumo Logic&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터베이스&lt;/b&gt;: MySQL, PostgreSQL, MongoDB&lt;/li&gt;
&lt;li&gt;&lt;b&gt;버전 관리 시스템&lt;/b&gt;: GitHub, GitLab, Bitbucket&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에도 &lt;a href=&quot;https://registry.terraform.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Terraform Registry&lt;/a&gt; 에서 수천개 이상의 Provider 를 찾을 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. &lt;b&gt;Terraform의 언어: HCL&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform은 HashiCorp Configuration Language(HCL)라는 선언적 언어를 사용한다.&lt;br /&gt;이 언어를 이용하면 &lt;code&gt;.tf 확장자&lt;/code&gt;를 가진 구성 파일에 인프라 리소스를 코드로 정의 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HCL 은 코드 작성과 읽기가 쉬워서 초보자도 금방 배울 수 있다는 장점이 있다.&lt;/li&gt;
&lt;li&gt;선언형 코드로 현재 상태에서 &lt;b&gt;원하는 상태(Desired State)&lt;/b&gt;로 인프라를 관리할 수 있다.&lt;br /&gt;예를 들어, AWS에 EC2 인스턴스를 프로비저닝하는 코드를 작성하면, Terraform이 알아서 필요한 작업을 진행해서 현재 상태를 원하는 상태로 맞춰준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. &lt;b&gt;Terraform의 작업 단계&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform은 세 가지 주요 단계를 거쳐 작업한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;terraform-3step.webp&quot; data-origin-width=&quot;2038&quot; data-origin-height=&quot;1773&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ah43S/btsKLvIjeUo/HpKXEjxkGIk7Yp100MZ0A0/tfile.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ah43S/btsKLvIjeUo/HpKXEjxkGIk7Yp100MZ0A0/tfile.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ah43S/btsKLvIjeUo/HpKXEjxkGIk7Yp100MZ0A0/tfile.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAh43S%2FbtsKLvIjeUo%2FHpKXEjxkGIk7Yp100MZ0A0%2Ftfile.webp&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;650&quot; height=&quot;565&quot; data-filename=&quot;terraform-3step.webp&quot; data-origin-width=&quot;2038&quot; data-origin-height=&quot;1773&quot;/&gt;&lt;/span&gt;&lt;/figure&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;b&gt;Write&lt;/b&gt;: 프로젝트에 사용될 Provider 와 리소스를 정의하고&amp;nbsp;원하는 상태를 작성한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Plan&lt;/b&gt;: Terrafrom 은 해당 리소스의 현재 상태와 원하는 상태를 &lt;b&gt;비교&lt;/b&gt;해서 실행 계획 작성한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Apply&lt;/b&gt;: 실행 계획대로 리소스를 생성, 수정, 삭제 등을 수행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 환경이 원하는 상태에서 벗어나면, Terraform이 자동으로 수정해서 다시 맞춰준다. 덕분에 인프라는 항상 정의된 상태로 유지된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. &lt;b&gt;Terraform의 상태(State)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform은 리소스의 상태를 기록해서 &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;b&gt;State 파일&lt;/b&gt;: 인프라의 설계도 같은 역할을 수행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 소스&lt;/b&gt;: 기존 리소스의 속성을 읽어서 다른 리소스 설정에 활용할 수도 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Import 기능&lt;/b&gt;: 수동으로 생성된 리소스나 다른 IAC 도구로 만든 리소스를 가져와서 Terraform 관리 하에 둘 수 있음&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>탐구 생활/Terraform</category>
      <category>Terraform</category>
      <category>terrafrom 이란?</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/37</guid>
      <comments>https://probehub.tistory.com/37#entry37comment</comments>
      <pubDate>Sun, 17 Nov 2024 23:24:09 +0900</pubDate>
    </item>
    <item>
      <title>IAC 개요 및 주요 도구</title>
      <link>https://probehub.tistory.com/36</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;DALL&amp;amp;middot;E 2024-11-17 19.39.58 - A visually engaging thumbnail for a blog post about 'Introduction to Infrastructure as Code (IAC).' The image should depict a modern and clean design .webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOgEDU/btsKL35I5gK/e0TvVEwVN6ltdnkphcxKr0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOgEDU/btsKL35I5gK/e0TvVEwVN6ltdnkphcxKr0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOgEDU/btsKL35I5gK/e0TvVEwVN6ltdnkphcxKr0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOgEDU%2FbtsKL35I5gK%2Fe0TvVEwVN6ltdnkphcxKr0%2Fimg.webp&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;350&quot; height=&quot;350&quot; data-filename=&quot;DALL&amp;middot;E 2024-11-17 19.39.58 - A visually engaging thumbnail for a blog post about 'Introduction to Infrastructure as Code (IAC).' The image should depict a modern and clean design .webp&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;1. &lt;b&gt;IAC(코드형 인프라)란?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAC(Infrastructure as Code)는 클라우드 인프라를 코드로 정의하고 관리하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 클라우드 관리 콘솔을 통해 수작업으로 리소스를 프로비저닝(provisioning)하는 대신, IAC를 통해 코드로 정의하고 실행하여 리소스를 자동으로 생성, 구성, 업데이트, 삭제할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAC는 데이터베이스, 네트워크, 스토리지, 애플리케이션 구성과 같은 다양한 인프라 구성 요소를 코드로 관리할 수 있도록 해준다. 예를 들어, 기존의 셸 스크립트를 사용해도 되지만, 이 방식은 관리와 재사용이 어려우며 유지보수를 위해 개발 기술을 필요로 한다. 이러한 문제를 해결하기 위해 Terraform이나 Ansible과 같은 IAC 도구가 등장했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. &lt;b&gt;주요 IAC 도구 소개&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAC 도구는 크게 세 가지 유형으로 분류할 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;구성 관리(Configuration Management) 도구&lt;/b&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;: Ansible, Chef, Puppet, SaltStack&lt;/li&gt;
&lt;li&gt;&lt;b&gt;용도&lt;/b&gt;: 기존 인프라 리소스(서버, 데이터베이스, 네트워크 장치 등)에 소프트웨어를 설치하고 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특징&lt;/b&gt;:
&lt;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;b&gt;Idempotent(멱등성)&lt;/b&gt;: 동일한 코드를 여러 번 실행해도 환경을 정의된 상태로 유지하며, 불필요한 변경을 방지&lt;/li&gt;
&lt;li&gt;재사용성과 버전 관리 가능 (예: Ansible Playbook)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버 템플릿(Server Templating) 도구&lt;/b&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;: Docker, Vagrant, Packer&lt;/li&gt;
&lt;li&gt;&lt;b&gt;용도&lt;/b&gt;: 필요한 소프트웨어와 종속성이 미리 설치된 VM(가상 머신) 이미지 또는 컨테이너 생성&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;b&gt;Immutable Infrastructure(불변 인프라)&lt;/b&gt;를 지향&lt;/li&gt;
&lt;li&gt;기존 인스턴스를 업데이트하는 대신 이미지를 업데이트하고 새로운 인스턴스를 재배포&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시&lt;/b&gt;: Amazon AWS의 Custom AMI, DockerHub의 Docker 이미지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로비저닝(Provisioning) 도구&lt;/b&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;b&gt;'&lt;span style=&quot;background-color: #ffffff;&quot;&gt;Terraform'&lt;/span&gt;&lt;/b&gt;, AWS CloudFormation&lt;/li&gt;
&lt;li&gt;&lt;b&gt;용도&lt;/b&gt;: 서버(VM), 데이터베이스, 네트워크(VPC, 서브넷, 보안 그룹 등)와 같은 인프라 구성 요소를 코드로 정의하고 프로비저닝&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;b&gt;CloudFormation&lt;/b&gt;: AWS에 특화된 도구&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Terraform&lt;/b&gt;: 클라우드 제공업체에 구애받지 않고 다양한 클라우드 제공업체 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. &lt;b&gt;IAC 도구 간 차이점 및 활용 사례&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;Ansible vs Terraform&lt;/b&gt;&lt;br /&gt;Ansible은 소프트웨어 구성 관리에 적합하며, Terraform은 인프라 프로비저닝에 적합하다. 각 도구는 특정 목적에 맞춰 설계되었으며, 적절한 도구를 선택하는 것이 중요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. &lt;b&gt;왜 IAC를 사용해야 할까?&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;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;</description>
      <category>탐구 생활/Terraform</category>
      <category>IAC</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/36</guid>
      <comments>https://probehub.tistory.com/36#entry36comment</comments>
      <pubDate>Sun, 17 Nov 2024 19:40:43 +0900</pubDate>
    </item>
    <item>
      <title>python 사용자 지정 불변 객체를 만드는 3가지 방법</title>
      <link>https://probehub.tistory.com/35</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;DALL&amp;amp;middot;E 2024-11-17 16.09.31 - An illustration symbolizing immutability in user-defined Python classes. Depict a sturdy, locked treasure chest with a glowing, immutable object (like.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ynJ9M/btsKMlSiePL/tApdTFOnaMQARwmaGraiBK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ynJ9M/btsKMlSiePL/tApdTFOnaMQARwmaGraiBK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ynJ9M/btsKMlSiePL/tApdTFOnaMQARwmaGraiBK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FynJ9M%2FbtsKMlSiePL%2FtApdTFOnaMQARwmaGraiBK%2Fimg.webp&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;350&quot; height=&quot;350&quot; data-filename=&quot;DALL&amp;middot;E 2024-11-17 16.09.31 - An illustration symbolizing immutability in user-defined Python classes. Depict a sturdy, locked treasure chest with a glowing, immutable object (like.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용자 지정 불변 객체&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1;&quot; href=&quot;https://probehub.tistory.com/34&quot;&gt;이전 글&lt;/a&gt;에서는 사용자가 만든 클래스는 모두 가변 객체인것 처럼 설명되었다. 정말 사용자가 만든 클래스는 불변 객체가 될 수 없는 걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;용자가 작성한 클래스를 불변 객체로 만들기 위해서는 다음의 조건이 충족되어야 할 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로운 attribute 를 추가하는것을 막는다.&lt;/li&gt;
&lt;li&gt;기존 attribute 를 변경하는 것 모두가 막는다.&lt;/li&gt;
&lt;li&gt;값을 기반으로 객체간 동등성을 비교하도록 재정의 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style2&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 새로운 attribute 추가 제한&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬 객체는 별다른 설정을 하지 않는다면 attribute 를 dict 자료형에 저장한다. 그리고 dict 자료형은 값을 추가, 삭제할 수 있다. 아래의 예시를 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자유로은 attribute 추가&lt;/p&gt;
&lt;pre id=&quot;code_1731826880008&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MutableClass:
    value: str

    def __init__(self, value: str):
        self.value = value

    def __repr__(self):
        # __dict__ 를 통해서 내부 attribute 에 접근
        attrs = &quot;, &quot;.join(f&quot;{key}: {value}&quot; for key, value in self.__dict__.items())
        return f&quot;MutableClass({attrs})&quot;


mutable = MutableClass(&quot;mutable&quot;)

print(f&quot;mutable: {mutable}&quot;)
# mutable: MutableClass(value: mutable)

mutable.value2 = &quot;mutable...&quot;

print(f&quot;mutable: {mutable}&quot;)
# mutable: MutableClass(value: mutable, value2: mutable...)&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;MutableClass 내부에 정의된 __repr__ 메서드에서 명시적으로 __dict__ 에서 값을 가져오는 것을 알 수 있다.&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;__slots__ 를 이용한 attribute 추가 제한&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스를 정의할때 __slots__ 를 통해 attribute&amp;nbsp; 의 이름값을 정의한다면, 새로운 attribute 를 추가할때 AttributeError 가 발생하게 되며, 이는 IDE 에 따라서는 적절한 경고를 띄워준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;__slots__ 를 이용하는 것은 객체의 사용방법을 제한하는 것 외에도 다른 장점이 있지만 이 글에서는 이 정도만 알아본다.&lt;/p&gt;
&lt;pre id=&quot;code_1731827137507&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MutableClass:
    value: str

    def __init__(self, value: str):
        self.value = value

    def __repr__(self):
        # __dict__ 를 통해서 내부 attribute 에 접근
        attrs = &quot;, &quot;.join(f&quot;{key}: {value}&quot; for key, value in self.__dict__.items())
        return f&quot;MutableClass({attrs})&quot;


class ImmutableClass1:
    __slots__ = [&quot;value&quot;]

    def __init__(self, value: str):
        self.value = value

    def __repr__(self):
        # __slots__ 를 통해서 내부 attribute 에 접근
        attrs = &quot;, &quot;.join(
            f&quot;{slot}: {getattr(self, slot, None)}&quot; for slot in self.__slots__
        )
        return f&quot;ImmutableClass1({attrs})&quot;


mutable = MutableClass(&quot;mutable&quot;)
immutable1 = ImmutableClass1(&quot;immutable1&quot;)

print(f&quot;mutable: {mutable}\nimmutable1: {immutable1}&quot;)
# mutable: MutableClass(value: mutable)
# immutable1: ImmutableClass1(value: immutable1)

mutable.value2 = &quot;mutable...&quot;
immutable1.value2 = &quot;immutable1...&quot; # AttributeError: 'ImmutableClass1' object has no attribute 'value2'&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;하지만 __slots__ 를 통해 새로운 attribute 를 추가하는 것을 막았다고 하더라도 이는 기존 attribute 의 불변성을 보장하는 것이 아니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 기존 attribute 변경 제한&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.1 __setattr__ 재정의&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬 클래스는 내부적으로 attribute의 값을 초기화하거나 변경할때 __setattr__ 메서드를 호출하게 되어있다. 따라서 처음에 객체를 초기화할때를 제외하는 __setattr__ 을 호출하지 못하게 하면 attribute 를 변경하지 못하는 불변 객체가 완성되는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1731827999020&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class ImmutableClass1:
    __slots__ = [&quot;value&quot;]

    def __init__(self, value: str):
        self.value = value

    def __repr__(self):
        # __slots__ 를 통해서 내부 attribute 에 접근
        attrs = &quot;, &quot;.join(
            f&quot;{slot}: {getattr(self, slot, None)}&quot; for slot in self.__slots__
        )
        return f&quot;ImmutableClass1({attrs})&quot;


class ImmutableClass2:
    __slots__ = [&quot;value&quot;]

    def __init__(self, value: str):
        # 속성을 직접 설정 (이 단계에서는 __setattr__ 호출 방지)
        super().__setattr__(&quot;value&quot;, value)

    def __setattr__(self, key, value):
        # 객체 초기화 후에는 속성 변경 금지
        raise AttributeError(f&quot;Cannot modify attribute '{key}'&quot;)

    def __repr__(self):
        # __slots__ 를 통해서 내부 attribute 에 접근
        attrs = &quot;, &quot;.join(
            f&quot;{slot}: {getattr(self, slot, None)}&quot; for slot in self.__slots__
        )
        return f&quot;ImmutableClass2({attrs})&quot;



immutable1 = ImmutableClass1(&quot;immutable1&quot;)
immutable2 = ImmutableClass2(&quot;immutable2&quot;)

print(f&quot;immutable1: {immutable1}\nimmutable2: {immutable2}&quot;)
# immutable1: ImmutableClass1(value: immutable1)
# immutable2: ImmutableClass2(value: immutable2)

immutable1.value = &quot;immutable1+1&quot;
immutable2.value = &quot;immutable2+2&quot; # AttributeError: Cannot modify attribute 'value'&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 두가지를 한번에&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;사실 위의 과정은 너무 복잡하고 boilerplate 가 많다. 사용자가 정의한 클래스를 불변 객체로 만들고자하는 욕구는 먼저 python 을 거쳐갔던 수많은 개발자들도 동일했을 것이므로 더 멋진 방법이 있을 것이라고 생각하는게 당연하다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.1 dataclass&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그 방법 중 하나가 datablasses 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 __slots__ 를 이용하지 않아도, __setattr__ 을 재정의 하지 않아도 기존에 기대하던 모든 것들을 충족시켜준다. IDE 에 따라서는 적절한 경고를 띄워준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(기본적으로 slot=False 로 설정되어 있는데, slot=True 로 설정하여 slots 을 사용하는 이점을 누릴 수 있다.)&lt;/p&gt;
&lt;pre id=&quot;code_1731828310122&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from dataclasses import dataclass


class ImmutableClass2:
    __slots__ = [&quot;value&quot;]

    def __init__(self, value: str):
        # 속성을 직접 설정 (이 단계에서는 __setattr__ 호출 방지)
        super().__setattr__(&quot;value&quot;, value)

    def __setattr__(self, key, value):
        # 객체 초기화 후에는 속성 변경 금지
        raise AttributeError(f&quot;Cannot modify attribute '{key}'&quot;)

    def __repr__(self):
        # __slots__ 를 통해서 내부 attribute 에 접근
        attrs = &quot;, &quot;.join(
            f&quot;{slot}: {getattr(self, slot, None)}&quot; for slot in self.__slots__
        )
        return f&quot;ImmutableClass2({attrs})&quot;


@dataclass(frozen=True)
class ImmutableClass3:
    value: str

    def __repr__(self):
        # __dict__ 를 통해서 내부 attribute 에 접근
        attrs = &quot;, &quot;.join(f&quot;{key}: {value}&quot; for key, value in self.__dict__.items())
        return f&quot;ImmutableClass3({attrs})&quot;


immutable2 = ImmutableClass2(&quot;immutable2&quot;)
immutable3 = ImmutableClass3(&quot;immutable3&quot;)

print(f&quot;immutable2: {immutable2}\nimmutable3: {immutable3}&quot;)
# immutable2: ImmutableClass2(value: immutable2)
# immutable3: ImmutableClass3(value: immutable3)

immutable3.value = &quot;immutable3+3&quot;  # dataclasses.FrozenInstanceError: cannot assign to field 'value'
immutable3.value2 = &quot;immutable3...&quot; # dataclasses.FrozenInstanceError: cannot assign to field 'value2'&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;3.2 NamedTuple&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NamedTuple 을 클래스가 상속하게 하는 방법도 있다. 이 방법 역시 새로운 attribute 의 추가와 기존 attribute 의 변경을 막는다. 이때는 기존 클래스의 __dict__ 나 __slots__ 를 이용하는 것이 아니라 tuple 구조로 바로 변경된다. tuple 자체가 불변 객체이므로 사용자가 정의한 객체에 불변성을 부여할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IDE 에 따라서는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;적절한 에러&lt;/span&gt;를 띄워준다.&lt;/p&gt;
&lt;pre id=&quot;code_1731828988871&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from dataclasses import dataclass
from typing import NamedTuple


@dataclass(frozen=True)
class ImmutableClass3:
    value: str

    def __repr__(self):
        # __dict__ 를 통해서 내부 attribute 에 접근
        attrs = &quot;, &quot;.join(f&quot;{key}: {value}&quot; for key, value in self.__dict__.items())
        return f&quot;ImmutableClass3({attrs})&quot;


class ImmutableClass4(NamedTuple):
    value: str

    def __repr__(self):
        # ._fields 를 통해 NamedTuple 필드에 접근
        attrs = &quot;, &quot;.join(f&quot;{field}: {getattr(self, field)}&quot; for field in self._fields)
        return f&quot;ImmutableClass4({attrs})&quot;


immutable3 = ImmutableClass3(&quot;immutable3&quot;)
immutable4 = ImmutableClass4(&quot;immutable4&quot;)

print(f&quot;immutable3: {immutable3}\nimmutable4: {immutable4}&quot;)
# immutable3: ImmutableClass3(value: immutable3)
# immutable4: ImmutableClass4(value: immutable4)

immutable4.value = &quot;immutable4+4&quot;  # AttributeError: can't set attribute
immutable4.value2 = &quot;immutable4...&quot;  # AttributeError: 'ImmutableClass4' object has no attribute 'value2'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 동등성 비교 재정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불변 객체의 동등성은 값 비교로 이루어지는게 타당할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 위의 예제 중 ImmutableClass2 에 __eq__ 메서드를 재정의할 수 있을 것이고, dataclass 와 NamedTuple 은 모두 값을 기준으로 __eq__ 비교를 하도록 재정의 해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1731829654019&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from dataclasses import dataclass
from typing import NamedTuple

class ImmutableClass2:
    __slots__ = [&quot;value&quot;]

    def __init__(self, value: str):
        # 속성을 직접 설정 (이 단계에서는 __setattr__ 호출 방지)
        super().__setattr__(&quot;value&quot;, value)

    def __setattr__(self, key, value):
        # 객체 초기화 후에는 속성 변경 금지
        raise AttributeError(f&quot;Cannot modify attribute '{key}'&quot;)

    def __repr__(self):
        # __slots__ 를 통해서 내부 attribute 에 접근
        attrs = &quot;, &quot;.join(
            f&quot;{slot}: {getattr(self, slot, None)}&quot; for slot in self.__slots__
        )
        return f&quot;ImmutableClass2({attrs})&quot;

    def __eq__(self, other):
        # 동등성 비교: 다른 객체와 값 비교
        if not isinstance(other, ImmutableClass2):
            return False
        return all(
            getattr(self, slot, None) == getattr(other, slot, None)
            for slot in self.__slots__
        )

@dataclass(frozen=True)
class ImmutableClass3:
    value: str

    def __repr__(self):
        # __dict__ 를 통해서 내부 attribute 에 접근
        attrs = &quot;, &quot;.join(f&quot;{key}: {value}&quot; for key, value in self.__dict__.items())
        return f&quot;ImmutableClass3({attrs})&quot;


class ImmutableClass4(NamedTuple):
    value: str

    def __repr__(self):
        # ._fields 를 통해 NamedTuple 필드에 접근
        attrs = &quot;, &quot;.join(f&quot;{field}: {getattr(self, field)}&quot; for field in self._fields)
        return f&quot;ImmutableClass4({attrs})&quot;


immutable2_1 = ImmutableClass2(&quot;immutable2&quot;)
immutable2_2 = ImmutableClass2(&quot;immutable2&quot;)

immutable3_1 = ImmutableClass3(&quot;immutable3&quot;)
immutable3_2 = ImmutableClass3(&quot;immutable3&quot;)

immutable4_1 = ImmutableClass4(&quot;immutable4&quot;)
immutable4_2 = ImmutableClass4(&quot;immutable4&quot;)

print(f&quot;&quot;&quot;immutable2: {immutable2_1 == immutable2_2}
immutable3: {immutable3_1 == immutable3_2}
immutable4: {immutable4_1 == immutable4_2}
&quot;&quot;&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 어떤걸 사용할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 지금까지 알아본 3가지 방법중 어떤것을 이용하는게 좋을까?&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;dataclass 를 이용한 불변 객체&lt;/li&gt;
&lt;li&gt;NamedTuple 을 이용한 불변 객체&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5.1 구현 복잡도&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 불변 객체를 재정의한 경우가 구현 복잡도가 가장 높았다. 그리고 dataclass 와 NamedTuple 의 경우 구현복잡도 에서 크게 차이가 있는것 같지는 않다, dataclass는 구현 복잡도가 아주 조금 더 높지만 그 덕분에 다양한 요구 사항에 따라 적절한 객체를 커스터마이징 하여 사용할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5.2 메모리 효율성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 복잡성은 너무나 명확한 문제이고, 메모리 효율성을 따져보자&lt;/p&gt;
&lt;pre id=&quot;code_1731830315097&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from dataclasses import dataclass
from typing import NamedTuple
import sys
from pympler import asizeof


class ImmutableClass2:
    __slots__ = [&quot;id&quot;, &quot;value&quot;]

    def __init__(self, id: int, value: str):
        super().__setattr__(&quot;id&quot;, id)
        super().__setattr__(&quot;value&quot;, value)

    def __setattr__(self, key, value):
        raise AttributeError(f&quot;Cannot modify attribute '{key}'&quot;)


@dataclass(frozen=True, slots=True)
class ImmutableClass3:
    id: int
    value: str


class ImmutableClass4(NamedTuple):
    id: int
    value: str


immutable2 = ImmutableClass2(10, &quot;immutable&quot;)
immutable3 = ImmutableClass3(10, &quot;immutable&quot;)
immutable4 = ImmutableClass4(10, &quot;immutable&quot;)

# 객체의 기본 메모리 크기만 측정
print(f&quot;self created size: {sys.getsizeof(immutable2)} bytes&quot;)
print(f&quot;dataclass size: {sys.getsizeof(immutable3)} bytes&quot;)
print(f&quot;NamedTuple size: {sys.getsizeof(immutable4)} bytes&quot;)
# self created size: 48 bytes
# dataclass size: 48 bytes
# NamedTuple size: 56 bytes

print(f&quot;======================&quot;)

# 객체와 객체가 참조하는 모든 메모리
print(f&quot;self created actual size: {asizeof.asizeof(immutable2)} bytes&quot;)
print(f&quot;dataclass actual size: {asizeof.asizeof(immutable3)} bytes&quot;)
print(f&quot;NamedTuple actual size: {asizeof.asizeof(immutable4)} bytes&quot;)
# self created actual size: 136 bytes
# dataclass actual size: 136 bytes
# NamedTuple actual size: 144 bytes&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;NamedTuple 방식이 8 byte 정도 메모리를 더 소모&lt;/span&gt;하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 NamedTuple 이 Tuple 의 기능에 더해&amp;nbsp; &quot;key&quot; 값으로 &quot;value&quot; 를 조회할 수 있는 추가적인 기능을 갖는 자료구조를 구현함으로 추가 메모리 공간을 소모하는 것으로 추측된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5.3 결론&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 확인한 사실을 상대적 비교로 도표화하면 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-17 오후 5.15.16.png&quot; data-origin-width=&quot;2254&quot; data-origin-height=&quot;2102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dzncUw/btsKK9ek9oi/11KoPhbTjRtg47jR93UO7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dzncUw/btsKK9ek9oi/11KoPhbTjRtg47jR93UO7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dzncUw/btsKK9ek9oi/11KoPhbTjRtg47jR93UO7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdzncUw%2FbtsKK9ek9oi%2F11KoPhbTjRtg47jR93UO7K%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;500&quot; height=&quot;466&quot; data-filename=&quot;스크린샷 2024-11-17 오후 5.15.16.png&quot; data-origin-width=&quot;2254&quot; data-origin-height=&quot;2102&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 항상 dataclass 를 사용하는게 이득이 아닐까? 반드시 그런것은 아니다. &lt;br /&gt;&lt;br /&gt;dataclass 는 사용자에 따라서 설정 값이 달라질 수 있으므려 협업시 예상치 못한 행위를 일으키지 않기 위해 옵션을 확인해야하는 불편함이 있곗지만 NamedTuple 은 별다른 옵션이 없으므로 예상치 못한 행위가 발생될 가능성이 적다. 그리고 tuple 의 기능을 이용할 수 있다는 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dataclass 를 이용하는 것은 직접 구현에 비해서 관련 클래스 메서드 오버라이딩 등 오버헤드가 있으므로 조금이지만 메모리 효율성에서 불리한 점이 있다. 따라서 엄청 대용량의 데이터를 객체로 매핑해서 다뤄야하는 경우에는 직접 구현하는 방법을 고려할 수 있겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>탐구 생활/Python</category>
      <category>python dataclass</category>
      <category>python namedtuple</category>
      <category>python 불변 객체</category>
      <author>개발프로브</author>
      <guid isPermaLink="true">https://probehub.tistory.com/35</guid>
      <comments>https://probehub.tistory.com/35#entry35comment</comments>
      <pubDate>Sun, 17 Nov 2024 16:09:55 +0900</pubDate>
    </item>
  </channel>
</rss>