<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Penguin Dev</title>
    <link>https://penguin-dev.tistory.com/</link>
    <description>penguin-dev 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Mon, 15 Jun 2026 00:42:31 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>KilPenguin</managingEditor>
    <image>
      <title>Penguin Dev</title>
      <url>https://tistory1.daumcdn.net/tistory/7330093/attach/c7dd719f485d4fe1aa87836cd8647951</url>
      <link>https://penguin-dev.tistory.com</link>
    </image>
    <item>
      <title>[가면사배 시리즈 #2] 개략적인 규모 추정 - 시스템 설계의 첫 번째 관문</title>
      <link>https://penguin-dev.tistory.com/41</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;934&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUDUGN/btsQIJs4tyX/f09HzlIWKxOpKFRwkcJIR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUDUGN/btsQIJs4tyX/f09HzlIWKxOpKFRwkcJIR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUDUGN/btsQIJs4tyX/f09HzlIWKxOpKFRwkcJIR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUDUGN%2FbtsQIJs4tyX%2Ff09HzlIWKxOpKFRwkcJIR1%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;1024&quot; height=&quot;934&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;934&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;가면사배 스터디 2주차! 1장에서 단일 서버부터 수백만 사용자까지의 시스템 진화 과정을 배웠다면, 이번 2장에서는 그런 시스템을 설계하기 전에 반드시 필요한 &lt;b&gt;개략적인 규모 추정(Back-of-the-envelope Estimation)&lt;/b&gt;에 대해 다룹니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &quot;봉투 뒷면 계산&quot;이라는 번역이 좀 어색했는데, 실제로 읽어보니 정말 핵심적인 내용이더라고요. 시스템 설계 면접에서도 자주 나오고, 실무에서도 &quot;이 정도 트래픽이면 서버가 몇 대나 필요할까?&quot; 같은 질문에 답할 수 있어야 하거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스터디 동기들과 함께 실제 서비스들을 예시로 들어가며 계산해보니, 이론으로만 알던 것들이 구체적인 숫자로 와닿더라고요. &quot;아, 유튜브가 이 정도 규모구나&quot;, &quot;우리 회사 서비스와 비교하면 이 정도 차이가 나는구나&quot; 하는 감각을 기를 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 개발자로서 &quot;감으로 때려맞추기&quot;가 아니라 &lt;b&gt;논리적이고 체계적으로 접근하는 방법&lt;/b&gt;을 배울 수 있어서 정말 유용했어요. 이번 포스트에서는 책에서 배운 핵심 개념들과 실무에 적용할 수 있는 팁들을 정리해보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개략적 규모 추정의 정의와 중요성&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;개략적 규모 추정(Back-of-the-envelope Estimation)&lt;/b&gt;은 복잡한 시스템의 규모를 간단한 계산을 통해 대략적으로 추정하는 기법입니다. 정확한 수치보다는 &lt;b&gt;올바른 사고 과정과 합리적인 가정&lt;/b&gt;을 통해 현실적인 범위 내의 답을 도출하는 것이 목표입니다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot; data-ke-language=&quot;delphi&quot;&gt;&lt;code&gt;graph TD
    A[문제 정의] --&amp;gt; B[가정 설정]
    B --&amp;gt; C[기본 수치 확인]
    C --&amp;gt; D[단계별 계산]
    D --&amp;gt; E[결과 검증]
    E --&amp;gt; F[합리성 판단]

    style A fill:#ff9800,color:#fff
    style B fill:#2196f3,color:#fff
    style C fill:#4caf50,color:#fff
    style D fill:#9c27b0,color:#fff
    style E fill:#f44336,color:#fff
    style F fill:#607d8b,color:#fff&lt;/code&gt;&lt;/pre&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;논리적 사고 과정을 평가하는 핵심 도구&lt;/li&gt;
&lt;li&gt;복잡한 문제를 단순화하는 능력 검증&lt;/li&gt;
&lt;li&gt;실무 경험과 기술적 감각 측정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;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;용량 계획(Capacity Planning) 수립&lt;/li&gt;
&lt;li&gt;인프라 비용 추정&lt;/li&gt;
&lt;li&gt;성능 병목 지점 예측&lt;/li&gt;
&lt;li&gt;아키텍처 의사결정 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 제가 경험한 상황들입니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;새 기능 출시 시 예상 트래픽 증가량은?&quot;&lt;/li&gt;
&lt;li&gt;&quot;데이터베이스 샤딩이 필요한 시점은 언제?&quot;&lt;/li&gt;
&lt;li&gt;&quot;CDN 도입의 비용 대비 효과는?&quot;&lt;/li&gt;
&lt;li&gt;&quot;캐시 서버 메모리 용량을 얼마나 할당해야 할까?&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 질문들에 &quot;감으로&quot; 답하는 것이 아니라 &lt;b&gt;체계적인 계산&lt;/b&gt;을 통해 근거 있는 답변을 할 수 있게 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;필수 기초 지식 - 2의 제곱수와 데이터 단위&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;기본 단위에 대한 정확한 이해&lt;/b&gt;입니다. 면접에서 &quot;1GB가 몇 바이트인지&quot; 매번 계산하면 시간만 낭비하죠.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단위&lt;/th&gt;
&lt;th&gt;2의 제곱수&lt;/th&gt;
&lt;th&gt;대략적인 값&lt;/th&gt;
&lt;th&gt;실제 값&lt;/th&gt;
&lt;th&gt;실무 예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1KB&lt;/td&gt;
&lt;td&gt;2^10&lt;/td&gt;
&lt;td&gt;~1천&lt;/td&gt;
&lt;td&gt;1,024&lt;/td&gt;
&lt;td&gt;짧은 텍스트 파일, JSON 응답&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1MB&lt;/td&gt;
&lt;td&gt;2^20&lt;/td&gt;
&lt;td&gt;~1백만&lt;/td&gt;
&lt;td&gt;1,048,576&lt;/td&gt;
&lt;td&gt;고화질 사진, 작은 동영상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1GB&lt;/td&gt;
&lt;td&gt;2^30&lt;/td&gt;
&lt;td&gt;~10억&lt;/td&gt;
&lt;td&gt;1,073,741,824&lt;/td&gt;
&lt;td&gt;영화 1편, 대용량 데이터베이스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1TB&lt;/td&gt;
&lt;td&gt;2^40&lt;/td&gt;
&lt;td&gt;~1조&lt;/td&gt;
&lt;td&gt;1,099,511,627,776&lt;/td&gt;
&lt;td&gt;서버 스토리지, 데이터 센터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1PB&lt;/td&gt;
&lt;td&gt;2^50&lt;/td&gt;
&lt;td&gt;~1천조&lt;/td&gt;
&lt;td&gt;1,125,899,906,842,624&lt;/td&gt;
&lt;td&gt;대규모 클라우드 스토리지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;사진 1장 = 1MB&lt;/li&gt;
&lt;li&gt;영화 1편 = 1GB&lt;/li&gt;
&lt;li&gt;개인 PC 하드디스크 = 1TB&lt;/li&gt;
&lt;li&gt;대기업 데이터 센터 = 1PB&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시간 단위와 계산 상수&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단위&lt;/th&gt;
&lt;th&gt;초(seconds)&lt;/th&gt;
&lt;th&gt;실무 활용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1일&lt;/td&gt;
&lt;td&gt;86,400&lt;/td&gt;
&lt;td&gt;일간 트래픽 계산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1주&lt;/td&gt;
&lt;td&gt;604,800&lt;/td&gt;
&lt;td&gt;주간 성장률 분석&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1개월&lt;/td&gt;
&lt;td&gt;2,592,000&lt;/td&gt;
&lt;td&gt;월간 사용자 추정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1년&lt;/td&gt;
&lt;td&gt;31,536,000&lt;/td&gt;
&lt;td&gt;연간 데이터 증가량&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;계산 팁&lt;/b&gt;: 하루는 대략 10^5초(100,000초)로 근사하면 계산이 쉬워집니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능 지표의 핵심 - 응답 지연시간(Latency)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제프 딘의 응답지연 수치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글의 제프 딘(Jeff Dean)이 정리한 &lt;b&gt;컴퓨터 시스템 응답지연 수치&lt;/b&gt;는 시스템 설계의 바이블과 같습니다. 이 수치들을 알고 있으면 &quot;캐시를 도입하면 얼마나 빨라질까?&quot;, &quot;네트워크 호출을 줄이면 성능이 얼마나 개선될까?&quot; 같은 질문에 구체적으로 답할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot; data-ke-language=&quot;delphi&quot;&gt;&lt;code&gt;graph TD
    A[CPU 캐시 접근&amp;lt;br/&amp;gt;0.5ns] --&amp;gt; B[메모리 접근&amp;lt;br/&amp;gt;100ns]
    B --&amp;gt; C[SSD 읽기&amp;lt;br/&amp;gt;16&amp;mu;s]
    C --&amp;gt; D[디스크 탐색&amp;lt;br/&amp;gt;2ms]
    D --&amp;gt; E[네트워크 패킷&amp;lt;br/&amp;gt;같은 DC: 0.5ms]
    E --&amp;gt; F[네트워크 패킷&amp;lt;br/&amp;gt;대륙간: 150ms]

    style A fill:#4caf50,color:#fff
    style B fill:#8bc34a,color:#fff
    style C fill:#ffeb3b,color:#000
    style D fill:#ff9800,color:#fff
    style E fill:#f44336,color:#fff
    style F fill:#9c27b0,color:#fff&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;작업&lt;/th&gt;
&lt;th&gt;응답시간&lt;/th&gt;
&lt;th&gt;상대적 비교&lt;/th&gt;
&lt;th&gt;실무 적용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU 캐시 접근&lt;/td&gt;
&lt;td&gt;0.5ns&lt;/td&gt;
&lt;td&gt;1배&lt;/td&gt;
&lt;td&gt;CPU 집약적 연산 최적화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메모리 접근&lt;/td&gt;
&lt;td&gt;100ns&lt;/td&gt;
&lt;td&gt;200배&lt;/td&gt;
&lt;td&gt;인메모리 캐시(Redis)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSD 읽기&lt;/td&gt;
&lt;td&gt;16&amp;mu;s&lt;/td&gt;
&lt;td&gt;32,000배&lt;/td&gt;
&lt;td&gt;데이터베이스 인덱스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;디스크 탐색&lt;/td&gt;
&lt;td&gt;2ms&lt;/td&gt;
&lt;td&gt;4,000,000배&lt;/td&gt;
&lt;td&gt;전통적인 하드디스크&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;네트워크(같은 DC)&lt;/td&gt;
&lt;td&gt;0.5ms&lt;/td&gt;
&lt;td&gt;1,000,000배&lt;/td&gt;
&lt;td&gt;마이크로서비스 통신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;네트워크(대륙간)&lt;/td&gt;
&lt;td&gt;150ms&lt;/td&gt;
&lt;td&gt;300,000,000배&lt;/td&gt;
&lt;td&gt;글로벌 API 호출&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;메모리 vs 디스크&lt;/b&gt;: 메모리가 디스크보다 20,000배 빠르다는 것은 단순한 숫자가 아닙니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// 디스크 기반 조회 (2ms)
const userFromDB = await database.query(&quot;SELECT * FROM users WHERE id = ?&quot;, [
  userId,
]);

// 메모리 캐시 조회 (0.0001ms = 100ns)
const userFromCache = await redis.get(`user:${userId}`);

// 성능 차이: 20,000배 향상 (2ms &amp;divide; 0.0001ms)&lt;/code&gt;&lt;/pre&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;같은 데이터센터 내 API 호출: 0.5ms&lt;/li&gt;
&lt;li&gt;해외 API 호출: 150ms (300배 차이!)&lt;/li&gt;
&lt;li&gt;CDN 활용의 중요성을 수치로 이해할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제 경험담&lt;/b&gt;:&lt;br /&gt;해외 결제 API를 호출하는 기능을 개발할 때, 150ms 지연 때문에 사용자 경험이 크게 저하됐었습니다. 이 수치를 알고 있었다면 처음부터 비동기 처리나 캐싱 전략을 고려했을 텐데요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;체계적 추정 방법론&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5단계 추정 프로세스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개략적 규모 추정은 다음과 같은 체계적인 과정을 따릅니다:&lt;/p&gt;
&lt;pre class=&quot;delphi&quot; data-ke-language=&quot;delphi&quot;&gt;&lt;code&gt;flowchart TD
    A[1단계: 문제 이해 및 범위 설정] --&amp;gt; B[2단계: 가정 설정]
    B --&amp;gt; C[3단계: 기본 계산]
    C --&amp;gt; D[4단계: 상세 계산]
    D --&amp;gt; E[5단계: 결과 검증 및 조정]

    A1[요구사항 명확화&amp;lt;br/&amp;gt;제약조건 파악] --&amp;gt; A
    B1[합리적 가정&amp;lt;br/&amp;gt;면접관과 확인] --&amp;gt; B
    C1[QPS, 저장소&amp;lt;br/&amp;gt;대역폭 계산] --&amp;gt; C
    D1[피크 시간 고려&amp;lt;br/&amp;gt;성장률 반영] --&amp;gt; D
    E1[상식적 검증&amp;lt;br/&amp;gt;현실성 확인] --&amp;gt; E

    style A fill:#2196f3,color:#fff
    style B fill:#4caf50,color:#fff
    style C fill:#ff9800,color:#fff
    style D fill:#9c27b0,color:#fff
    style E fill:#f44336,color:#fff&lt;/code&gt;&lt;/pre&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;QPS(초당 쿼리 수) 계산&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;평균 QPS = 일간 총 요청 수 &amp;divide; 86,400초
최대 QPS = 평균 QPS &amp;times; 피크 배수 (보통 2~10배)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;저장소 요구량 계산&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;일간 저장소 = 일간 데이터 생성량 &amp;times; 평균 데이터 크기
연간 저장소 = 일간 저장소 &amp;times; 365일 &amp;times; 복제 계수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대역폭 계산&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;fix&quot;&gt;&lt;code&gt;읽기 대역폭 = 평균 QPS &amp;times; 평균 응답 크기
쓰기 대역폭 = 쓰기 QPS &amp;times; 평균 요청 크기&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실전 예제: 트위터 규모 추정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계: 문제 이해 및 가정 설정&lt;/h3&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;월간 활성 사용자(MAU): 3억 명&lt;/li&gt;
&lt;li&gt;일간 활성 사용자 비율: 50% (DAU = 1.5억 명)&lt;/li&gt;
&lt;li&gt;사용자당 일간 트윗 수: 2개&lt;/li&gt;
&lt;li&gt;사용자당 일간 타임라인 조회: 5회&lt;/li&gt;
&lt;li&gt;미디어 포함 트윗 비율: 10%&lt;/li&gt;
&lt;li&gt;평균 트윗 크기: 280자 = 280바이트&lt;/li&gt;
&lt;li&gt;평균 미디어 크기: 1MB&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계: QPS 계산&lt;/h3&gt;
&lt;pre class=&quot;delphi&quot; data-ke-language=&quot;delphi&quot;&gt;&lt;code&gt;graph TD
    A[DAU: 1.5억 명] --&amp;gt; B[일간 트윗 생성]
    A --&amp;gt; C[일간 타임라인 조회]

    B --&amp;gt; D[3억 개 트윗/일]
    C --&amp;gt; E[7.5억 회 조회/일]

    D --&amp;gt; F[쓰기 QPS: 3,500]
    E --&amp;gt; G[읽기 QPS: 8,700]

    F --&amp;gt; H[피크 쓰기 QPS: 7,000]
    G --&amp;gt; I[피크 읽기 QPS: 17,400]

    style A fill:#2196f3,color:#fff
    style F fill:#4caf50,color:#fff
    style G fill:#ff9800,color:#fff
    style H fill:#f44336,color:#fff
    style I fill:#9c27b0,color:#fff&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상세 계산&lt;/b&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;트윗 생성 (쓰기)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일간 트윗: 1.5억 &amp;times; 2 = 3억 개&lt;/li&gt;
&lt;li&gt;평균 쓰기 QPS: 3억 &amp;divide; 86,400 = 3,472 &amp;asymp; 3,500&lt;/li&gt;
&lt;li&gt;피크 쓰기 QPS: 3,500 &amp;times; 2 = 7,000&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;일간 조회: 1.5억 &amp;times; 5 = 7.5억 회&lt;/li&gt;
&lt;li&gt;평균 읽기 QPS: 7.5억 &amp;divide; 86,400 = 8,681 &amp;asymp; 8,700&lt;/li&gt;
&lt;li&gt;피크 읽기 QPS: 8,700 &amp;times; 2 = 17,400&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계: 저장소 요구량 계산&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;텍스트 데이터&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// 일간 텍스트 저장소
const dailyTextStorage = 300_000_000 * 280; // 84GB/일
const yearlyTextStorage = dailyTextStorage * 365; // 약 30TB/년&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;미디어 데이터&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// 일간 미디어 저장소
const dailyMediaTweets = 300_000_000 * 0.1; // 3천만 개
const dailyMediaStorage = dailyMediaTweets * 1_000_000; // 30TB/일
const yearlyMediaStorage = dailyMediaStorage * 365; // 약 11PB/년&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;총 저장소 (5년 보관)&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;텍스트: 30TB &amp;times; 5 = 150TB&lt;/li&gt;
&lt;li&gt;미디어: 11PB &amp;times; 5 = 55PB&lt;/li&gt;
&lt;li&gt;&lt;b&gt;총합: 약 55PB&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4단계: 대역폭 계산&lt;/h3&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;평균 응답 크기: 1KB (트윗 메타데이터)&lt;/li&gt;
&lt;li&gt;읽기 대역폭: 8,700 QPS &amp;times; 1KB = 8.7MB/s&lt;/li&gt;
&lt;/ul&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;평균 요청 크기: 300바이트&lt;/li&gt;
&lt;li&gt;쓰기 대역폭: 3,500 QPS &amp;times; 300바이트 = 1.05MB/s&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5단계: 결과 검증&lt;/h3&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;55PB는 개인 PC 하드디스크(1TB) 55,000개 분량&lt;/li&gt;
&lt;li&gt;피크 QPS 17,400은 초당 17,400명이 동시 접속하는 수준&lt;/li&gt;
&lt;li&gt;실제 트위터의 공개된 수치와 비교해보면 합리적인 범위&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실무 인사이트&lt;/b&gt;:&lt;br /&gt;이 계산을 통해 &quot;왜 트위터가 복잡한 분산 시스템을 구축해야 하는지&quot; 이해할 수 있습니다. 단일 서버로는 절대 처리할 수 없는 규모죠.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무 적용 전략과 고급 기법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파레토 법칙(80/20 법칙) 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 원리&lt;/b&gt;: 전체 사용자의 20%가 80%의 트래픽을 생성한다는 경험적 법칙&lt;/p&gt;
&lt;pre class=&quot;delphi&quot; data-ke-language=&quot;delphi&quot;&gt;&lt;code&gt;pie title 트래픽 분포 (파레토 법칙)
    &quot;파워 유저 20%&quot; : 80
    &quot;일반 유저 80%&quot; : 20&lt;/code&gt;&lt;/pre&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;상위 20% 인기 콘텐츠만 캐시 &amp;rarr; 80% 성능 향상 달성&lt;/li&gt;
&lt;li&gt;캐시 메모리 효율성: 4배 향상 (0.8 &amp;divide; 0.2)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제 경험&lt;/b&gt;:&lt;br /&gt;우리 서비스에서 전체 API 중 상위 20%만 캐싱했는데, 전체 응답 시간이 70% 개선됐습니다. 파레토 법칙이 실제로 적용되는 걸 확인할 수 있었어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;계산 단순화 기법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;반올림 전략&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;복잡한 수치는 10의 배수로 반올림&lt;/li&gt;
&lt;li&gt;99,987 &amp;divide; 9.1 &amp;rarr; 100,000 &amp;divide; 10 = 10,000&lt;/li&gt;
&lt;li&gt;정확성보다는 &lt;b&gt;사고 과정의 명확성&lt;/b&gt;이 중요&lt;/li&gt;
&lt;/ul&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;하루 = 86,400초 &amp;rarr; 100,000초(10^5)로 근사&lt;/li&gt;
&lt;li&gt;오차 약 15%이지만 계산이 훨씬 간단&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상식적 검증 체크리스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계산 결과의 합리성을 검증하는 방법:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;검증 항목&lt;/th&gt;
&lt;th&gt;체크 포인트&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;규모 비교&lt;/td&gt;
&lt;td&gt;알려진 서비스와 비교&lt;/td&gt;
&lt;td&gt;&quot;페이스북보다 10배 큰데 서버가 더 적다?&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;물리적 한계&lt;/td&gt;
&lt;td&gt;하드웨어 제약 확인&lt;/td&gt;
&lt;td&gt;&quot;1초에 백만 건 처리? 현실적인가?&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비용 합리성&lt;/td&gt;
&lt;td&gt;경제적 타당성&lt;/td&gt;
&lt;td&gt;&quot;연간 인프라 비용이 매출을 초과?&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;성장률 검증&lt;/td&gt;
&lt;td&gt;지속 가능성&lt;/td&gt;
&lt;td&gt;&quot;매년 10배 성장이 가능한가?&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보수적: 사용자 증가 1.5배, 사용량 증가 1.2배&lt;/li&gt;
&lt;li&gt;현실적: 사용자 증가 2.0배, 사용량 증가 1.5배&lt;/li&gt;
&lt;li&gt;낙관적: 사용자 증가 3.0배, 사용량 증가 2.0배&lt;/li&gt;
&lt;/ul&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;: QPS, 응답시간&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;h2 data-ke-size=&quot;size26&quot;&gt;시스템 설계 면접 전략&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;면접에서의 체계적 접근법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접관이 &quot;대규모 이미지 서비스의 저장소 요구량을 추정해보세요&quot;라고 물어봤을 때의 모범 답안 과정:&lt;/p&gt;
&lt;pre class=&quot;delphi&quot; data-ke-language=&quot;delphi&quot;&gt;&lt;code&gt;sequenceDiagram
    participant I as 면접관
    participant C as 지원자

    I-&amp;gt;&amp;gt;C: 문제 제시
    C-&amp;gt;&amp;gt;I: 1. 요구사항 명확화 질문
    C-&amp;gt;&amp;gt;I: 2. 가정 설정 및 확인
    C-&amp;gt;&amp;gt;I: 3. 단계별 계산 과정
    C-&amp;gt;&amp;gt;I: 4. 결과 검증 및 조정
    C-&amp;gt;&amp;gt;I: 5. 추가 고려사항 제시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단계별 실제 대화 예시&lt;/b&gt;:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1단계 - 요구사항 명확화&lt;/b&gt;:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이미지 서비스의 범위를 명확히 하고 싶습니다. 소셜 미디어 형태인가요, 아니면 클라우드 스토리지 형태인가요? 그리고 글로벌 서비스인지 특정 지역 서비스인지도 확인하고 싶습니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2단계 - 가정 설정&lt;/b&gt;:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;다음과 같이 가정하겠습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MAU 10억 명 (인스타그램 수준)&lt;/li&gt;
&lt;li&gt;일간 활성 사용자 비율 30% (DAU 3억 명)&lt;/li&gt;
&lt;li&gt;사용자당 일간 이미지 업로드 1장&lt;/li&gt;
&lt;li&gt;평균 이미지 크기 2MB&lt;/li&gt;
&lt;li&gt;데이터 보관 기간 5년&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3단계 - 계산 과정&lt;/b&gt;:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;단계별로 계산해보겠습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일간 업로드: 3억 명 &amp;times; 1장 = 3억 장&lt;/li&gt;
&lt;li&gt;일간 저장소: 3억 장 &amp;times; 2MB = 600GB&lt;/li&gt;
&lt;li&gt;연간 저장소: 600GB &amp;times; 365일 &amp;times; 3배 복제 = 650TB&lt;/li&gt;
&lt;li&gt;5년 총 저장소: 650TB &amp;times; 5년 = 3.25PB&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4단계 - 결과 검증&lt;/b&gt;:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;3.25PB라는 수치가 합리적인지 검증해보겠습니다. 이는 개인 PC 하드디스크 3,250개 분량으로, 10억 사용자 서비스 규모로는 현실적인 수치라고 판단됩니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5단계 - 추가 고려사항&lt;/b&gt;:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;실제 구현 시에는 다음 사항들도 고려해야 합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지 압축 및 최적화로 30-50% 용량 절약 가능&lt;/li&gt;
&lt;li&gt;CDN 캐싱으로 원본 저장소 부하 분산&lt;/li&gt;
&lt;li&gt;콜드 스토리지로 비용 최적화&lt;/li&gt;
&lt;li&gt;지역별 데이터 센터 분산&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&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;DO (해야 할 것)&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가정을 명확히 설정하고 면접관과 확인&lt;/li&gt;
&lt;li&gt;계산 과정을 단계별로 설명&lt;/li&gt;
&lt;li&gt;중간중간 상식적 검증 수행&lt;/li&gt;
&lt;li&gt;결과에 대한 추가 고려사항 제시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DON'T (하지 말아야 할 것)&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;침묵 속에서 혼자 계산&lt;/li&gt;
&lt;li&gt;복잡한 수식이나 정확한 계산에 매몰&lt;/li&gt;
&lt;li&gt;가정 없이 바로 계산 시작&lt;/li&gt;
&lt;li&gt;결과에 대한 검증 없이 끝내기&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트레이드오프와 고려사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정확성 vs 단순성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개략적 규모 추정에서 가장 중요한 트레이드오프는 &lt;b&gt;정확성과 단순성&lt;/b&gt; 사이의 균형입니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;접근법&lt;/th&gt;
&lt;th&gt;장점&lt;/th&gt;
&lt;th&gt;단점&lt;/th&gt;
&lt;th&gt;적용 상황&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&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;/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;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;1. 단위 혼동 함정&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;❌ &lt;code&gt;storage = 1000000 * 1000&lt;/code&gt; (단위 불분명)&lt;/li&gt;
&lt;li&gt;✅ &lt;code&gt;1,000,000장 &amp;times; 1MB = 1,000GB&lt;/code&gt; (단위 명시)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 비현실적 수치 함정&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;1초에 1억 건 처리&quot; &amp;rarr; 물리적으로 불가능&lt;/li&gt;
&lt;li&gt;&quot;개인 서비스가 페이스북보다 큰 트래픽&quot; &amp;rarr; 상식적으로 불합리&lt;/li&gt;
&lt;li&gt;&quot;무제한 성장률&quot; &amp;rarr; 경제적으로 지속 불가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 가정 설명 누락 함정&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;❌ &lt;code&gt;result = 1000000 * 2 * 365&lt;/code&gt; (가정 불분명)&lt;/li&gt;
&lt;li&gt;✅ &quot;DAU 100만 명, 사용자당 일 2개 포스트, 365일 기준으로 계산&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;불확실성 관리 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시나리오 기반 접근&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;delphi&quot; data-ke-language=&quot;delphi&quot;&gt;&lt;code&gt;graph TD
    A[기본 추정] --&amp;gt; B[보수적 시나리오&amp;lt;br/&amp;gt;-50%]
    A --&amp;gt; C[현실적 시나리오&amp;lt;br/&amp;gt;기본값]
    A --&amp;gt; D[낙관적 시나리오&amp;lt;br/&amp;gt;+100%]

    B --&amp;gt; E[최소 요구사항]
    C --&amp;gt; F[권장 요구사항]
    D --&amp;gt; G[최대 요구사항]

    style A fill:#2196f3,color:#fff
    style B fill:#4caf50,color:#fff
    style C fill:#ff9800,color:#fff
    style D fill:#f44336,color:#fff&lt;/code&gt;&lt;/pre&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;/li&gt;
&lt;li&gt;최대값: 낙관적 가정 기반&lt;/li&gt;
&lt;li&gt;권장값: 현실적 가정 기반&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무에서의 검증 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;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;트위터: MAU 3.3억, QPS 18,000 (비율: 54 QPS/백만 사용자)&lt;/li&gt;
&lt;li&gt;인스타그램: MAU 10억, QPS 50,000 (비율: 50 QPS/백만 사용자)&lt;/li&gt;
&lt;li&gt;페이스북: MAU 28억, QPS 100,000 (비율: 36 QPS/백만 사용자)&lt;/li&gt;
&lt;li&gt;우리 서비스 추정치가 이 범위 내에 있는지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;A/B 테스트를 통한 검증&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소규모 사용자 그룹으로 실제 측정&lt;/li&gt;
&lt;li&gt;추정값과 실측값 비교 분석&lt;/li&gt;
&lt;li&gt;오차 패턴 파악 및 모델 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무에 적용할 수 있는 인사이트들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 용량 계획(Capacity Planning)의 체계화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기존 방식&lt;/b&gt;: &quot;서버가 느려지면 그때 증설하자&quot;&lt;br /&gt;&lt;b&gt;개선된 방식&lt;/b&gt;: 데이터 기반 사전 계획&lt;/p&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;현재 지표: QPS 1,000, 저장소 100GB, 사용자 5만 명&lt;/li&gt;
&lt;li&gt;성장률 예측: 사용자 월 20% 증가, 사용량 월 10% 증가&lt;/li&gt;
&lt;li&gt;확장 임계값: CPU 70%, 저장소 80% 도달 시 확장&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;&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;컴퓨팅 비용: QPS 기반 서버 수 계산&lt;/li&gt;
&lt;li&gt;스토리지 비용: 데이터 증가율 기반 용량 계획&lt;/li&gt;
&lt;li&gt;네트워크 비용: 대역폭 사용량 추정&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 아키텍처 의사결정 지원&lt;/h3&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;읽기 QPS: 8,000&lt;/li&gt;
&lt;li&gt;캐시 히트율: 80% 예상&lt;/li&gt;
&lt;li&gt;메모리 지연: 0.1ms vs DB 지연: 10ms&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 개선 효과&lt;/b&gt;: 79.2% 향상 ((10-0.1)/10 &amp;times; 0.8)&lt;/li&gt;
&lt;/ul&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;QPS 임계값: 추정 최대 QPS의 80%&lt;/li&gt;
&lt;li&gt;저장소 임계값: 예상 증가율 기반 알림 설정&lt;/li&gt;
&lt;li&gt;응답시간 임계값: 성능 지연시간 기반 SLA 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;책에서 배운 핵심 원칙들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추정의 3대 원칙&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&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;/ol&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대략적 규모 추정으로 기술 스택 선택&lt;/li&gt;
&lt;li&gt;예상 비용 범위 산정&lt;/li&gt;
&lt;li&gt;개발 일정 계획 수립&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;운영 단계&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성능 병목 지점 예측&lt;/li&gt;
&lt;li&gt;확장 시점 계획&lt;/li&gt;
&lt;li&gt;장애 대응 시나리오 준비&lt;/li&gt;
&lt;/ul&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;/li&gt;
&lt;li&gt;계산 과정 설명 능력 향상&lt;/li&gt;
&lt;li&gt;트레이드오프 분석 역량 개발&lt;/li&gt;
&lt;/ul&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;숫자를 맞추는 게임이 아니라 시스템적 사고를 기르는 도구&lt;/b&gt;라는 걸 깨달았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 &quot;감으로 때려맞추기&quot;에서 벗어나 &lt;b&gt;데이터와 논리에 기반한 의사결정&lt;/b&gt;을 할 수 있게 되었고, 팀원들과의 기술 토론에서도 훨씬 설득력 있는 근거를 제시할 수 있게 됐어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 인상 깊었던 부분은 &lt;b&gt;트레이드오프의 명시적 고려&lt;/b&gt;였습니다. &quot;정확성 vs 단순성&quot;, &quot;비용 vs 성능&quot; 같은 선택의 순간에서 각각의 장단점을 수치로 비교할 수 있다는 것이 정말 유용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 포스트에서는 3장 &quot;시스템 설계 면접 공략법&quot;을 다룰 예정입니다. 1-2장에서 배운 이론적 기반을 실제 면접에서 어떻게 활용하는지 구체적인 전략을 정리해보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  규모 추정 실무 적용에 대한 질문&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러분의 서비스에서 가장 중요한 추정 지표는 무엇인가요?&lt;/li&gt;
&lt;li&gt;추정과 실제 결과가 크게 달랐던 경험이 있으신가요?&lt;/li&gt;
&lt;li&gt;용량 계획 수립 시 어떤 방법을 사용하고 계신가요?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  계산 방법론에 대한 질문&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;QPS 추정 시 피크 시간을 몇 배로 계산하시나요?&lt;/li&gt;
&lt;li&gt;데이터 증가율을 어떻게 예측하고 계신가요?&lt;/li&gt;
&lt;li&gt;클라우드 비용 추정에서 놓치기 쉬운 항목은 무엇인가요?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  면접 준비에 대한 질문&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템 설계 면접에서 가장 어려웠던 추정 문제는 무엇이었나요?&lt;/li&gt;
&lt;li&gt;계산 실수를 줄이는 나만의 팁이 있나요?&lt;/li&gt;
&lt;li&gt;면접관과의 소통에서 중요하게 생각하는 포인트는?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  도구와 방법에 대한 질문&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추정 정확도를 높이기 위해 사용하는 도구나 방법이 있나요?&lt;/li&gt;
&lt;li&gt;벤치마킹할 때 참고하는 서비스나 자료는 무엇인가요?&lt;/li&gt;
&lt;li&gt;팀 내에서 추정 결과를 공유하고 검증하는 프로세스가 있나요?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글로 경험을 공유해주시면 함께 배워나갈 수 있을 것 같습니다!&lt;/p&gt;</description>
      <category>  Career &amp;amp; Growth/  Learning Journey</category>
      <author>KilPenguin</author>
      <guid isPermaLink="true">https://penguin-dev.tistory.com/41</guid>
      <comments>https://penguin-dev.tistory.com/41#entry41comment</comments>
      <pubDate>Mon, 22 Sep 2025 21:00:00 +0900</pubDate>
    </item>
    <item>
      <title>[가면사배 시리즈 #1] 사용자 수에 따른 규모 확장성 - 단일 서버에서 수백만 사용자까지</title>
      <link>https://penguin-dev.tistory.com/40</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_ypi3lcypi3lcypi3.png&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;2048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xd4Yy/btsQhQsS9eU/gdd14EfMNj8tJTEtgWOrTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xd4Yy/btsQhQsS9eU/gdd14EfMNj8tJTEtgWOrTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xd4Yy/btsQhQsS9eU/gdd14EfMNj8tJTEtgWOrTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxd4Yy%2FbtsQhQsS9eU%2Fgdd14EfMNj8tJTEtgWOrTK%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;2048&quot; height=&quot;2048&quot; data-filename=&quot;Gemini_Generated_Image_ypi3lcypi3lcypi3.png&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;2048&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;항해 플러스 동기들과 함께 &quot;가상 면접 사례로 배우는 대규모 시스템 설계 기초(가면사배)&quot; 독서 스터디를 시작했습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 각 장마다 학습한 내용을 정리해서 공유할 예정인데, 첫 번째로 1장 &quot;사용자 수에 따른 규모 확장성&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;독서 스터디 동기들과 함께 토론하면서 더 깊이 이해할 수 있었고, 혼자만 알고 있기 아까워서 핵심 내용들을 정리해서 공유해보려고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;책에서 제시하는 11단계 시스템 진화 과정&lt;/h2&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;pre class=&quot;delphi&quot; data-ke-language=&quot;delphi&quot;&gt;&lt;code&gt;graph TD
    A[사용자] --&amp;gt;|1. DNS 조회| B[DNS 서버]
    B --&amp;gt;|2. IP 주소 반환| A
    A --&amp;gt;|3. HTTP 요청| C[단일 서버 - 15.125.23.214]

    subgraph &quot;단일 서버 내부&quot;
        D[웹 애플리케이션&amp;lt;br/&amp;gt;- 비즈니스 로직&amp;lt;br/&amp;gt;- API 처리]
        E[데이터베이스&amp;lt;br/&amp;gt;- 사용자 데이터&amp;lt;br/&amp;gt;- 애플리케이션 데이터]
        F[캐시&amp;lt;br/&amp;gt;- 메모리 캐시&amp;lt;br/&amp;gt;- 세션 데이터]

        D &amp;lt;--&amp;gt; E
        D &amp;lt;--&amp;gt; F
    end

    C --&amp;gt; D
    C --&amp;gt;|4. HTML/JSON 응답| A&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제 구현 예시&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;// 모든 것이 하나의 서버에서 실행
const express = require(&quot;express&quot;);
const app = express();

// 메모리 캐시 (단순한 Map 객체)
const cache = new Map();

// 데이터베이스 연결 (같은 서버)
const db = require(&quot;./database&quot;); // localhost 연결

app.get(&quot;/api/users/:id&quot;, async (req, res) =&amp;gt; {
  // 캐시 확인
  if (cache.has(req.params.id)) {
    return res.json(cache.get(req.params.id));
  }

  // DB 조회
  const user = await db.query(&quot;SELECT * FROM users WHERE id = ?&quot;, [
    req.params.id,
  ]);
  cache.set(req.params.id, user);

  res.json(user);
});

app.listen(3000); // 모든 것이 포트 3000에서 실행&lt;/code&gt;&lt;/pre&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;/li&gt;
&lt;li&gt;소규모 트래픽에는 충분&lt;/li&gt;
&lt;li&gt;개발과 배포가 단순함&lt;/li&gt;
&lt;li&gt;네트워크 지연이 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;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;단일 장애 지점(SPOF) 문제&lt;/li&gt;
&lt;li&gt;확장성 제한 (CPU, 메모리, 디스크 한계)&lt;/li&gt;
&lt;li&gt;성능 병목 (모든 요청이 하나의 서버로)&lt;/li&gt;
&lt;li&gt;컴포넌트 간 리소스 경합&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분을 읽으면서 &quot;아, 우리도 처음엔 이랬지&quot;라는 생각이 들었습니다. 모든 서비스가 이런 단순한 구조에서 시작하는 게 자연스러운 것 같아요. 실제로 스타트업이나 개인 프로젝트에서는 이런 구조로 시작해서 점진적으로 확장해나가는 게 일반적이죠.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계: 데이터베이스 분리 - 첫 번째 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계는 웹 서버와 데이터베이스를 분리하는 것입니다. 각각 독립적으로 확장할 수 있게 되죠.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot; data-ke-language=&quot;delphi&quot;&gt;&lt;code&gt;graph TD
    A[사용자] --&amp;gt;|HTTP 요청| B[웹 서버]
    B --&amp;gt;|SQL 쿼리| C[데이터베이스 서버]
    C --&amp;gt;|데이터 반환| B
    B --&amp;gt;|HTML/JSON 응답| A

    style B fill:#e1f5fe
    style C fill:#f3e5f5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책에서 흥미로웠던 부분은 데이터베이스 선택 기준을 표로 정리해놓은 것이었습니다:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;관계형 데이터베이스&lt;/th&gt;
&lt;th&gt;비관계형 데이터베이스&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;복잡한 쿼리와 트랜잭션&lt;/td&gt;
&lt;td&gt;낮은 응답 지연시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACID 속성 필요&lt;/td&gt;
&lt;td&gt;비정형 데이터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;성숙한 생태계&lt;/td&gt;
&lt;td&gt;대용량 데이터&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 &quot;언제 NoSQL을 써야 할까?&quot;라고 고민했었는데, 이런 기준이 명확하게 정리되어 있어서 좋았습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계: 로드밸런서 도입 - 가용성의 시작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드밸런서를 도입해서 여러 웹 서버로 트래픽을 분산시키는 단계입니다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot; data-ke-language=&quot;delphi&quot;&gt;&lt;code&gt;graph TD
    A[사용자] --&amp;gt;|HTTP 요청| B[로드밸런서&amp;lt;br/&amp;gt;공개 IP]
    B --&amp;gt;|트래픽 분산| C[웹 서버 1&amp;lt;br/&amp;gt;사설 IP]
    B --&amp;gt;|트래픽 분산| D[웹 서버 2&amp;lt;br/&amp;gt;사설 IP]
    C --&amp;gt;|SQL 쿼리| E[데이터베이스]
    D --&amp;gt;|SQL 쿼리| E
    E --&amp;gt;|데이터 반환| C
    E --&amp;gt;|데이터 반환| D
    C --&amp;gt;|응답| B
    D --&amp;gt;|응답| B
    B --&amp;gt;|HTML/JSON 응답| A

    style B fill:#ffeb3b
    style C fill:#e1f5fe
    style D fill:#e1f5fe
    style E fill:#f3e5f5&lt;/code&gt;&lt;/pre&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;공개 IP는 로드밸런서만 보유&lt;/li&gt;
&lt;li&gt;웹 서버들은 사설 IP로 통신&lt;/li&gt;
&lt;li&gt;장애 시 자동 복구 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책에서 강조한 건 &quot;가용성 향상&quot;이었습니다. 한 서버가 죽어도 서비스가 계속 돌아간다는 것, 정말 중요한 개념이죠.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4단계: 데이터베이스 다중화 - 읽기 성능 개선&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주(Master) 데이터베이스와 부(Slave) 데이터베이스로 나누는 단계입니다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot; data-ke-language=&quot;delphi&quot;&gt;&lt;code&gt;graph TD
    A[사용자] --&amp;gt;|HTTP 요청| B[로드밸런서]
    B --&amp;gt; C[웹 서버 1]
    B --&amp;gt; D[웹 서버 2]

    C --&amp;gt;|쓰기 연산| E[주 데이터베이스&amp;lt;br/&amp;gt;Master]
    D --&amp;gt;|쓰기 연산| E
    C --&amp;gt;|읽기 연산| F[부 데이터베이스&amp;lt;br/&amp;gt;Slave 1]
    C --&amp;gt;|읽기 연산| G[부 데이터베이스&amp;lt;br/&amp;gt;Slave 2]
    D --&amp;gt;|읽기 연산| F
    D --&amp;gt;|읽기 연산| G

    E -.-&amp;gt;|데이터 복제| F
    E -.-&amp;gt;|데이터 복제| G

    style E fill:#ff5722,color:#fff
    style F fill:#4caf50,color:#fff
    style G fill:#4caf50,color:#fff&lt;/code&gt;&lt;/pre&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;/li&gt;
&lt;li&gt;부 데이터베이스: 읽기 연산&lt;/li&gt;
&lt;li&gt;데이터 복제를 통한 동기화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;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;이 부분에서 &quot;아, 실제 운영에서는 이런 시나리오들을 다 고려해야 하는구나&quot;라는 걸 깨달았습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5단계: 캐시 계층 - 성능의 핵심&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 부분이 개인적으로 가장 흥미로웠습니다. 책에서 제시한 &lt;b&gt;캐시 우선 읽기(Cache-Aside)&lt;/b&gt; 패턴:&lt;/p&gt;
&lt;pre class=&quot;delphi&quot; data-ke-language=&quot;delphi&quot;&gt;&lt;code&gt;sequenceDiagram
    participant U as 사용자
    participant W as 웹 서버
    participant C as 캐시
    participant D as 데이터베이스

    U-&amp;gt;&amp;gt;W: 사용자 데이터 요청
    W-&amp;gt;&amp;gt;C: 캐시에서 데이터 확인
    alt 캐시 히트
        C-&amp;gt;&amp;gt;W: 캐시된 데이터 반환
        W-&amp;gt;&amp;gt;U: 응답 (빠름)
    else 캐시 미스
        C-&amp;gt;&amp;gt;W: 데이터 없음
        W-&amp;gt;&amp;gt;D: 데이터베이스 조회
        D-&amp;gt;&amp;gt;W: 데이터 반환
        W-&amp;gt;&amp;gt;C: 캐시에 데이터 저장
        W-&amp;gt;&amp;gt;U: 응답 (느림)
    end&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function getUser(userId) {
  // 1. 캐시에서 확인
  const user = await cache.get(`user:${userId}`);
  if (user) {
    return JSON.parse(user);
  }

  // 2. 캐시 미스 시 DB 조회
  const userData = await database.query(&quot;SELECT * FROM users WHERE id = ?&quot;, [
    userId,
  ]);

  // 3. 캐시에 저장 (1시간 TTL)
  await cache.setex(`user:${userId}`, 3600, JSON.stringify(userData));
  return userData;
}&lt;/code&gt;&lt;/pre&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;/li&gt;
&lt;li&gt;데이터 일관성: 원본과 캐시 간 동기화&lt;/li&gt;
&lt;li&gt;만료 정책: TTL 설정&lt;/li&gt;
&lt;li&gt;장애 대응: 캐시 서버 다중화&lt;/li&gt;
&lt;li&gt;데이터 방출 정책: LRU, LFU, FIFO&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 세부사항들까지 고려해야 한다는 걸 보고 &quot;캐시가 단순해 보이지만 실제로는 복잡하구나&quot;라고 생각했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6단계: CDN - 정적 콘텐츠 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CDN(콘텐츠 전송 네트워크) 도입 단계입니다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot; data-ke-language=&quot;delphi&quot;&gt;&lt;code&gt;graph TD
    A[사용자 - 서울] --&amp;gt;|정적 콘텐츠 요청| B[CDN 서울]
    C[사용자 - 도쿄] --&amp;gt;|정적 콘텐츠 요청| D[CDN 도쿄]
    E[사용자 - 뉴욕] --&amp;gt;|정적 콘텐츠 요청| F[CDN 뉴욕]

    B -.-&amp;gt;|캐시 미스 시| G[원본 서버]
    D -.-&amp;gt;|캐시 미스 시| G
    F -.-&amp;gt;|캐시 미스 시| G

    G -.-&amp;gt;|콘텐츠 전송| B
    G -.-&amp;gt;|콘텐츠 전송| D
    G -.-&amp;gt;|콘텐츠 전송| F

    style B fill:#2196f3,color:#fff
    style D fill:#2196f3,color:#fff
    style F fill:#2196f3,color:#fff
    style G fill:#ff9800,color:#fff&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동작 과정&lt;/b&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;/li&gt;
&lt;li&gt;CDN에 캐시된 콘텐츠가 있으면 반환&lt;/li&gt;
&lt;li&gt;없으면 원본 서버에서 가져와 캐시 후 반환&lt;/li&gt;
&lt;/ol&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;/li&gt;
&lt;li&gt;적절한 TTL 설정&lt;/li&gt;
&lt;li&gt;CDN 장애 대응 방안&lt;/li&gt;
&lt;li&gt;콘텐츠 무효화 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &quot;비용&quot; 부분이 현실적이었습니다. 자주 사용되지 않는 콘텐츠를 캐싱하는 건 이득이 크지 않다는 점, 실무에서 정말 중요한 고려사항이죠.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7단계: 무상태 웹 계층 - 확장성의 핵심&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 가장 중요하다고 생각한 부분입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상태 정보 의존적 vs 무상태&lt;/b&gt; 비교:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상태 정보 의존적&lt;/th&gt;
&lt;th&gt;무상태&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;서버가 클라이언트 상태 보관&lt;/td&gt;
&lt;td&gt;상태 정보를 외부 저장소에 보관&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;특정 서버로만 요청 전달 필요&lt;/td&gt;
&lt;td&gt;어떤 서버로든 요청 전달 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;확장성 제한&lt;/td&gt;
&lt;td&gt;자동 확장 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;세션 데이터를 Redis, Memcached 등 외부 저장소로 이동&lt;/li&gt;
&lt;li&gt;데이터베이스에 사용자 상태 저장&lt;/li&gt;
&lt;li&gt;NoSQL 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 개념을 이해하고 나니 &quot;왜 세션을 외부 저장소에 두라고 하는지&quot; 명확해졌습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8단계: 데이터 센터 - 글로벌 서비스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지리적 라우팅(GeoDNS)을 통해 사용자를 가장 가까운 데이터 센터로 라우팅하는 단계입니다.&lt;/p&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;트래픽 우회: GeoDNS를 통한 효과적인 라우팅&lt;/li&gt;
&lt;li&gt;데이터 동기화: 데이터 센터 간 데이터 일관성&lt;/li&gt;
&lt;li&gt;테스트와 배포: 여러 지역에서의 테스트 및 배포 자동화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글로벌 서비스를 운영할 때 고려해야 할 점들이 정말 많다는 걸 깨달았습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9단계: 메시지 큐 - 비동기 처리&lt;/h3&gt;
&lt;pre class=&quot;delphi&quot; data-ke-language=&quot;delphi&quot;&gt;&lt;code&gt;graph LR
    A[웹 서버&amp;lt;br/&amp;gt;생산자] --&amp;gt;|메시지 발행| B[메시지 큐]
    B --&amp;gt;|메시지 소비| C[이미지 처리&amp;lt;br/&amp;gt;소비자 1]
    B --&amp;gt;|메시지 소비| D[이메일 발송&amp;lt;br/&amp;gt;소비자 2]
    B --&amp;gt;|메시지 소비| E[로그 분석&amp;lt;br/&amp;gt;소비자 3]

    F[사용자] --&amp;gt;|이미지 업로드| A
    A --&amp;gt;|즉시 응답| F

    style A fill:#4caf50,color:#fff
    style B fill:#ff9800,color:#fff
    style C fill:#2196f3,color:#fff
    style D fill:#2196f3,color:#fff
    style E fill:#2196f3,color:#fff&lt;/code&gt;&lt;/pre&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;생산자(Producer): 메시지 발행&lt;/li&gt;
&lt;li&gt;메시지 큐: 메시지 버퍼 역할&lt;/li&gt;
&lt;li&gt;소비자(Consumer): 메시지 처리&lt;/li&gt;
&lt;/ul&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;/li&gt;
&lt;li&gt;안정성: 메시지 지속성 보장&lt;/li&gt;
&lt;li&gt;확장성: 각 컴포넌트 독립적 확장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;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;이미지 처리: 업로드 &amp;rarr; 큐 &amp;rarr; 비동기 처리&lt;/li&gt;
&lt;li&gt;이메일 발송: 요청 &amp;rarr; 큐 &amp;rarr; 배치 처리&lt;/li&gt;
&lt;li&gt;로그 처리: 수집 &amp;rarr; 큐 &amp;rarr; 분석&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10단계: 로그, 메트릭, 자동화 - 운영의 기초&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로그&lt;/b&gt;: 에러 로그 모니터링, 중앙화된 로그 수집 시스템&lt;br /&gt;&lt;b&gt;메트릭&lt;/b&gt;: 호스트 단위, 종합 메트릭, 비즈니스 메트릭&lt;br /&gt;&lt;b&gt;자동화&lt;/b&gt;: 지속적 통합(CI), 자동 빌드, 테스트, 배포&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분에서 &quot;시스템을 만드는 것과 운영하는 것은 다른 영역이구나&quot;라는 걸 느꼈습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11단계: 데이터베이스 샤딩 - 최종 확장&lt;/h3&gt;
&lt;pre class=&quot;delphi&quot; data-ke-language=&quot;delphi&quot;&gt;&lt;code&gt;graph TD
    A[사용자 요청&amp;lt;br/&amp;gt;user_id: 1001] --&amp;gt;|해시 함수| B[샤딩 로직&amp;lt;br/&amp;gt;1001 % 4 = 1]

    B --&amp;gt; C[샤드 0&amp;lt;br/&amp;gt;user_id % 4 = 0]
    B --&amp;gt; D[샤드 1&amp;lt;br/&amp;gt;user_id % 4 = 1]
    B --&amp;gt; E[샤드 2&amp;lt;br/&amp;gt;user_id % 4 = 2]
    B --&amp;gt; F[샤드 3&amp;lt;br/&amp;gt;user_id % 4 = 3]

    D --&amp;gt;|데이터 조회| G[사용자 1001 데이터]

    style D fill:#4caf50,color:#fff
    style C fill:#e0e0e0
    style E fill:#e0e0e0
    style F fill:#e0e0e0
    style G fill:#ffeb3b&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;샤딩 전략&lt;/b&gt;: 샤딩 키 선택, 해시 기반 샤딩&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;고려사항&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 재샤딩: 샤드 추가 시 데이터 재분배&lt;/li&gt;
&lt;li&gt;유명인사 문제: 특정 샤드에 트래픽 집중&lt;/li&gt;
&lt;li&gt;조인과 비정규화: 여러 샤드 간 조인의 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샤딩이 가장 복잡한 단계라는 걸 알 수 있었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;책에서 배운 핵심 아키텍처 패턴들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;수직적 확장 vs 수평적 확장&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;수직적 확장 (Scale Up)&lt;/th&gt;
&lt;th&gt;수평적 확장 (Scale Out)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;서버 성능 향상&lt;/td&gt;
&lt;td&gt;서버 수량 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;구현 단순&lt;/td&gt;
&lt;td&gt;복잡성 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;하드웨어 한계 존재&lt;/td&gt;
&lt;td&gt;이론적 무제한 확장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPOF 위험&lt;/td&gt;
&lt;td&gt;장애 내성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;고비용&lt;/td&gt;
&lt;td&gt;상대적 저비용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로드밸런싱 전략&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;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;IP 해시&lt;/b&gt;: 클라이언트 IP 기반 서버 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐시 패턴&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Cache-Aside&lt;/b&gt;: 애플리케이션이 캐시 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Write-Through&lt;/b&gt;: 쓰기 시 캐시와 DB 동시 업데이트&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Write-Behind&lt;/b&gt;: 캐시 먼저 쓰고 나중에 DB 업데이트&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Refresh-Ahead&lt;/b&gt;: 만료 전 미리 캐시 갱신&lt;/li&gt;
&lt;/ul&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;트레이드오프 분석&lt;/b&gt;이었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;성능 vs 일관성&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;최종 일관성&lt;/b&gt;: 일시적 불일치 허용, 성능 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CAP 정리&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;일관성(Consistency)&lt;/b&gt;: 모든 노드가 동일한 데이터&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가용성(Availability)&lt;/b&gt;: 시스템이 항상 응답&lt;/li&gt;
&lt;li&gt;&lt;b&gt;분할 내성(Partition Tolerance)&lt;/b&gt;: 네트워크 분할 시에도 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비용 vs 성능&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;분산 시스템&lt;/b&gt;: 상대적 저비용, 복잡성 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;모든 기술 선택에는 트레이드오프가 있다&quot;는 걸 명확하게 이해하게 됐습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무에 적용할 수 있는 인사이트들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 점진적 확장의 중요성&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;과도한 엔지니어링(Over-engineering) 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 모니터링의 필수성&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;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 데이터 중심 설계&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;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 장애 대응 설계&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;우아한 성능 저하(Graceful Degradation)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;책의 핵심 메시지&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;수백만 사용자를 지원하는 시스템을 설계하는 것은 도전적인 과제이며, 지속적인 계량과 끝없는 개선이 요구되는 여정이다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문장이 책의 핵심을 잘 표현한다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배운 핵심 원칙들&lt;/b&gt;:&lt;/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;점진적 확장&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;li&gt;&lt;b&gt;데이터 중심 설계&lt;/b&gt;: 데이터 특성에 맞는 아키텍처 선택&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책의 1장을 읽으면서 시스템 설계에 대한 체계적인 사고 과정을 배울 수 있었습니다. 특히 &quot;단계별 진화&quot;라는 접근 방식이 인상 깊었어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 &quot;처음부터 완벽한 시스템을 만들어야 한다&quot;는 압박감을 느꼈었는데, 이 책을 통해 &quot;필요에 따라 점진적으로 개선해나가면 된다&quot;는 걸 깨달았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 포스트에서는 2장 &quot;개략적인 규모 추정&quot;을 다룰 예정입니다. 시스템 설계할 때 용량 계산하는 방법을 다룬다고 하는데, 이것도 정말 필요한 내용일 것 같아요.&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;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 서버에서 시작하는 것 vs 처음부터 분산 시스템으로 설계하는 것, 어떤 상황에서 어떤 선택을 해야 할까요?&lt;/li&gt;
&lt;li&gt;수직 확장과 수평 확장의 기준점은 어떻게 정해야 할까요?&lt;/li&gt;
&lt;li&gt;로드밸런서 도입 시점을 어떻게 판단하시나요?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  데이터베이스 확장에 대한 질문&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;읽기 복제본을 언제 도입해야 할까요? 트래픽 기준이 있나요?&lt;/li&gt;
&lt;li&gt;데이터베이스 샤딩을 고려해야 하는 시점은 언제인가요?&lt;/li&gt;
&lt;li&gt;NoSQL vs RDBMS 선택 기준은 무엇인가요?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚡ 캐시 전략에 대한 질문&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 데이터를 캐시해야 하고, 어떤 데이터는 캐시하지 말아야 할까요?&lt;/li&gt;
&lt;li&gt;캐시 무효화 전략 중 가장 효과적인 방법은 무엇인가요?&lt;/li&gt;
&lt;li&gt;메모리 캐시 vs 분산 캐시, 언제 어떤 것을 선택해야 할까요?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; ️ 아키텍처 진화에 대한 질문&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마이크로서비스로 전환하는 적절한 시점은 언제일까요?&lt;/li&gt;
&lt;li&gt;CDN 도입의 비용 대비 효과를 어떻게 측정하나요?&lt;/li&gt;
&lt;li&gt;메시지 큐 도입이 필요한 상황은 어떤 경우인가요?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  모니터링과 운영에 대한 질문&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템 확장 시점을 판단하는 핵심 지표는 무엇인가요?&lt;/li&gt;
&lt;li&gt;장애 대응을 위한 모니터링 체계는 어떻게 구축해야 할까요?&lt;/li&gt;
&lt;li&gt;성능 병목점을 찾는 가장 효과적인 방법은 무엇인가요?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글로 공유해주시면 함께 배워나갈 수 있을 것 같습니다!&lt;/p&gt;</description>
      <category>  Career &amp;amp; Growth/  Learning Journey</category>
      <author>KilPenguin</author>
      <guid isPermaLink="true">https://penguin-dev.tistory.com/40</guid>
      <comments>https://penguin-dev.tistory.com/40#entry40comment</comments>
      <pubDate>Wed, 3 Sep 2025 20:00:52 +0900</pubDate>
    </item>
    <item>
      <title>TSBM 7월 밋업 후기</title>
      <link>https://penguin-dev.tistory.com/39</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_uctcqbuctcqbuctc.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bg0PIR/btsQg9mb7WJ/23SK3W0QDGhpNDA02f6Re1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bg0PIR/btsQg9mb7WJ/23SK3W0QDGhpNDA02f6Re1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bg0PIR/btsQg9mb7WJ/23SK3W0QDGhpNDA02f6Re1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbg0PIR%2FbtsQg9mb7WJ%2F23SK3W0QDGhpNDA02f6Re1%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;1024&quot; height=&quot;1024&quot; data-filename=&quot;Gemini_Generated_Image_uctcqbuctcqbuctc.png&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;b&gt;TypeScript Backend Meetup #7&lt;/b&gt;에 다녀왔습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TSBM이 뭔가요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 TSBM을 처음 들어보시는 분들을 위해 간단히 소개하면, &lt;b&gt;TypeScript Backend Meetup&lt;/b&gt;의 줄임말로 TypeScript 기반 백엔드 개발자들의 커리어 성장과 네트워킹을 위한 정기 모임입니다. 매월 개최되며, 실무 경험을 바탕으로 한 기술 발표와 자유로운 네트워킹이 특징이에요.&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;공식 GitHub&lt;/b&gt;: &lt;a href=&quot;https://github.com/ts-backend-meetup-ts/meetup&quot;&gt;https://github.com/ts-backend-meetup-ts/meetup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공식 유튜브&lt;/b&gt;: &lt;a href=&quot;https://youtube.com/@typescriptbackend&quot;&gt;Typescript Backend Meetup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문의&lt;/b&gt;: &lt;a href=&quot;https://open.kakao.com/o/gKXJtxEh&quot;&gt;TSBM 공지 오픈카톡방&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;나의 TSBM 여정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 TSBM은 이번이 처음이 아니에요. 5월에 열린 첫 번째 모임에도 참석했었는데, 그때는 바쁘다는 핑계로 후기를 제대로 남기지 못했거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 첫 모임에서 받은 인상이 너무 좋았어요. 단순한 기술 발표가 아니라 실제 현업에서 겪는 고민들과 해결 과정을 솔직하게 나누는 분위기가 정말 인상적이었습니다. 그래서 이번 7월 모임(실제로는 8월 1일에 진행)에도 망설임 없이 신청했어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시나 이번 밋업도 기대를 저버리지 않았습니다. 평소 온라인으로만 접하던 기술들을 실제로 현업에서 어떻게 적용하고 있는지 직접 들을 수 있는 좋은 기회였고, 특히 실제 회사에서 겪은 시행착오와 해결 과정을 솔직하게 공유해주는 세션들이 많아서 더욱 인상 깊었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 꼭 후기를 남겨야겠다는 생각으로 열심히 메모하며 들었던 내용들을 정리해보려고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;토스 김인성님 오픈마이크: &quot;Node.js 챕터는 무엇을 하는가?&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;밋업 시작 전, 토스 Node.js 챕터 리드 김인성님의 오픈마이크가 있었습니다. &quot;솔직히 채용 홍보를 해보려고 준비했다&quot;고 진솔하게 말씀하셔서 오히려 더 흥미롭게 들을 수 있었어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토스 Node.js 챕터의 진화 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토스 Node.js 챕터는 2018년 &lt;b&gt;스크래핑 기술 내재화&lt;/b&gt;로 시작해서, 지금은 토스 전 계열사의 기술적 엣지를 만들어내는 조직으로 성장했다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 놀라웠던 건 &lt;b&gt;하루 45만개의 테스트&lt;/b&gt;를 돌리는 테스트 자동화 플랫폼이었어요. 이 정도 규모의 테스트를 빠른 시간 내에 처리한다는 것 자체가 정말 대단하다고 생각했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현재 토스 Node.js 챕터가 하는 일들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;김인성님이 소개해주신 주요 프로젝트들:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TDMS (Toss Deploy Management System)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토스 전체 계열사의 배포 관리 시스템&lt;/li&gt;
&lt;li&gt;프론트엔드, 서버, 네이티브 앱 모든 배포를 관리&lt;/li&gt;
&lt;li&gt;외부 의존성 장애 시에도 배포 가능한 격리 시스템 구축&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Deus (디자인 툴)&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;Figma, Framer와 같은 디자인 툴을 자체 개발&lt;/li&gt;
&lt;li&gt;내부 시스템과 강결합으로 생산성, 로깅, 컴플라이언스까지 챙김&lt;/li&gt;
&lt;li&gt;토스 앱 내 미니앱 개발에 활용 예정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;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;Slack, Google, 외부 SaaS 연동 자동화&lt;/li&gt;
&lt;li&gt;Greenhouse, Workday 등 인사 시스템 연동&lt;/li&gt;
&lt;li&gt;바이럴 모니터링, AML, FDS 등 다양한 업무 자동화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AI/ML 플랫폼&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;핀테크 특성상 고객 정보 보안이 중요한 환경에서의 AI 활용&lt;/li&gt;
&lt;li&gt;내부 직원용 LLM 빌더 구축&lt;/li&gt;
&lt;li&gt;전 세계 10개국 40여개 AI 모델 배포 및 오케스트레이션&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모듈화를 통한 생산성 우상향&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;김인성님이 강조한 핵심은 &lt;b&gt;&quot;모듈화를 통한 생산성 우상향&quot;&lt;/b&gt;이었습니다. 특히 t2-http 라이브러리 구조가 인상적이었어요:&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;// 계층화된 HTTP 통신 라이브러리 구조
HttpTemplate (개발자 친화적 인터페이스)
  &amp;darr;
HttpClient (실제 통신 스펙 정의)
  &amp;darr;
다양한 구현체들:
- @tossteam/t2-http-client.axios (서버용)
- @tossteam/t2-http-client.got (고성능 서버용)
- @tossteam/t2-http-client.fetcher (브라우저용)
- HttpClientWithApp (앱 네이티브 통신용)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국 사이트들이 표준 스펙을 엄격히 따르지 않는 문제까지 고려해서 자체 쿠키 스토어를 구현했다는 점도 흥미로웠습니다. 이런 디테일한 부분까지 신경 쓰는 게 토스다운 접근이라는 생각이 들었어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;슬랙 자동화의 혁신&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Slack Bolt의 복잡한 이벤트 처리 방식을 NestJS 스타일 컨트롤러 패턴으로 개선한 사례도 인상적이었습니다:&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@SlackController()
export class AutomationController {
  @MessageHandler()
  async handleMessage(message: SlackMessage) {
    const thread = await this.slack.createThread(message);
    return thread.reply(&quot;처리 완료&quot;);
  }

  @ActionHandler(&quot;deploy_button&quot;)
  async handleDeployAction(action: SlackAction) {
    const deployment = await this.deployService.start(action.payload);
    return action.respond(`배포 시작: ${deployment.id}`);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 슬랙 이벤트를 체계적으로 관리할 수 있다니, 우리 팀에서도 참고할 만한 아이디어라고 생각했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토스의 기술 철학&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;모든 모듈이 전 계열사에서 재사용되면서 생산성이 우상향한다&quot;는 철학이 특히 인상적이었어요. 한 번 잘 만든 모듈이 토스페이먼츠, 토스플레이스, 토스뱅크, 토스증권 등 모든 계열사에서 활용되면서 조직 전체의 생산성이 계속 올라간다는 것이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;김인성님이 마지막에 &quot;토스 Node.js 챕터는 토스 커뮤니티의 기술적 엣지를 만들어내는 조직&quot;이라고 정의하신 것이 정말 와닿았습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;김범서님의 DDD 세션: &quot;해보지 않겠나 DDD..?&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 메인 세션은 퓨처스콜레의 김범서님이 발표한 DDD 세션이었습니다. 프로그래밍 농담으로 시작하신 게 기억에 남아요:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의사: &quot;하나님이 아담의 갈비뼈로 이브를 만들었으니, 의사가 가장 오래된 직업!&quot;&lt;br /&gt;엔지니어: &quot;그 전에 혼돈 속에서 세상의 질서를 만들고 건축했으니, 엔지니어가 먼저!&quot;&lt;br /&gt;프로그래머: &quot;그 혼돈을 누가 만들었죠?&quot;  &lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;레거시 MVC의 현실적 문제들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;김범서님이 보여준 레거시 코드 예시가 너무 현실적이어서 뜨끔했습니다:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 끝없이 늘어나는 Repository 메서드들
class UserRepository {
  findAll() {}
  findById(id: string) {}
  findByEmail(email: string) {}
  findByRole(role: string) {}
  findActiveUsersWithRecentOrders() {} // 화면별 특화 쿼리들...
  findInactiveUsersOlderThan(months: number) {}
  // ... 끝없는 find 메서드들

  save(user: User): Promise&amp;lt;void&amp;gt; {} // 정작 이게 Repository의 본질!
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 코드, 우리 프로젝트에도 있지 않나요?  &lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CQRS로 조회와 명령 분리하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 실용적이었던 건 CQRS + DAO 패턴이었습니다:&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;// Command: Repository는 비즈니스 로직에서만 사용
class OrderRepository {
  async findById(id: string): Promise&amp;lt;Order&amp;gt; {}
  async save(order: Order): Promise&amp;lt;void&amp;gt; {}
}

// Query: 화면 로직을 위한 직접 조회
class GetOrderListQueryHandler {
  constructor(private prisma: PrismaClient) {}

  async execute(customerId: string) {
    return this.prisma.order.findMany({
      where: { customerId },
      select: { id: true, total: true, createdAt: true },
    });
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 새로운 화면 요구사항이 생겨도 QueryHandler만 추가하면 되고, 핵심 비즈니스 로직은 건드리지 않아도 되겠더라고요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UI와 조회의 밀접성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Server Components나 GraphQL처럼 UI와 데이터 조회를 가까이 두는 트렌드에 대한 이야기도 흥미로웠습니다. &quot;응집도 관점에서 관련된 것들을 함께 두는 게 유지보수에 좋다&quot;는 말씀이 인상적이었어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최성국님의 MSA 세션: &quot;생산성 높은 MSA 환경 구성하기&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 메인 세션은 아임웹 생산성팀 최성국님의 MSA 환경 구성 이야기였습니다. 15년간 쌓인 PHP 모놀리스를 MSA로 전환하면서 겪은 현실적인 문제들과 해결 과정이 정말 솔직하고 유익했어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MSA의 이상과 현실&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 MSA를 도입하면 개발 속도가 빨라질 거라고 생각했는데, 오히려 복잡도만 증가했다는 고백이 인상적이었습니다:&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;기대: 서비스별 독립 배포 &amp;rarr; 빠른 개발 &amp;rarr; 빠른 피드백
현실: 인프라 복잡도 폭증 &amp;rarr; 인증 파편화 &amp;rarr; 개발 속도 저하&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 마이크로서비스 하나 띄우려면 3개의 저장소(Terraform, Kubernetes, ArgoCD)를 오가며 일주일이 걸렸다고 하니, 정말 비효율적이었겠어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자동화를 통한 혁신적 개선&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 문제를 해결한 방법이 정말 인상적이었습니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Before&lt;/b&gt;: 인프라 엔지니어가 3개 저장소 &amp;times; 일주일 작업&lt;br /&gt;&lt;b&gt;After&lt;/b&gt;: 개발자가 &lt;code&gt;apps/my-service/Dockerfile&lt;/code&gt; 푸시 &amp;rarr; 20분 후 서비스 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;99% 시간 단축이라니, 정말 대단한 성과라고 생각했어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Terraform 모듈화의 힘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Terraform 모듈 패키징 아이디어가 좋았습니다:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 기존: 매번 50줄의 복잡한 리소스 정의
resource &quot;aws_ecr_repository&quot; &quot;app&quot; { ... }
resource &quot;aws_iam_role&quot; &quot;app&quot; { ... }
resource &quot;aws_secretsmanager_secret&quot; &quot;app&quot; { ... }

# 개선: 한 줄로 모든 인프라 구성
module &quot;microservice&quot; {
  source = &quot;./modules/microservice&quot;
  service_name = &quot;my-service&quot;
  environment  = &quot;production&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm install하듯이 인프라를 설치하는 개념이라는 설명이 직관적이었어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JWT 기반 인증 통합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 파편화 문제도 중앙화된 JWT 서버로 해결했다고 합니다. 각 팀이 OAuth 2.0, Passport, JWT를 제각각 구현하던 문제를 하나의 표준으로 통일한 거죠.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Kong Gateway 선택 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API Gateway로 Kong을 선택한 이유도 흥미로웠어요:&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;Istio&lt;/b&gt;: 학습 곡선이 가파르고 k8s 외부 서비스 연결 불가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Nginx/Caddy&lt;/b&gt;: 설정 복잡하고 백엔드 엔지니어들이 문법 학습 필요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Kong&lt;/b&gt;: YAML 기반 선언형 설정으로 Git 형상 관리 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;선언형 설정이 중요한 이유는 인프라 엔지니어와 백엔드 엔지니어 간 커뮤니케이션 비용을 줄일 수 있기 때문&quot;이라는 설명이 와닿았습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;김현우님의 NestJS 세션: &quot;NestJS 의존성 주입 딥다이브&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 세션은 모두싸인의 김현우님의 NestJS 의존성 주입 딥다이브였습니다. &quot;사사똑 코마없&quot;(사람 사는 거 똑같고 코딩에 마법은 없다)라는 표현이 기억에 남아요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의존성 주입의 4단계 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJS가 어떻게 마법처럼 의존성을 주입해주는지 4단계로 설명해주셨습니다:&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;메타데이터 기록&lt;/b&gt;: TypeScript가 컴파일 시 &lt;code&gt;design:paramtypes&lt;/code&gt; 등 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모듈 스캔&lt;/b&gt;: DFS로 모든 모듈 순회하며 의존성 트리 구성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;InstanceWrapper 생성&lt;/b&gt;: 실제 인스턴스를 담을 래퍼 객체 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인스턴스 생성&lt;/b&gt;: 의존성 순서대로 &lt;code&gt;new&lt;/code&gt; 키워드로 실제 객체 생성&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TypeScript 메타데이터 시스템&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 TypeScript가 컴파일 시 자동으로 생성하는 메타데이터를 활용한다는 점이 흥미로웠어요:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// TypeScript 컴파일 후 자동 추가
Reflect.defineMetadata(&quot;design:paramtypes&quot;, [UserRepository], UserService);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Inject 데코레이터는 이런 메타데이터에 추가 정보를 덧붙이는 역할이고요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 소스코드 레벨 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;김현우님이 실제 NestJS 소스코드를 보여주면서 설명해주신 게 정말 좋았어요. &lt;code&gt;NestFactory.create()&lt;/code&gt;부터 시작해서 실제 인스턴스가 생성되는 과정까지, 정말 &quot;마법은 없다&quot;는 걸 보여주셨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순환 참조 처리나 성능 최적화(Promise.all로 병렬 처리) 같은 디테일까지 다뤄주셔서 NestJS에 대한 이해가 한층 깊어졌어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개인적인 소감과 적용 계획&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&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;b&gt;김범서님의 DDD 실용적 접근&lt;/b&gt;: 완벽한 DDD보다는 CQRS 같은 실용적 패턴부터 시작하는 점진적 접근&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최성국님의 자동화 집착&lt;/b&gt;: 99% 시간 단축이라는 구체적 성과와 &quot;수기 작업 완전 제거&quot;라는 명확한 목표&lt;/li&gt;
&lt;li&gt;&lt;b&gt;김현우님의 NestJS 내부 분석&lt;/b&gt;: &quot;마법은 없다&quot;는 철학으로 실제 소스코드까지 보여준 깊이 있는 설명&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기술 선택의 현실성&lt;/b&gt;: 이론적 완벽함보다는 팀의 학습 곡선과 유지보수성을 고려한 선택&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;우리 팀에 적용해보고 싶은 것들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;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;CQRS 패턴으로 조회 로직 분리해보기&lt;/li&gt;
&lt;li&gt;공통 기능들을 패키지로 모듈화하기&lt;/li&gt;
&lt;li&gt;반복적인 인프라 작업 자동화 검토&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;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;DDD 패턴을 작은 기능부터 점진적 적용&lt;/li&gt;
&lt;li&gt;API Gateway 도입으로 공통 기능 중앙화&lt;/li&gt;
&lt;li&gt;배포 프로세스 완전 자동화&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아쉬웠던 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간 관계상 각 세션이 좀 빠르게 진행되어서, 더 깊이 있는 질의응답을 하지 못한 게 아쉬웠어요. 특히 DDD 세션에서는 Domain Service나 Bounded Context 같은 고급 개념들을 더 듣고 싶었는데, 시간이 부족했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 TSBM 7월 밋업은 단순한 기술 소개를 넘어서, 실제 현업에서의 시행착오와 해결 과정을 솔직하게 공유해주는 자리였습니다. 특히 &quot;완벽한 기술&quot;보다는 &quot;현실적인 해결책&quot;에 초점을 맞춘 발표들이 많아서 더욱 유익했어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;김범서님의 DDD 점진적 접근, 최성국님의 MSA 자동화 집착, 김현우님의 NestJS 내부 구조 분석까지... 각각 다른 관점에서 TypeScript 백엔드 개발의 현실과 미래를 엿볼 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 TSBM 밋업도 꼭 참석해서, 또 다른 인사이트를 얻어오고 싶네요. 혹시 비슷한 경험이나 의견이 있으신 분들은 댓글로 공유해주세요!&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;이 글이 도움이 되셨다면 공유해주세요. 더 많은 개발자들과 함께 성장하고 싶습니다.&lt;/i&gt;&lt;/p&gt;</description>
      <category>  Career &amp;amp; Growth/  Conference &amp;amp; Community</category>
      <author>KilPenguin</author>
      <guid isPermaLink="true">https://penguin-dev.tistory.com/39</guid>
      <comments>https://penguin-dev.tistory.com/39#entry39comment</comments>
      <pubDate>Wed, 3 Sep 2025 08:56:36 +0900</pubDate>
    </item>
    <item>
      <title>토스 메이커스 컨퍼런스 2025 참석 후기: 대규모 서비스의 기술적 진화를 엿보다</title>
      <link>https://penguin-dev.tistory.com/38</link>
      <description>&lt;h1&gt;토스 메이커스 컨퍼런스 2025 참석 후기: 대규모 서비스의 기술적 진화를 엿보다&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참석일&lt;/b&gt;: 2025년 7월 25일 (금)&lt;br /&gt;&lt;b&gt;장소&lt;/b&gt;: 코엑스 컨벤션센터&lt;br /&gt;&lt;b&gt;주요 세션&lt;/b&gt;: 10개 기술 세션 참석&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_cbgl8qcbgl8qcbgl.jpeg&quot; data-origin-width=&quot;2816&quot; data-origin-height=&quot;1536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BwqDt/btsPBJvvA6O/xJCWkRg1OZ0mHGS5gY2Pj0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BwqDt/btsPBJvvA6O/xJCWkRg1OZ0mHGS5gY2Pj0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BwqDt/btsPBJvvA6O/xJCWkRg1OZ0mHGS5gY2Pj0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBwqDt%2FbtsPBJvvA6O%2FxJCWkRg1OZ0mHGS5gY2Pj0%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;733&quot; height=&quot;400&quot; data-filename=&quot;Gemini_Generated_Image_cbgl8qcbgl8qcbgl.jpeg&quot; data-origin-width=&quot;2816&quot; data-origin-height=&quot;1536&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;토스 메이커스 컨퍼런스 2025에 참석하며 가장 인상 깊었던 점은 &lt;b&gt;&quot;실패를 감추지 않고 드러내며 빠르게 공유하여 다음 선택의 근거로 삼는다&quot;&lt;/b&gt;는 토스의 기술 철학이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;20년 된 레거시 시스템의 현대화부터 최신 ML 플랫폼 구축까지, 각 세션에서 공유된 내용은 단순한 성공 사례가 아닌 &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;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1️⃣ 레거시는 짐이 아닌 자산이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;20년 레거시를 넘어 미래를 준비하는 시스템 만들기&quot;&lt;/b&gt; 세션에서 가장 놀라웠던 점은 토스가 2020년 인수한 20년 된 결제 시스템을 &lt;b&gt;완전히 새로 만들지 않고 점진적으로 현대화&lt;/b&gt;했다는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;현대화 전략:
  - Git 도입으로 코드 관리 체계화
  - MSA 전환 (150개+ 마이크로서비스)
  - 4개 AZ 분산 Kubernetes 클러스터
  - Java 17/21 사용률 50% 이상 달성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 교훈&lt;/b&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;&lt;b&gt;&quot;현장결제 서비스의 분산 트랜잭션 관리학 개론&quot;&lt;/b&gt;에서 소개된 &lt;b&gt;트리 구조 기반 트랜잭션 관리&lt;/b&gt;는 기존의 2PC나 Saga 패턴과는 완전히 다른 접근법이었습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;문제: 2개 트랜잭션 &amp;rarr; 16가지 경우의 수
      4개 트랜잭션 &amp;rarr; 256가지 경우의 수

해결: 트리 구조 + Resolve 메커니즘
      - 각 노드가 직계 하위 노드 상태만 관리
      - 복잡도를 O(1) 서브트리 단위로 축소
      - 양방향 보정 (재시도 + 롤백)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 교훈&lt;/b&gt;: 복잡한 문제를 해결할 때는 기존 패턴에 얽매이지 말고, 문제의 본질을 이해하여 새로운 접근법을 시도해볼 용기가 필요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3️⃣ 설정 관리도 소프트웨어 개발이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;수천 개의 API/BATCH 서버를 하나의 설정 체계로 관리하기&quot;&lt;/b&gt;에서 인상 깊었던 점은 &lt;b&gt;설정을 코드처럼 설계하고 관리&lt;/b&gt;한다는 철학이었습니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Overlay Architecture:
  App (최고 우선순위)
    &amp;darr;
  AppGroup 
    &amp;darr;  
  Phase
    &amp;darr;
  Cluster
    &amp;darr;
  Global (기본값)

특징:
  - 템플릿 시스템으로 중복 제거
  - Python 스크립트 주입으로 동적 설정
  - JobDSL로 배치 작업 코드화&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ScyllaDB로 극한 성능 달성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;토스뱅크의 Online Feature Store&lt;/b&gt;에서 ScyllaDB를 선택한 이유와 운영 경험이 매우 실용적이었습니다.&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;성능 지표:
  - 쓰기: 120,000 TPS, P99 &amp;lt; 5ms
  - 읽기: P99 &amp;lt; 5ms 안정적 유지
  - 처리량: 매일 수천만건 적재

주의사항:
  - Large Partition 관리 (100MB+ 파티션 문제)
  - Full Scan 쿼리 제어 필요
  - 중간 Feature Server로 쿼리 검증&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 교훈&lt;/b&gt;: 새로운 기술 도입시 성능만 보지 말고, 운영 복잡도와 함정까지 고려해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결제 시스템의 CQRS + 이벤트 기반 전환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;확장성과 회복탄력성을 갖춘 결제시스템 만들기&quot;&lt;/b&gt;에서 가장 인상 깊었던 점은 &lt;b&gt;실제 장애 사례와 해결 과정&lt;/b&gt;을 숨김없이 공유한 것이었습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;주요 장애와 해결:
  1. DB 부하 &amp;rarr; 인덱스 힌트로 옵티마이저 제어
  2. 타임아웃 불일치 &amp;rarr; 전체 서버 체인 타임아웃 재설정  
  3. 이벤트 누락 &amp;rarr; 아웃박스 패턴 + 보상 트랜잭션
  4. 데이터 불일치 &amp;rarr; 두 원장간 보정 배치 운영&lt;/code&gt;&lt;/pre&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  개발 조직의 성장&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Infrastructure as Code의 진화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;성장하는 엔터프라이즈를 위한 인터널 서비스 제공 전략&quot;&lt;/b&gt;에서는 토스가 어떻게 계열사 확장에 대응했는지 볼 수 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;해결 전략:
  - Terraform으로 인프라 &quot;찍어내기&quot;
  - ArgoCD로 Kubernetes 리소스 관리
  - Config Store로 MSA 환경 설정 통합
  - Gateway Plugin으로 공통 로직 처리&lt;/code&gt;&lt;/pre&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;h3 data-ke-size=&quot;size23&quot;&gt;배치 시스템의 완전한 재구축&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;레거시 정산 개편기&quot;&lt;/b&gt;에서는 20년 된 정산 시스템을 어떻게 현대적으로 재구축했는지 상세히 다뤘습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;성능 최적화 3법칙:
  1. 캐싱: 가맹점 설정정보 메모리 캐시
  2. 벌크 처리: 래퍼 클래스로 배치 처리
  3. JDBC Batch Insert: N번 &amp;rarr; 1번 처리

결과:
  - 처리 성능: 최대 10배 향상
  - 2,100개 배치 자동화 관리
  - 하루 수천만건 안정적 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 교훈&lt;/b&gt;: 성능 최적화의 핵심은 &lt;b&gt;I/O 최소화&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;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;권태기 극복의 3가지 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 세션 &lt;b&gt;&quot;개발자 권태기, 10년 동안 이렇게 이겨냈습니다&quot;&lt;/b&gt;는 기술적 내용은 아니었지만, 가장 실용적인 세션이었습니다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;1. 일상 무료함 탈출 (Flow 이론):
   - 명확한 목표 + 적절한 난이도 + 즉각적 피드백
   - 매일 작은 도전 추가

2. 동기 회복 (자기결정성 이론):
   - 자율성: 스스로 선택하는 일
   - 유능감: 기술적 성장 체감
   - 관계성: 동료와의 협업

3. 번아웃 예방:
   - 수면 위생이 핵심 (한국 OECD 수면시간 꼴등)
   - 침실은 수면 전용, 20도 유지&lt;/code&gt;&lt;/pre&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  실시간 데이터 처리의 현실&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Apache Druid vs StarRocks&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;고객은 절대 기다려주지 않는다&quot;&lt;/b&gt; 세션에서는 두 OLAP 엔진의 실제 사용 경험을 비교해볼 수 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;Apache Druid:
  장점: 시계열 특화, 비트맵 인덱스, 월 5천만원 절감
  한계: 멱등성 부족, 스키마 변경 어려움, 운영 복잡

StarRocks:
  장점: 멱등성 지원, CDC 네이티브, 조인 성능 84.6% 개선
  특징: Colocation Group으로 로컬 조인

현재: 하이브리드 운영 (각각의 장점 활용)&lt;/code&gt;&lt;/pre&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  마무리: 토스에서 배운 것들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&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;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;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;즉시 적용해볼 것들&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 설정 관리의 체계화 (계층형 구조 도입)&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 배치 작업의 코드화 및 자동화&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 카나리 배포를 통한 안전한 시스템 전환&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 개발자 권태기 예방을 위한 작은 도전들&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장기적으로 고민해볼 것들&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 레거시 시스템의 점진적 현대화 전략&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 분산 트랜잭션 관리 방식 개선&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; Infrastructure as Code 도입&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;disabled&quot; type=&quot;checkbox&quot; /&gt; 실시간 데이터 처리 아키텍처 고도화&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;토스 메이커스 컨퍼런스의 가장 큰 가치는 '완벽한 해답'을 제시하는 것이 아니라, '시행착오와 학습의 과정'을 공유한다는 점이었습니다.&lt;/b&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;기술 컨퍼런스 참석이 단순한 트렌드 파악을 넘어 &lt;b&gt;실무에 바로 적용할 수 있는 구체적인 인사이트&lt;/b&gt;를 얻을 수 있는 소중한 경험이었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;본 후기는 토스 메이커스 컨퍼런스 2025 참석 후 개인적인 관점에서 작성되었으며, 실제 발표 내용을 바탕으로 구성되었습니다.&lt;/i&gt;&lt;/p&gt;</description>
      <category>  Career &amp;amp; Growth/  Conference &amp;amp; Community</category>
      <author>KilPenguin</author>
      <guid isPermaLink="true">https://penguin-dev.tistory.com/38</guid>
      <comments>https://penguin-dev.tistory.com/38#entry38comment</comments>
      <pubDate>Thu, 31 Jul 2025 08:19:48 +0900</pubDate>
    </item>
    <item>
      <title>AI 시대, 개발자는 어떻게 살아남을 것인가?</title>
      <link>https://penguin-dev.tistory.com/37</link>
      <description>&lt;h1&gt;AI 시대, 개발자는 어떻게 살아남을 것인가?&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토비님의 &quot;AI 시대 개발자로 살아가는 법&quot; 세미나에서 얻은 통찰과 실무 적용 방안&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div id=&quot;79889802072bb0c5&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gemini_Generated_Image_u984zu984zu984zu.jpeg&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;2048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B1gko/btsPs0CqsB6/tvushFkZruKMvS3rk8j8KK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B1gko/btsPs0CqsB6/tvushFkZruKMvS3rk8j8KK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B1gko/btsPs0CqsB6/tvushFkZruKMvS3rk8j8KK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB1gko%2FbtsPs0CqsB6%2FtvushFkZruKMvS3rk8j8KK%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;616&quot; height=&quot;616&quot; data-filename=&quot;Gemini_Generated_Image_u984zu984zu984zu.jpeg&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;2048&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 개발자 커뮤니티는 그 어느 때보다 혼란스럽습니다. &quot;AI가 개발자를 대체할 것이다&quot;라는 위기론과 &quot;AI는 개발자를 대체할 수 없다&quot;는 낙관론이 동시에 쏟아지고 있죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황에서 《토비의 스프링》 저자이자 31년차 개발자인 토비님이 제시한 &lt;b&gt;&quot;AI 시대 개발자 생존 전략&quot;&lt;/b&gt;을 정리해보았습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; ️ 지금은 &quot;AI 대혼란의 시대&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토비는 현재를 &lt;b&gt;&quot;AI 시대가 아닌 AI 대혼란의 시대&quot;&lt;/b&gt;라고 정의합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상반된 메시지들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;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;신입 개발자 안 뽑습니다&quot; (AI가 대체)&lt;/li&gt;
&lt;li&gt;저커버그: &quot;1년 내 50% 대체&quot;&lt;/li&gt;
&lt;li&gt;Claude CEO: &quot;90% 코드를 AI가 생성&quot;&lt;/li&gt;
&lt;/ul&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;IBM CEO: &quot;AI는 프로그래머 대체 못해&quot;&lt;/li&gt;
&lt;li&gt;이코노미스트: &quot;실제 직업 감소 미미&quot;&lt;/li&gt;
&lt;li&gt;실리콘밸리 개발자 수 큰 변화 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 혼란 속에서 개발자들은 어떻게 대응해야 할까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  핵심 역량 1: 멀티태스킹 마스터가 되어라&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;기존&lt;/b&gt;: 하나의 프로젝트에 집중&lt;br /&gt;&lt;b&gt;AI 시대&lt;/b&gt;: 동시 다발적 프로젝트 관리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 한 개발자는 &lt;b&gt;Cursor 4개 창&lt;/b&gt;으로 4개 프로젝트를 동시에 개발하고 있고, 극단적인 경우 &lt;b&gt;40개 화면&lt;/b&gt;을 동시에 운영하는 사례도 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;멀티태스킹의 핵심 원리&lt;/h3&gt;
&lt;pre class=&quot;fix&quot;&gt;&lt;code&gt;AI 작업 대기시간 = 다른 프로젝트 작업 시간
&amp;rarr; 빠른 컨텍스트 스위칭 능력 필수
&amp;rarr; 전체 생산성 극대화&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 코드를 생성하는 동안 멍하니 기다리지 말고, 다른 프로젝트로 넘어가서 작업하는 것이 핵심입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; ️ 핵심 역량 2: 상황에 맞는 AI 협업 모드 선택&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토비는 AI와의 협업을 3가지 모드로 구분합니다:&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. AI in the Loop (AI가 주도)&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;: AI가 주요 작업 수행, 인간은 최종 검토&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적용&lt;/b&gt;: 반복적 CRUD 개발, 테스트 코드 작성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;도구&lt;/b&gt;: GitHub Copilot&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Human in the Loop (인간이 주도)&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;: 인간이 방향 설정, AI는 보조 도구&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적용&lt;/b&gt;: 아키텍처 설계, 핵심 비즈니스 로직&lt;/li&gt;
&lt;li&gt;&lt;b&gt;도구&lt;/b&gt;: Claude, ChatGPT&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Human on the Loop (감시자 역할)&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;: AI가 자율 작업, 인간은 모니터링&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적용&lt;/b&gt;: 자동화된 테스트, 배포 파이프라인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;도구&lt;/b&gt;: AI 에이전트, 자동화 도구&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심은 상황에 맞는 모드를 선택하는 능력&lt;/b&gt;입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  AI 코딩 스펙트럼: 현재 우리는 어디에?&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;th&gt;도구 예시&lt;/th&gt;
&lt;th&gt;현재 상태&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Level 0&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;정적 도구&lt;/td&gt;
&lt;td&gt;전통적 IDE&lt;/td&gt;
&lt;td&gt;✅ 완성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Level 1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;자동완성&lt;/td&gt;
&lt;td&gt;IntelliSense&lt;/td&gt;
&lt;td&gt;✅ 완성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Level 2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;코드 제안&lt;/td&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;✅ 완성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Level 3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;대화형 개발&lt;/td&gt;
&lt;td&gt;Claude Code, Cursor&lt;/td&gt;
&lt;td&gt;✅ 현재&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Level 4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;프로젝트 단위&lt;/td&gt;
&lt;td&gt;고급 AI 에이전트&lt;/td&gt;
&lt;td&gt;  개발 중&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Level 5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;완전 자율&lt;/td&gt;
&lt;td&gt;미래 기술&lt;/td&gt;
&lt;td&gt;❌ 미래&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 대부분의 개발자는 &lt;b&gt;Level 2-3&lt;/b&gt; 사이에서 작업하고 있으며, Level 4 도구들이 빠르게 등장하고 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  실제 기업 사례: Shopify의 AI 전면 도입&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전략&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;전사 AI 필수 사용&lt;/b&gt;: 임원부터 인턴까지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;마케팅팀도 Cursor 사용&lt;/b&gt; (개발 도구를 비개발자도!)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&quot;AI 안 쓰면 퇴사&quot;&lt;/b&gt; 정책&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;✅ &lt;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;인턴 채용 40배 증가&lt;/b&gt; (25명 &amp;rarr; 1000명)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 인사이트&lt;/b&gt;: AI는 대체가 아닌 &lt;b&gt;증강(Augmentation)&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; ️ 실무에서 바로 써먹는 AI 도구 활용법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상황별 도구 선택 가이드&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;작업 유형&lt;/th&gt;
&lt;th&gt;1순위 도구&lt;/th&gt;
&lt;th&gt;2순위 도구&lt;/th&gt;
&lt;th&gt;선택 이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;일상적 코딩&lt;/td&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;안정성과 속도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;복잡한 로직&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;ChatGPT&lt;/td&gt;
&lt;td&gt;설명과 대화 능력&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프로젝트 분석&lt;/td&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;컨텍스트 이해&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;학습/연구&lt;/td&gt;
&lt;td&gt;NotebookLM&lt;/td&gt;
&lt;td&gt;Claude&lt;/td&gt;
&lt;td&gt;문서 처리 능력&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TDD의 새로운 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AI 시대의 TDD = AI 생성 코드의 안전장치&lt;/b&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;AI에게 테스트부터 요청&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;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  학습 전략: Active Learning이 핵심&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❌ 수동적 수용 (위험)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI 결과를 그대로 받아들임&lt;/li&gt;
&lt;li&gt;이해 없이 복사-붙여넣기&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 능동적 참여 (권장)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI에게 &quot;왜?&quot;와 &quot;어떻게?&quot; 질문&lt;/li&gt;
&lt;li&gt;코드의 동작 원리 파악&lt;/li&gt;
&lt;li&gt;대안 방법 탐구&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Learning vs Production 균형&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;학습 모드 (30%)&lt;/b&gt;: 개인 성장, AI 설명 요청&lt;/li&gt;
&lt;li&gt;&lt;b&gt;생산 모드 (70%)&lt;/b&gt;: 업무 효율, 빠른 결과 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  내일부터 시작할 수 있는 실천 방안&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&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;b&gt;멀티 프로젝트 시작&lt;/b&gt;: 최소 2개 프로젝트 동시 진행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AI 도구 일일 사용&lt;/b&gt;: 매일 다른 AI 도구 경험&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모드 스위칭 연습&lt;/b&gt;: 상황별 적절한 협업 모드 선택&lt;/li&gt;
&lt;li&gt;&lt;b&gt;능동적 질문하기&lt;/b&gt;: AI에게 설명 요청 습관화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TDD 적용&lt;/b&gt;: AI 생성 코드 검증 도구로 활용&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&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;b&gt;AI 도구 공유 세션&lt;/b&gt;: 주간 경험 공유&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드 리뷰에 AI 활용&lt;/b&gt;: 분석 및 개선점 제안&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AI 가이드라인 수립&lt;/b&gt;: 사용 기준 및 보안 정책&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  단계별 성장 로드맵&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1개월차: 기초 적응&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI 도구 3개 이상 경험&lt;/li&gt;
&lt;li&gt;멀티태스킹 패턴 실험&lt;/li&gt;
&lt;li&gt;개인 활용 가이드 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3개월차: 숙련도 향상&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;팀 내 AI 활용 공유&lt;/li&gt;
&lt;li&gt;생산성 지표 측정&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6개월차: 전문성 구축&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI 협업 모델 마스터&lt;/li&gt;
&lt;li&gt;주니어 개발자 멘토링&lt;/li&gt;
&lt;li&gt;새로운 도구 평가 및 도입&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  결론: AI 시대 개발자의 생존 공식&lt;/h2&gt;
&lt;pre class=&quot;fix&quot;&gt;&lt;code&gt;성공하는 AI 시대 개발자 = 
  멀티태스킹 능력 &amp;times; 
  모드 스위칭 스킬 &amp;times; 
  능동적 학습 자세 &amp;times; 
  전통적 개발 원칙 유지&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토비의 핵심 메시지&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;AI가 우리를 대체하는 것이 아니라, AI를 잘 활용하는 개발자가 그렇지 못한 개발자를 대체할 것이다.&quot;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;우리가 가져야 할 마음가짐&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;혼란스러워하지 말고, 실험하고 적응하자&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AI를 두려워하지 말고, 파트너로 받아들이자&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전통적 개발 원칙을 버리지 말고, AI와 결합하자&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 시대의 개발자에게 요구되는 것은 &lt;b&gt;기술적 스킬의 변화&lt;/b&gt;가 아닌 &lt;b&gt;일하는 방식의 변화&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 프로젝트에 몰입하던 기존 방식에서 벗어나 &lt;b&gt;여러 프로젝트를 동시에 관리&lt;/b&gt;하고, &lt;b&gt;상황에 맞는 AI 협업 모드&lt;/b&gt;를 선택하며, &lt;b&gt;능동적으로 학습&lt;/b&gt;하는 개발자가 되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변화는 이미 시작되었습니다. 이제 우리가 선택할 수 있는 것은 &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;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토비의 스프링 부트 강의 (인프런)&lt;/li&gt;
&lt;li&gt;클린 스프링 강의 (인프런)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/features/copilot&quot;&gt;GitHub Copilot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://claude.ai&quot;&gt;Claude&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cursor.sh&quot;&gt;Cursor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://notebooklm.google.com&quot;&gt;NotebookLM&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;이 글은 토비의 &quot;AI 시대 개발자로 살아가는 법&quot; 세미나(2025.07.10) 내용을 바탕으로 작성되었습니다.&lt;/i&gt;&lt;/p&gt;</description>
      <category>  Career &amp;amp; Growth/  Conference &amp;amp; Community</category>
      <author>KilPenguin</author>
      <guid isPermaLink="true">https://penguin-dev.tistory.com/37</guid>
      <comments>https://penguin-dev.tistory.com/37#entry37comment</comments>
      <pubDate>Mon, 21 Jul 2025 20:46:47 +0900</pubDate>
    </item>
    <item>
      <title>Dependabot으로 의존성 관리 자동화하기</title>
      <link>https://penguin-dev.tistory.com/36</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UbHmZ/btsO8827wxI/wIWPhxMVixZEtN2WG82ITK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UbHmZ/btsO8827wxI/wIWPhxMVixZEtN2WG82ITK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UbHmZ/btsO8827wxI/wIWPhxMVixZEtN2WG82ITK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUbHmZ%2FbtsO8827wxI%2FwIWPhxMVixZEtN2WG82ITK%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;543&quot; height=&quot;362&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;Dependabot으로 의존성 관리 자동화하기&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;매일 아침 의존성 업데이트 PR을 확인하는 것이 일상이 된 개발자를 위한 실전 가이드&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 오늘은 저희 팀에서 실제로 사용하고 있는 &lt;b&gt;Dependabot 설정과 운영 노하우&lt;/b&gt;를 공유해드리려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 관리, 정말 귀찮지 않나요? 새로운 보안 패치가 나올 때마다 수동으로 업데이트하고, 어떤 패키지가 outdated인지 확인하고... 이런 반복 작업을 자동화할 수 있다면 얼마나 좋을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Dependabot&lt;/b&gt;이 바로 그 해답입니다!  &lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 Dependabot을 도입했을까?&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;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;p data-ke-size=&quot;size16&quot;&gt;그래서 Dependabot을 도입했고, 지금까지 &lt;b&gt;62개의 PR&lt;/b&gt;을 자동으로 처리했습니다!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;우리의 Dependabot 설정 공개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 저희가 사용하는 설정을 그대로 공개합니다:&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: &quot;npm&quot; # Yarn 사용해도 &quot;npm&quot;으로 설정해요
    directory: &quot;/&quot;
    schedule:
      interval: &quot;daily&quot; # 매일 체크
      time: &quot;09:00&quot; # 오전 9시 (출근 시간!)
      timezone: &quot;Asia/Seoul&quot; # 한국 시간 기준
    target-branch: &quot;develop&quot; # develop 브랜치로 PR 생성
    commit-message:
      prefix: &quot;chore(deps)&quot; # 깔끔한 커밋 메시지
      include: &quot;scope&quot; # deps, deps-dev 구분
    open-pull-requests-limit: 10
    assignees:
      - &quot;kil-penguin&quot; # 담당자 자동 할당
    labels:
      - &quot;dependencies&quot;
      - &quot;automated&quot;
    rebase-strategy: &quot;auto&quot;&lt;/code&gt;&lt;/pre&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;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;하지만 PR이 너무 많다면 &lt;code&gt;weekly&lt;/code&gt;로 변경해도 됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 develop 브랜치로 할까요?&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;main 브랜치에 바로 PR을 만들면 위험하니까요&lt;/li&gt;
&lt;li&gt;develop에서 테스트 후 안전하게 배포하는 전략입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;매일 아침 루틴: Dependabot PR 체크하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희 팀의 아침 루틴을 공개합니다! ☕&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  오전 9:30 체크리스트&lt;/h3&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;# 1. 새로운 PR 확인
gh pr list --author &quot;app/dependabot&quot; --state open&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어 하나면 모든 Dependabot PR을 볼 수 있어요!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  우선순위는 이렇게 정해요&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;  긴급도&lt;/th&gt;
&lt;th&gt;타입&lt;/th&gt;
&lt;th&gt;어떻게 처리할까?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;  &lt;b&gt;최우선&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;바로 병합!&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;  &lt;b&gt;자동처리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Patch (1.0.1 &amp;rarr; 1.0.2)&lt;/td&gt;
&lt;td&gt;자동 승인됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;  &lt;b&gt;확인필요&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Minor (1.0.0 &amp;rarr; 1.1.0)&lt;/td&gt;
&lt;td&gt;자동 승인됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;  &lt;b&gt;신중검토&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Major (1.0.0 &amp;rarr; 2.0.0)&lt;/td&gt;
&lt;td&gt;수동 검토 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Dependabot PR 읽는 법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Dependabot PR이 복잡해 보일 수 있어요. 하지만 구조를 알면 쉬워집니다!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  PR 제목 해석하기&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;chore(deps)(deps): bump remeda from 2.23.1 to 2.23.2&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;&lt;code&gt;chore(deps)&lt;/code&gt;: 의존성 관련 작업이라는 뜻&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(deps)&lt;/code&gt;: 프로덕션 의존성 (deps-dev면 개발 의존성)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bump&lt;/code&gt;: 버전 업데이트&lt;/li&gt;
&lt;li&gt;&lt;code&gt;remeda from 2.23.1 to 2.23.2&lt;/code&gt;: 어떤 패키지가 어떻게 바뀌는지&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  PR 본문에서 확인할 것들&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Release Notes&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;Bug Fixes, Features, Breaking Changes 체크&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Compatibility Score&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dependabot이 알려주는 안전도 점수&lt;/li&gt;
&lt;li&gt;높을수록 안전한 업데이트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Dependabot Commands&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@dependabot rebase&lt;/code&gt;: 충돌 해결&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@dependabot ignore&lt;/code&gt;: 이 업데이트 무시&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실전 처리 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 일반적인 경우 (Patch/Minor)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 경우는 이렇게 처리해요:&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;# 자동 승인 확인 후 바로 병합
gh pr merge &amp;lt;PR_NUMBER&amp;gt; --squash&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Major 업데이트는 조심스럽게&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Major 업데이트는 Breaking Changes가 있을 수 있어서 신중하게 처리합니다:&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;Release Notes 꼼꼼히 읽기&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로컬에서 테스트해보기&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;git checkout &amp;lt;dependabot-branch&amp;gt;
yarn install &amp;amp;&amp;amp; yarn lint &amp;amp;&amp;amp; yarn build
# 문제없으면 병합
gh pr merge &amp;lt;PR_NUMBER&amp;gt; --squash&lt;/code&gt;&lt;/pre&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;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;gh pr review &amp;lt;PR_NUMBER&amp;gt; --approve
gh pr merge &amp;lt;PR_NUMBER&amp;gt; --squash&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 발생하는 문제와 해결법&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;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 브랜치로 이동해서
git checkout &amp;lt;dependabot-branch&amp;gt;

# 린팅 자동 수정
yarn install
yarn lint --fix

# 수정사항 커밋
git add .
git commit -m &quot;fix: resolve linting issues&quot;
git push&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚔️ 충돌이 발생했을 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 &lt;code&gt;yarn.lock&lt;/code&gt; 파일에서 충돌이 발생해요:&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 리베이스로 해결
git checkout &amp;lt;dependabot-branch&amp;gt;
git rebase develop

# 충돌 해결 후
git add yarn.lock
git rebase --continue
git push --force-with-lease&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 더 간단하게:&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@dependabot rebase&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PR에 이 코멘트를 달면 Dependabot이 자동으로 리베이스해줘요!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설정 커스터마이징 팁&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  그룹화로 PR 수 줄이기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS SDK처럼 관련 패키지들이 많다면 그룹화하세요:&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;groups:
  aws-sdk:
    patterns:
      - &quot;@aws-sdk/*&quot;
    update-types:
      - &quot;minor&quot;
      - &quot;patch&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 AWS SDK 관련 업데이트가 하나의 PR로 묶여요!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  특정 패키지 제외하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 있는 패키지는 제외할 수 있어요:&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;ignore:
  - dependency-name: &quot;problematic-package&quot;
  - dependency-name: &quot;legacy-lib&quot;
    update-types: [&quot;version-update:semver-major&quot;]&lt;/code&gt;&lt;/pre&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;pre class=&quot;vim&quot;&gt;&lt;code&gt;allow:
  - dependency-type: &quot;direct&quot;
    update-types: [&quot;security&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;우리가 배운 것들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&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;b&gt;Yarn 사용해도 &lt;code&gt;package-ecosystem: &quot;npm&quot;&lt;/code&gt;으로 설정&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dependabot이 &lt;code&gt;yarn.lock&lt;/code&gt; 파일을 보고 자동으로 Yarn 명령어 사용해요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CODEOWNERS 파일과 연동&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;package.json&lt;/code&gt; 변경 시 자동으로 리뷰어 할당&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GitHub CLI 적극 활용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 브라우저보다 CLI가 훨씬 빨라요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  우리의 성과&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;총 처리한 PR&lt;/b&gt;: 62개&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 업데이트 패키지&lt;/b&gt;: @aws-sdk/&lt;i&gt;, @types/&lt;/i&gt;, NestJS 관련&lt;/li&gt;
&lt;li&gt;&lt;b&gt;평균 처리 시간&lt;/b&gt;: PR당 5분 이내&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안 업데이트 대응&lt;/b&gt;: 당일 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 묻는 질문들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q: PR이 너무 많이 생성돼요!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;A&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;code&gt;interval: &quot;weekly&quot;&lt;/code&gt;로 변경&lt;/li&gt;
&lt;li&gt;&lt;code&gt;open-pull-requests-limit: 5&lt;/code&gt;로 줄이기&lt;/li&gt;
&lt;li&gt;그룹화 설정 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q: 자동 병합이 안 돼요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;A&lt;/b&gt;: 저희도 현재는 자동 승인만 하고 수동 병합해요. 테스트 환경이 완전히 구축되면 auto-merge를 활성화할 예정입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q: Major 업데이트가 무서워요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;A&lt;/b&gt;: 맞아요! 저희도 조심스럽게 처리해요:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Release Notes 꼼꼼히 읽기&lt;/li&gt;
&lt;li&gt;로컬에서 테스트&lt;/li&gt;
&lt;li&gt;스테이징 환경에서 검증&lt;/li&gt;
&lt;li&gt;문제없으면 배포&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dependabot 도입 후 저희 팀의 의존성 관리가 정말 편해졌어요. 특히:&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;: 수동 체크 시간이 90% 줄었어요&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;p data-ke-size=&quot;size16&quot;&gt;여러분도 Dependabot을 도입해서 더 중요한 개발 업무에 집중하세요!&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;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;# PR 목록 보기
gh pr list --author &quot;app/dependabot&quot; --state open

# PR 상세 정보
gh pr view &amp;lt;PR_NUMBER&amp;gt;

# PR 승인 및 병합
gh pr review &amp;lt;PR_NUMBER&amp;gt; --approve
gh pr merge &amp;lt;PR_NUMBER&amp;gt; --squash

# PR 닫기
gh pr close &amp;lt;PR_NUMBER&amp;gt;

# 테스트 상태 확인
gh pr checks &amp;lt;PR_NUMBER&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 글이 도움이 되셨나요?&lt;/b&gt; 댓글로 여러분의 Dependabot 경험도 공유해주세요!  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;이 글은 저희 팀의 실제 운영 경험을 바탕으로 작성되었습니다.&lt;/i&gt;&lt;/p&gt;</description>
      <category>  Dev Tools &amp;amp; Environment/  Version Control</category>
      <author>KilPenguin</author>
      <guid isPermaLink="true">https://penguin-dev.tistory.com/36</guid>
      <comments>https://penguin-dev.tistory.com/36#entry36comment</comments>
      <pubDate>Wed, 9 Jul 2025 07:24:31 +0900</pubDate>
    </item>
    <item>
      <title>BigQuery Storage Read API와 DATE 타입 불일치 해결하기</title>
      <link>https://penguin-dev.tistory.com/35</link>
      <description>&lt;h1&gt;BigQuery Storage Read API와 DATE 타입 불일치 해결하기&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l30zG/btsO0vconWq/gjyNLSeqjh8ulKEFGHS5HK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l30zG/btsO0vconWq/gjyNLSeqjh8ulKEFGHS5HK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l30zG/btsO0vconWq/gjyNLSeqjh8ulKEFGHS5HK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl30zG%2FbtsO0vconWq%2FgjyNLSeqjh8ulKEFGHS5HK%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;615&quot; height=&quot;410&quot; data-origin-width=&quot;1536&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;최근 프로젝트에서 BigQuery의 성능 개선을 위해 Storage Read API를 도입하는 과정에서 흥미로운 문제를 만났습니다. GoogleSQL의 DATE 타입과 Apache Avro의 DATE 타입 간의 불일치로 인해 예상치 못한 데이터 변환 이슈가 발생했는데요, 이를 해결하는 과정을 공유해보려 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 Storage Read API인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BigQuery에서 데이터를 읽는 방법은 크게 두 가지가 있습니다:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;Legacy API&lt;/th&gt;
&lt;th&gt;Storage Read API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;통신 방식&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;REST 기반&lt;/td&gt;
&lt;td&gt;gRPC 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;데이터 형식&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;Apache Avro/Arrow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;처리 방식&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;순차적 페이지네이션&lt;/td&gt;
&lt;td&gt;병렬 스트림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;성능&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;기준&lt;/td&gt;
&lt;td&gt;약 20-30% 개선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;메모리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;기준&lt;/td&gt;
&lt;td&gt;상당한 절약&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Storage Read API는 특히 대용량 데이터 처리에서 뛰어난 성능을 보여주는데, 바이너리 직렬화와 병렬 스트림 처리가 핵심입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Apache Avro를 선택한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BigQuery Storage Read API는 Apache Avro와 Apache Arrow 두 가지 직렬화 포맷을 지원합니다. 저희가 Avro를 선택한 이유는:&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;: Node.js 환경에서 안정적인 라이브러리 지원&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네트워크 효율성&lt;/b&gt;: 컴팩트한 바이너리 형식으로 전송량 최소화&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 발견: 날짜가 숫자로?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Storage Read API를 적용하고 테스트를 돌려보니 예상치 못한 결과가 나왔습니다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;// Legacy API 결과
console.log('Legacy API date:', result.event_date.value); // &quot;2025-06-29&quot;

// Storage Read API 결과  
console.log('Storage Read API date:', result.event_date); // 20268&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;날짜가 문자열이 아닌 숫자로 나오는 것이었습니다!  &lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;근본 원인 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 원인은 두 API 간의 DATE 타입 표현 방식 차이였습니다:&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;Legacy API&lt;/b&gt;: BigQueryDate 객체 &lt;code&gt;{value: &quot;YYYY-MM-DD&quot;}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Storage Read API&lt;/b&gt;: Avro DATE 타입으로 1970-01-01부터의 일수 (epoch days)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;20268&lt;/code&gt;이라는 숫자는 1970년 1월 1일부터 20,268일 후인 2025년 6월 29일을 의미하는 것이었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 과정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계: DATE 필드 자동 감지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 AVRO 스키마에서 DATE 타입 필드를 자동으로 찾는 함수를 만들었습니다:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;/**
 * AVRO 스키마에서 DATE 타입 필드인지 확인
 */
function isAvroDateField(fieldType: unknown): boolean {
  // Union type 처리 ([&quot;null&quot;, { &quot;type&quot;: &quot;int&quot;, &quot;logicalType&quot;: &quot;date&quot; }])
  if (Array.isArray(fieldType)) {
    return fieldType.some((type) =&amp;gt; isAvroDateField(type));
  }

  // Record type 처리 ({ type: &quot;int&quot;, logicalType: &quot;date&quot; })
  if (typeof fieldType === 'object' &amp;amp;&amp;amp; fieldType !== null) {
    const field = fieldType as { type?: string; logicalType?: string };
    return field.type === 'int' &amp;amp;&amp;amp; field.logicalType === 'date';
  }

  return false;
}

/**
 * AVRO 스키마에서 모든 DATE 필드 찾기
 */
function findDateFieldsInAvroSchema(schema: any): Set&amp;lt;string&amp;gt; {
  const dateFields = new Set&amp;lt;string&amp;gt;();

  function traverse(schemaNode: any): void {
    if (schemaNode?.type === 'record' &amp;amp;&amp;amp; schemaNode?.fields) {
      schemaNode.fields.forEach((field: any) =&amp;gt; {
        if (isAvroDateField(field.type)) {
          dateFields.add(field.name);
        }
        traverse(field.type);
      });
    } else if (Array.isArray(schemaNode)) {
      schemaNode.forEach(item =&amp;gt; traverse(item));
    }
  }

  traverse(schema);
  return dateFields;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계: Epoch Days 변환 유틸리티&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 epoch days를 BigQuery DATE 형식으로 변환하는 함수를 구현했습니다:&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const UNIX_EPOCH = new Date('1970-01-01T00:00:00.000Z');
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;

/**
 * Epoch days를 YYYY-MM-DD 문자열로 변환
 */
function convertEpochDaysToDateString(epochDays: number): string {
  const date = new Date(
    UNIX_EPOCH.getTime() + epochDays * MILLISECONDS_PER_DAY
  );
  return date.toISOString().split('T')[0];
}

/**
 * Epoch days를 BigQueryDate 객체로 변환
 */
function convertEpochDaysToBigQueryDate(epochDays: number) {
  const dateString = convertEpochDaysToDateString(epochDays);
  return { value: dateString };
}

/**
 * 데이터에서 DATE 필드들을 BigQueryDate 형태로 변환
 */
function convertDateFieldsInData&amp;lt;T&amp;gt;(data: T, dateFields: Set&amp;lt;string&amp;gt;): T {
  if (Array.isArray(data)) {
    return data.map(item =&amp;gt; convertDateFieldsInData(item, dateFields)) as T;
  } 

  if (data &amp;amp;&amp;amp; typeof data === 'object' &amp;amp;&amp;amp; data !== null) {
    const newObj: Record&amp;lt;string, unknown&amp;gt; = {};

    for (const [key, value] of Object.entries(data)) {
      if (dateFields.has(key) &amp;amp;&amp;amp; typeof value === 'number') {
        newObj[key] = convertEpochDaysToBigQueryDate(value);
      } else {
        newObj[key] = convertDateFieldsInData(value, dateFields);
      }
    }

    return newObj as T;
  }

  return data;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계: 필드명 매핑 문제 해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 발견한 문제가 하나 더 있었습니다. &lt;code&gt;30d&lt;/code&gt;, &lt;code&gt;1d&lt;/code&gt; 같은 숫자로 시작하는 필드명이 &lt;code&gt;_30d&lt;/code&gt;, &lt;code&gt;_1d&lt;/code&gt;로 변환되는 것이었습니다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;/**
 * 숫자로 시작하는 필드명에 _ prefix 추가
 */
function fixAvroSchemaFieldNames(schema: any): any {
  if (schema?.type === 'record' &amp;amp;&amp;amp; schema?.fields) {
    const newFields = schema.fields.map((field: any) =&amp;gt; {
      const newField = { ...field };
      if (/^\d/.test(newField.name)) {
        newField.name = `_${newField.name}`;
      }
      newField.type = fixAvroSchemaFieldNames(newField.type);
      return newField;
    });
    return { ...schema, fields: newFields };
  }

  if (Array.isArray(schema)) {
    return schema.map(item =&amp;gt; fixAvroSchemaFieldNames(item));
  }

  return schema;
}

/**
 * 변환된 필드명을 원래대로 복원
 */
function restoreOriginalFieldNames&amp;lt;T&amp;gt;(data: T): T {
  if (Array.isArray(data)) {
    return data.map(item =&amp;gt; restoreOriginalFieldNames(item)) as T;
  }

  if (data &amp;amp;&amp;amp; typeof data === 'object' &amp;amp;&amp;amp; data !== null) {
    const newObj: Record&amp;lt;string, unknown&amp;gt; = {};

    for (const [key, value] of Object.entries(data)) {
      let newKey = key;
      if (key.startsWith('_') &amp;amp;&amp;amp; /^\d/.test(key.substring(1))) {
        newKey = key.substring(1);
      }
      newObj[newKey] = restoreOriginalFieldNames(value);
    }

    return newObj as T;
  }

  return data;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4단계: 통합 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 변환 로직을 통합한 Storage Read API 구현:&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;async function readTableWithStorageApi(params: {
  tableName: string;
  selectedFields?: string[];
  rowRestriction?: string;
}): Promise&amp;lt;unknown[]&amp;gt; {
  const { tableName, selectedFields, rowRestriction } = params;

  try {
    // 1. Read Session 생성
    const [session] = await bigqueryReadClient.createReadSession({
      parent: `projects/${projectId}`,
      readSession: {
        table: `projects/${projectId}/datasets/dataset/tables/${tableName}`,
        dataFormat: 'AVRO',
        readOptions: {
          ...(selectedFields &amp;amp;&amp;amp; { selectedFields }),
          ...(rowRestriction &amp;amp;&amp;amp; { rowRestriction }),
        },
      },
      maxStreamCount: 1,
    });

    if (!session.streams?.length) {
      return [];
    }

    // 2. Avro 스키마 파싱 및 DATE 필드 감지
    let schema = JSON.parse(session.avroSchema.schema);
    const dateFields = findDateFieldsInAvroSchema(schema);

    // 필드명 변환 적용
    schema = fixAvroSchemaFieldNames(schema);
    const avroType = avro.Type.forSchema(schema);

    // 3. 스트리밍 데이터 읽기
    const results: unknown[] = [];

    for (const stream of session.streams) {
      const readStream = bigqueryReadClient.readRows({
        readStream: stream.name,
        offset: 0,
      });

      await new Promise&amp;lt;void&amp;gt;((resolve, reject) =&amp;gt; {
        readStream
          .on('error', reject)
          .on('data', (response: any) =&amp;gt; {
            if (response.avroRows?.serializedBinaryRows) {
              try {
                let pos;
                do {
                  const decodedData = avroType.decode(
                    Buffer.from(response.avroRows.serializedBinaryRows),
                    pos
                  );
                  if (decodedData.value) {
                    results.push(decodedData.value);
                  }
                  pos = decodedData.offset;
                } while (pos &amp;gt; 0);
              } catch (error) {
                console.error('Decode error:', error);
              }
            }
          })
          .on('end', resolve);
      });
    }

    // 4. DATE 필드 변환 및 필드명 복원
    const convertedResults = convertDateFieldsInData(results, dateFields);
    const restoredResults = restoreOriginalFieldNames(convertedResults);

    return restoredResults;
  } catch (error) {
    console.error('Storage Read API error:', error);
    throw error;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5단계: 안전한 Fallback 패턴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 Storage Read API 실패 시 Legacy API로 자동 전환하는 안전장치를 구현했습니다:&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;async function queryBigQueryData(params: QueryParams): Promise&amp;lt;unknown[]&amp;gt; {
  // Storage Read API 우선 시도
  try {
    console.log('[STORAGE-READ] Attempting to use Storage Read API');
    return await readTableWithStorageApi(params);
  } catch (error) {
    console.error('[STORAGE-READ] Failed:', error);
    console.warn('[FALLBACK] Using legacy BigQuery API');

    // Legacy API로 자동 전환
    return await queryWithLegacyApi(params);
  }
}

async function queryWithLegacyApi(params: QueryParams): Promise&amp;lt;unknown[]&amp;gt; {
  const query = `SELECT * FROM \`${params.tableName}\` WHERE ${params.rowRestriction}`;
  const [rows] = await bigquery.query(query);
  return rows;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 및 검증&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현한 솔루션을 검증하기 위해 단위 테스트를 작성했습니다:&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;describe('BigQuery Storage Read API Date 변환', () =&amp;gt; {
  test('Epoch days를 날짜 문자열로 변환', () =&amp;gt; {
    const epochDays = 20268; // 2025-06-29
    const result = convertEpochDaysToDateString(epochDays);
    expect(result).toBe('2025-06-29');
  });

  test('BigQueryDate 객체로 변환', () =&amp;gt; {
    const epochDays = 20268;
    const result = convertEpochDaysToBigQueryDate(epochDays);
    expect(result).toEqual({ value: '2025-06-29' });
  });

  test('AVRO DATE 필드 식별', () =&amp;gt; {
    const dateFieldType = { type: 'int', logicalType: 'date' };
    const stringFieldType = 'string';

    expect(isAvroDateField(dateFieldType)).toBe(true);
    expect(isAvroDateField(stringFieldType)).toBe(false);
  });

  test('필드명 복원', () =&amp;gt; {
    const avroData = [{ _30d: 100, _1d: 50, name: 'test' }];
    const result = restoreOriginalFieldNames(avroData);
    expect(result[0]).toEqual({ '30d': 100, '1d': 50, name: 'test' });
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능 개선 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 다음과 같은 성능 개선을 달성했습니다:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;Legacy API&lt;/th&gt;
&lt;th&gt;Storage Read API&lt;/th&gt;
&lt;th&gt;개선율&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;응답 시간&lt;/td&gt;
&lt;td&gt;5.3초&lt;/td&gt;
&lt;td&gt;4.1초&lt;/td&gt;
&lt;td&gt;23% &amp;uarr;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메모리 사용량&lt;/td&gt;
&lt;td&gt;192MB&lt;/td&gt;
&lt;td&gt;절약됨&lt;/td&gt;
&lt;td&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;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&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. 자동 DATE 필드 감지&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AVRO 스키마를 파싱하여 &lt;code&gt;logicalType: &quot;date&quot;&lt;/code&gt; 필드 자동 식별&lt;/li&gt;
&lt;li&gt;Union 타입과 Record 타입 모두 지원하는 재귀적 탐색&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 정확한 Epoch Days 변환&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UTC 기준 1970-01-01부터의 일수 계산&lt;/li&gt;
&lt;li&gt;BigQueryDate 객체 형식으로 변환하여 기존 코드와 호환성 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 필드명 매핑 처리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;숫자로 시작하는 필드명의 &lt;code&gt;_&lt;/code&gt; 접두사 자동 처리&lt;/li&gt;
&lt;li&gt;원본 필드명으로 복원하여 기존 코드 영향 최소화&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 안정적인 Fallback&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Storage Read API 실패 시 자동으로 Legacy API 사용&lt;/li&gt;
&lt;li&gt;서비스 중단 없이 점진적 마이그레이션 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발자를 위한 권장사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 데이터 타입 사전 검증&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 API 도입 시 다음 사항을 미리 확인하세요:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DATE, TIMESTAMP 등 복잡한 타입의 실제 반환 형태&lt;/li&gt;
&lt;li&gt;숫자로 시작하는 필드명의 변환 여부&lt;/li&gt;
&lt;li&gt;NULL 값 처리 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 점진적 도입 전략&lt;/h3&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;// 환경 변수로 API 방식 제어
const useStorageReadApi = process.env.USE_STORAGE_READ_API === 'true';

async function queryData(params: QueryParams) {
  if (useStorageReadApi) {
    try {
      return await readTableWithStorageApi(params);
    } catch (error) {
      console.warn('Storage Read API 실패, Legacy API로 전환');
      return await queryWithLegacyApi(params);
    }
  }

  return await queryWithLegacyApi(params);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 충분한 테스트와 모니터링&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 성능 모니터링
const startTime = Date.now();
const result = await queryData(params);
const duration = Date.now() - startTime;

console.log(`쿼리 실행 시간: ${duration}ms`);
console.log(`반환된 로우 수: ${result.length}`);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BigQuery Storage Read API 도입 과정에서 만난 DATE 타입 불일치 문제는 처음에는 당황스러웠지만, 차근차근 분석하고 해결해나가는 과정에서 많은 것을 배울 수 있었습니다.&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;p data-ke-size=&quot;size16&quot;&gt;이런 경험이 비슷한 문제를 겪고 있는 다른 개발자들에게 도움이 되기를 바랍니다. 혹시 질문이나 더 나은 해결 방법이 있다면 언제든 댓글로 공유해주세요!  &lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&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;a href=&quot;https://cloud.google.com/bigquery/docs/reference/storage&quot;&gt;BigQuery Storage Read API 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://avro.apache.org/docs/1.11.3/specification/&quot;&gt;Apache Avro 스펙&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types&quot;&gt;BigQuery 데이터 타입&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>⚡ Performance &amp;amp; Optimization/ ️ Database Tuning</category>
      <author>KilPenguin</author>
      <guid isPermaLink="true">https://penguin-dev.tistory.com/35</guid>
      <comments>https://penguin-dev.tistory.com/35#entry35comment</comments>
      <pubDate>Wed, 2 Jul 2025 07:19:29 +0900</pubDate>
    </item>
    <item>
      <title>Tistory에서 Mermaid 다이어그램 다크모드 자동 전환 구현하기</title>
      <link>https://penguin-dev.tistory.com/34</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7Xaai/btsOWJW3ve7/v4NT0kIik7kB6vovhKkyWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7Xaai/btsOWJW3ve7/v4NT0kIik7kB6vovhKkyWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7Xaai/btsOWJW3ve7/v4NT0kIik7kB6vovhKkyWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7Xaai%2FbtsOWJW3ve7%2Fv4NT0kIik7kB6vovhKkyWk%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;467&quot; height=&quot;467&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;Tistory에서 Mermaid 다이어그램 다크모드 자동 전환 구현하기&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;pronist/hello 스킨과 완벽하게 동기화되는 Mermaid 다이어그램 만들기&quot;&lt;/p&gt;
&lt;/blockquote&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;다이어그램&lt;/b&gt;은 필수입니다. 복잡한 아키텍처를 설명하거나 플로우차트를 그릴 때 &lt;b&gt;Mermaid&lt;/b&gt;만큼 편리한 도구도 없죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;다크모드&lt;/b&gt;가 대세가 된 지금, 한 가지 문제가 있었습니다:&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;  사용자가 다크모드로 전환
  다이어그램은 여전히 라이트 테마
  눈이 아픈 상황 발생...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &lt;b&gt;Tistory + pronist/hello 스킨&lt;/b&gt;을 사용하는 환경에서는 더욱 복잡했습니다. 스킨의 다크모드 토글과 Mermaid 테마가 따로 놀았거든요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황 분석&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Tistory의 코드 블록 제약&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tistory에서는 Mermaid를 직접 지원하지 않습니다. 대신 &lt;b&gt;코드 블록&lt;/b&gt;을 사용해서 우회해야 하죠:&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- Tistory에서 작성하는 방식 --&amp;gt;
&amp;lt;pre class=&quot;delphi&quot;&amp;gt;&amp;lt;code&amp;gt;
graph TD
    A[시작] --&amp;gt; B[처리]
    B --&amp;gt; C[완료]
&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드 블록을 JavaScript로 &lt;b&gt;동적 변환&lt;/b&gt;해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. pronist/hello 스킨의 다크모드 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;pronist/hello&lt;/b&gt; 스킨은 Tailwind CSS 기반으로 다크모드를 구현합니다:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;// 다크모드 감지 방법
document.documentElement.classList.contains('dark') // true/false&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;&lt;b&gt;다크모드 ON&lt;/b&gt;: &lt;code&gt;&amp;lt;html class=&quot;dark&quot;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다크모드 OFF&lt;/b&gt;: &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; (클래스 없음)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Mermaid 재렌더링의 함정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 까다로운 부분은 &lt;b&gt;다크모드 토글 시 재렌더링&lt;/b&gt;이었습니다:&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;// ❌ 잘못된 방법 - 문법 에러 발생
element.innerHTML = element.textContent; // SVG &amp;rarr; 텍스트 변환 실패&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 렌더링된 SVG를 다시 텍스트로 변환하려고 하면 문법 에러가 발생합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결책: 3단계 전략&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계: 원본 코드 안전 보관&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// 원본 Mermaid 코드를 두 곳에 저장
const originalMermaidCode = new Map();

function convertCodeBlocksToMermaid() {
  const elements = document.querySelectorAll('pre.delphi');

  for (const element of elements) {
    const codeElement = element.querySelector('code');
    const originalCode = codeElement.textContent.trim();

    const div = document.createElement('div');
    div.innerHTML = originalCode;
    div.className = 'mermaid';

    //   핵심: 원본 코드를 안전하게 보관
    div.setAttribute('data-original-code', originalCode);
    originalMermaidCode.set(div, originalCode);

    // DOM 교체
    const pre = codeElement.parentElement;
    pre.style.textAlign = 'center';
    pre.removeChild(codeElement);
    pre.appendChild(div);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 아이디어&lt;/b&gt;: 원본 텍스트를 HTML 속성과 JavaScript Map 두 곳에 저장해서 &lt;b&gt;이중 보안&lt;/b&gt;을 확보합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계: 스마트한 다크모드 감지&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function detectHelloDarkMode() {
  return document.documentElement.classList.contains('dark');
}

function initMermaid() {
  const isDark = detectHelloDarkMode();

  mermaid.initialize({
    theme: isDark ? 'dark' : 'default',
    startOnLoad: false, // 수동 제어
    securityLevel: 'loose'
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;pronist/hello&lt;/b&gt; 스킨의 다크모드 구조에 맞춰 정확히 감지합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계: 안전한 재렌더링&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function reRenderMermaidSafely() {
  const isDark = detectHelloDarkMode();

  // 새로운 테마로 재초기화
  mermaid.initialize({
    theme: isDark ? 'dark' : 'default',
    startOnLoad: false,
    securityLevel: 'loose'
  });

  const mermaidElements = document.querySelectorAll('.mermaid');

  mermaidElements.forEach(element =&amp;gt; {
    //   핵심: 원본 텍스트로 정확히 복원
    const originalCode = element.getAttribute('data-original-code') || 
                        originalMermaidCode.get(element);

    if (originalCode) {
      element.removeAttribute('data-processed');
      element.innerHTML = originalCode; // SVG가 아닌 원본 텍스트
    }
  });

  mermaid.init(); // 새로운 테마로 재렌더링
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심&lt;/b&gt;: SVG로 렌더링된 결과가 아닌 &lt;b&gt;원본 텍스트&lt;/b&gt;로 복원해서 재렌더링합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;완전한 구현 코드&lt;/h2&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- Tistory 스킨 HTML 편집 &amp;gt; head 태그 마지막 부분에 추가 --&amp;gt;
&amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
(function() {
  const originalMermaidCode = new Map();

  function detectHelloDarkMode() {
    return document.documentElement.classList.contains('dark');
  }

  function initMermaid() {
    const isDark = detectHelloDarkMode();

    mermaid.initialize({
      theme: isDark ? 'dark' : 'default',
      startOnLoad: false,
      securityLevel: 'loose'
    });
  }

  function convertCodeBlocksToMermaid() {
    const elements = document.querySelectorAll('pre.delphi');

    for (const element of elements) {
      const codeElement = element.querySelector('code');
      if (!codeElement) continue;

      const originalCode = codeElement.textContent.trim();

      const div = document.createElement('div');
      div.innerHTML = originalCode;
      div.className = 'mermaid';
      div.setAttribute('data-original-code', originalCode);

      originalMermaidCode.set(div, originalCode);

      const pre = codeElement.parentElement;
      pre.style.textAlign = 'center';
      pre.removeChild(codeElement);
      pre.appendChild(div);
    }
  }

  function reRenderMermaidSafely() {
    const isDark = detectHelloDarkMode();

    mermaid.initialize({
      theme: isDark ? 'dark' : 'default',
      startOnLoad: false,
      securityLevel: 'loose'
    });

    const mermaidElements = document.querySelectorAll('.mermaid');

    mermaidElements.forEach(element =&amp;gt; {
      const originalCode = element.getAttribute('data-original-code') || 
                          originalMermaidCode.get(element);

      if (originalCode) {
        element.removeAttribute('data-processed');
        element.innerHTML = originalCode;
      }
    });

    mermaid.init();
  }

  function watchThemeChanges() {
    let timeoutId;

    const observer = new MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
        if (mutation.type === 'attributes' &amp;amp;&amp;amp; 
            mutation.attributeName === 'class' &amp;amp;&amp;amp;
            mutation.target === document.documentElement) {

          clearTimeout(timeoutId);
          timeoutId = setTimeout(() =&amp;gt; {
            reRenderMermaidSafely();
          }, 150);
        }
      });
    });

    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['class']
    });
  }

  function initialize() {
    try {
      initMermaid();
      convertCodeBlocksToMermaid();
      mermaid.init();
      watchThemeChanges();

      console.log('Mermaid 다크모드 자동 전환 활성화 완료');
    } catch (error) {
      console.error('Mermaid 초기화 실패:', error);
    }
  }

  document.addEventListener('DOMContentLoaded', initialize);
})();
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Tistory 스킨 HTML 편집&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드를 &lt;b&gt;스킨 편집 &amp;gt; HTML 편집&lt;/b&gt;에서 &lt;code&gt;&amp;lt;/head&amp;gt;&lt;/code&gt; 태그 직전에 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;: &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt; 태그 직전에 추가해도 동일하게 작동하지만, head 태그에 넣는 것이 더 안정적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 글 작성 시&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;```delphi
graph TD
    A[Lambda 함수] --&amp;gt; B[RDS Proxy]
    B --&amp;gt; C[데이터베이스]
    C --&amp;gt; D[응답]&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;
**delphi** 언어로 코드 블록을 작성하면 자동으로 Mermaid 다이어그램으로 변환됩니다.

### 3. 자동 동작 확인

- 페이지 로드 시: 현재 테마에 맞는 다이어그램 표시
- 다크모드 토글 시: 0.15초 후 자동으로 테마 변경

## 기술적 하이라이트

### 1. **이중 보안 저장**
```javascript
div.setAttribute('data-original-code', originalCode); // HTML 속성
originalMermaidCode.set(div, originalCode);           // JavaScript Map&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. &lt;b&gt;디바운스 적용&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;clearTimeout(timeoutId);
timeoutId = setTimeout(() =&amp;gt; {
  reRenderMermaidSafely();
}, 150);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연속된 토글을 방지해서 성능을 최적화합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. &lt;b&gt;MutationObserver 활용&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;observer.observe(document.documentElement, {
  attributes: true,
  attributeFilter: ['class']
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DOM 변경을 실시간으로 감지해서 즉시 반응합니다.&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;b&gt;pronist/hello&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;✅ Mermaid 다이어그램이 자동으로 테마 변경&lt;/li&gt;
&lt;li&gt;✅ 문법 에러 없는 안전한 재렌더링&lt;/li&gt;
&lt;li&gt;✅ 0.15초의 부드러운 전환 효과&lt;/li&gt;
&lt;li&gt;✅ 원본 코드 완벽 보존&lt;/li&gt;
&lt;/ul&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;pronist/hello&lt;/b&gt; 외의 다른 스킨에서 사용하려면 다크모드 감지 부분만 수정하면 됩니다:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function detectDarkMode() {
  // 각 스킨에 맞게 수정
  return document.body.classList.contains('dark-theme') ||
         document.documentElement.classList.contains('dark') ||
         window.matchMedia('(prefers-color-scheme: dark)').matches;
}&lt;/code&gt;&lt;/pre&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;사용자 경험&lt;/b&gt;은 정말 중요합니다. 다크모드를 사용하는 독자가 밝은 다이어그램 때문에 눈이 아프다면, 아무리 좋은 내용이라도 읽기 힘들겠죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구현으로 &lt;b&gt;Tistory + Mermaid + 다크모드&lt;/b&gt;의 완벽한 조합을 만들 수 있었습니다. 여러분의 기술 블로그도 더욱 전문적이고 사용자 친화적으로 만들어보세요!&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비슷한 문제를 겪고 계신 분들께&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 블로그 플랫폼이나 스킨에서도 비슷한 방식으로 구현 가능합니다. 궁금한 점이 있으시면 댓글로 남겨주세요!&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;이 글은 실제 Tistory 블로그 운영 경험을 바탕으로 작성되었습니다.&lt;/i&gt;&lt;/p&gt;</description>
      <category>  Dev Tools &amp;amp; Environment/  Documentation Tools</category>
      <author>KilPenguin</author>
      <guid isPermaLink="true">https://penguin-dev.tistory.com/34</guid>
      <comments>https://penguin-dev.tistory.com/34#entry34comment</comments>
      <pubDate>Tue, 1 Jul 2025 22:00:58 +0900</pubDate>
    </item>
    <item>
      <title>하루 4번 서버가 죽는 사내 어드민, Lambda로 구원받다</title>
      <link>https://penguin-dev.tistory.com/33</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lbxfv/btsOY0QJ4Cw/pLsgiq4gCFYLnKlNnTWGDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lbxfv/btsOY0QJ4Cw/pLsgiq4gCFYLnKlNnTWGDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lbxfv/btsOY0QJ4Cw/pLsgiq4gCFYLnKlNnTWGDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flbxfv%2FbtsOY0QJ4Cw%2FpLsgiq4gCFYLnKlNnTWGDk%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;326&quot; height=&quot;326&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;사내 어드민의 딜레마 - t4g.small의 한계&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;하루 4번 서버가 죽는 이유를 찾아서&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프롤로그: 평범한 월요일 오전의 악몽&lt;/h2&gt;
&lt;pre class=&quot;delphi&quot; data-ke-language=&quot;delphi&quot;&gt;&lt;code&gt;flowchart LR
    %% ─── Google Cloud ───
    subgraph Google Cloud
        BQ[&quot;BigQuery\n(360일 지표)&quot;]
        DP[&quot;Dataflow\n배치 파이프라인&quot;]
        MT[&quot;Metrics Table\n파티션 테이블&quot;]
    end

    %% ─── Admin VPC ───
    subgraph Admin VPC
        API[&quot;NestJS Admin API&quot;]
        UI[&quot;어드민\nDashboard&quot;]
    end

    Scheduler[&quot;Cloud Scheduler\n하루 4회 트리거&quot;]

    %% ─── Edges ───
    Scheduler --&amp;gt; DP
    BQ --&amp;gt; DP
    DP --&amp;gt; MT
    MT --&amp;gt; API
    API --&amp;gt; UI

    %% ─── Styles ───
    classDef gcp   fill:#e8f4ff,stroke:#1a73e8,color:#1a73e8;
    classDef admin fill:#fff7e6,stroke:#ffb300,color:#ff6f00;
    class BQ,DP,MT gcp
    class API,UI admin&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마케팅 프로젝트는 전형적인 &lt;b&gt;사내 어드민 시스템&lt;/b&gt;입니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;  트래픽 패턴
- 평일 09:00-18:00: 마케팅 팀 활발 사용
- 평일 18:00-09:00: 거의 사용 없음
- 주말: 완전히 조용함

  비용 최적화 전략  
- t4g.small (2 vCPU, 2GB RAM) - $13/월
- 오토스케일링 최소화
- 불필요한 리소스 제거&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평상시에는 문제없었습니다. 하지만 &lt;b&gt;새로운 요구사항&lt;/b&gt;이 들어왔습니다:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;  BigQuery 마케팅 메트릭 동기화
- 360일치 마케팅 지표 데이터 처리
- 하루 4번 실행 (3:30, 9:30, 15:30, 21:30)
- 기존 스케줄링 시스템에 추가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;b&gt;매일 4번&lt;/b&gt;, 조용하던 서버에 지옥이 펼쳐지기 시작했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;하루 4번의 지옥 스케줄&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;  구글 빅쿼리 일일 마케팅 메트릭 동기화
- 새벽 03:30   (조용한 시간)
- 오전 09:30 ☀️ (출근 직후 - 업무 시작!)
- 오후 15:30 ☀️ (캠페인 최적화 골든타임!)  
- 밤   21:30   (퇴근 후)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;최악의 타이밍:&lt;/b&gt; 오전 9:30과 오후 3:30은 &lt;b&gt;마케팅 팀이 가장 활발하게 사용하는 시간&lt;/b&gt;이었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3가지 지옥의 증상들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 메모리 부족 지옥&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8Nt7H/btsOXIDHWw8/MsEYqI8huALUG5MWdjg1UK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8Nt7H/btsOXIDHWw8/MsEYqI8huALUG5MWdjg1UK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8Nt7H/btsOXIDHWw8/MsEYqI8huALUG5MWdjg1UK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8Nt7H%2FbtsOXIDHWw8%2FMsEYqI8huALUG5MWdjg1UK%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;490&quot; height=&quot;490&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;//   하루 4번의 메모리 학살
@Cron('30 3,9,15,21 * * *') // 3:30, 9:30, 15:30, 21:30
async syncBigQueryMetrics() {
  const today = moment().clone().startOf('day');
  const DAYS_TO_SYNC = 60; // 60일치 데이터 처리

  // 60일치 날짜 배열 생성
  const dates: string[] = Array.from({ length: DAYS_TO_SYNC }, (_, i) =&amp;gt;
    today.clone().subtract(i, 'day').format('YYYY-MM-DD'),
  );

  // 2GB RAM으로 60일치 BigQuery 작업을 한 번에 처리...
  await Promise.all(dates.map(date =&amp;gt; 
    this.processBigQueryData(date) // 즉시 OOM  
  ));
}&lt;/code&gt;&lt;/pre&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;t4g.small (2GB RAM)&lt;/b&gt;이 60일치 데이터를 감당 못함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BigQuery 결과 로딩 시 즉시 OOM&lt;/b&gt; 발생&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하루 4번&lt;/b&gt; 메모리 부족으로 프로세스 강제 종료&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ARM Graviton2 CPU 100% 점유&lt;/b&gt;로 다른 기능 완전 마비&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제 상황:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;09:30 - 마케팅 팀 출근 직후 &amp;rarr; 서버 다운  
15:30 - 캠페인 최적화 시간 &amp;rarr; 서버 다운  

&quot;t4g.small로 BigQuery 데이터 처리는 불가능!&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 업무 시간 마비 지옥&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;//   마케팅 팀의 절망적인 상황
// &quot;오전 9:30마다 어드민이 먹통이 돼요!&quot;
// &quot;오후 3:30에 캠페인 수정하려는데 서버가 죽어있어요!&quot;

// 최악의 타이밍
오전 09:30 - 출근 후 업무 시작 시간  
    &amp;darr;
서버 CPU 100%, 어드민 접속 불가 (30분간)
    &amp;darr;
오후 15:30 - 캠페인 최적화 골든타임    
    &amp;darr;
또 다시 서버 마비... (30분간)&lt;/code&gt;&lt;/pre&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;/li&gt;
&lt;li&gt;캠페인 최적화 기회 상실&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사내 어드민의 존재 이유&lt;/b&gt; 자체가 위협받음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 연쇄 실패 지옥&lt;/h3&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Cron('30 3,9,15,21 * * *') // 하루 4번 실행
async processAllSnapshots() {
  await this.syncGoogleMetrics();          // 45분 소요
  await this.createDailySnapshots();       // 60분 소요  
  await this.aggregateReportData();        // 30분 소요
  // 총 135분 &amp;rarr; 다음 스케줄과 겹침!  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제 상황:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;09:30 시작 &amp;rarr; 11:45 완료 (2시간 15분)
15:30 시작 &amp;rarr; 17:45 완료 (2시간 15분)  
21:30 시작 &amp;rarr; 23:45 완료 (2시간 15분)
03:30 시작 &amp;rarr; 05:45 완료 (2시간 15분)

= 하루 9시간 서버 점유!  &lt;/code&gt;&lt;/pre&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;6시간 간격&lt;/b&gt; 스케줄인데 &lt;b&gt;2시간 이상&lt;/b&gt; 소요&lt;/li&gt;
&lt;li&gt;하나가 실패하면 &lt;b&gt;연쇄적으로 모든 스케줄 지연&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;BigQuery 쿼리 타임아웃으로 &lt;b&gt;전체 동기화 실패&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&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: 서버 사양 업그레이드  &lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;현재: t4g.small (2 vCPU, 2GB RAM) - $13/월
최소 필요: t4g.medium (2 vCPU, 4GB RAM) - $26/월

비용 증가: 월 $13 &amp;rarr; $26 (2배 증가)&lt;/code&gt;&lt;/pre&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;2배 비용 증가&lt;/b&gt;로 사내 어드민 예산 부담&lt;/li&gt;
&lt;li&gt;하루 8시간을 위해 24시간 비용 지불&lt;/li&gt;
&lt;li&gt;2GB &amp;rarr; 4GB RAM 업그레이드해도 60일치 처리는 여전히 버거움&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방안 2: 스케줄링 시간 분산&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// 작업을 여러 시간대로 분산
@Cron('0 1 * * *') // 새벽 1시
async processRecentSnapshots() { /* 최근 30일 */ }

@Cron('0 3 * * *') // 새벽 3시  
async processOldSnapshots() { /* 31-360일 */ }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt; 여전한 스파이크 문제, 복잡한 관리&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방안 3: Lambda + Event-Driven 아키텍처  &lt;/h3&gt;
&lt;pre class=&quot;delphi&quot; data-ke-language=&quot;delphi&quot;&gt;&lt;code&gt;flowchart LR
    Pub[&quot;Publisher\n(NestJS Service)&quot;]
    Bus((&quot;Event Bus\n(SNS &amp;middot; EventBridge)&quot;))

    subgraph AWS Lambda
        L1[&quot;SyncMetrics\nLambda&quot;]
        L2[&quot;CreateSnapshot\nLambda&quot;]
        L3[&quot;AggregateReport\nLambda&quot;]
    end

    DB[&quot;Aurora (MySQL)\n+ RDS Proxy&quot;]

    %% Edges
    Pub --&amp;gt;|도메인 이벤트| Bus
    Bus --&amp;gt; L1
    Bus --&amp;gt; L2
    Bus --&amp;gt; L3
    L1 --&amp;gt; DB
    L2 --&amp;gt; DB
    L3 --&amp;gt; DB

    %% Style
    classDef lambda fill:#edf7ff,stroke:#1e90ff,color:#1e90ff;
    classDef infra  fill:#e9ffed,stroke:#34a853,color:#0b8026;
    class L1,L2,L3 lambda
    class DB infra&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 우리가 선택한 &lt;b&gt;게임 체인저&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;//   Lambda + 이벤트 기반 처리
async updateMarketingData(data: MarketingData) {
  const result = await this.marketingService.update(data);

  // 이벤트 발행 &amp;rarr; Lambda가 필요할 때만 실행
  await this.eventService.publish({
    eventName: EventName.MARKETING_SNAPSHOT_REQUESTED,
    payload: { date: data.date, metrics: data.metrics }
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 Lambda였을까?&lt;/b&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;메모리 문제 완전 해결&lt;/b&gt; &amp;rarr; 자동 스케일링으로 OOM 방지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NestJS DI 그대로 활용&lt;/b&gt; &amp;rarr; 기존 코드 재사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용량 기반 비용&lt;/b&gt; &amp;rarr; 24시간 서버 운영 대비 효율적&lt;/li&gt;
&lt;li&gt;&lt;b&gt;pub-sub 구조&lt;/b&gt; &amp;rarr; 기존 스케줄링 작업들도 전환 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사내 어드민에 Lambda가 완벽한 이유&lt;/h2&gt;
&lt;pre class=&quot;delphi&quot; data-ke-language=&quot;delphi&quot;&gt;&lt;code&gt;flowchart LR
    Pub[&quot;Publisher\n(이벤트 발행)&quot;]
    Bus((&quot;Event Bus&quot;))

    subgraph Subscribers
        Sub1[&quot;Subscriber A&quot;]
        Sub2[&quot;Subscriber B&quot;]
        Sub3[&quot;Subscriber C&quot;]
    end

    Pub --&amp;gt; Bus
    Bus --&amp;gt; Sub1
    Bus --&amp;gt; Sub2
    Bus --&amp;gt; Sub3&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 비용 효율성  &lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;기존 t4g.small (작동 불가): $13/월
t4g.medium 업그레이드 시: $26/월
Lambda 전환 후: $27/월 (Lambda $5 + RDS Proxy $22)

  비용 효과: t4g.medium과 거의 동일하지만 무한 확장성 확보&lt;/code&gt;&lt;/pre&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;Lambda 자체 비용은 매우 저렴&lt;/b&gt;: 월 $5 정도&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RDS Proxy 필수&lt;/b&gt;: Lambda 동시 실행으로 인한 DB 연결 문제 해결&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용량 기반 과금의 위력&lt;/b&gt;: 하루 8시간 vs 24시간 차이&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버 관리 비용 완전 제거&lt;/b&gt;: 운영 부담 대폭 감소&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 기술적 연속성  &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;NestJS DI 그대로 활용&lt;/b&gt; &amp;rarr; 기존 코드 재사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TypeORM 그대로 사용&lt;/b&gt; &amp;rarr; 데이터 레이어 유지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기존 서비스 로직 재활용&lt;/b&gt; &amp;rarr; 개발 시간 단축&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 아키텍처 개선 기회  &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;pub-sub 구조 도입&lt;/b&gt; &amp;rarr; 기존 스케줄링 작업들도 전환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Event-Driven 패턴&lt;/b&gt; &amp;rarr; 확장성과 유지보수성 향상&lt;/li&gt;
&lt;li&gt;&lt;b&gt;마이크로서비스 준비&lt;/b&gt; &amp;rarr; 향후 확장 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&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;b&gt;비용&lt;/b&gt;: t4g.medium과 거의 동일 ($26 &amp;rarr; $27/월)&lt;/li&gt;
&lt;li&gt;⚡ &lt;b&gt;처리 시간&lt;/b&gt;: 평균 2시간 &amp;rarr; 5분 이내 완료&lt;/li&gt;
&lt;li&gt;  &lt;b&gt;업무 방해&lt;/b&gt;: 하루 4번 마비 &amp;rarr; 0번 (완전 해결!)&lt;/li&gt;
&lt;li&gt;  &lt;b&gt;메모리 문제&lt;/b&gt;: t4g.small OOM &amp;rarr; Lambda 자동 스케일링&lt;/li&gt;
&lt;li&gt;  &lt;b&gt;운영 부담&lt;/b&gt;: 서버 관리 &amp;rarr; 완전 관리형 서비스&lt;/li&gt;
&lt;li&gt;  &lt;b&gt;확장성&lt;/b&gt;: 수동 스케일링 &amp;rarr; 자동 무한 확장&lt;/li&gt;
&lt;li&gt;  &lt;b&gt;개발 효율&lt;/b&gt;: 기존 코드 재사용 &amp;rarr; 빠른 개발&lt;/li&gt;
&lt;li&gt; ️ &lt;b&gt;아키텍처&lt;/b&gt;: 모놀리식 스케줄링 &amp;rarr; Event-Driven 구조&lt;/li&gt;
&lt;/ul&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;&quot;NestJS를 Lambda에서 그대로 쓰는 마법&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;기존 NestJS 코드를 Lambda에서 재사용하는 방법&lt;/li&gt;
&lt;li&gt;Lambda Handler Factory 패턴&lt;/li&gt;
&lt;li&gt;TypeORM 연결 관리 최적화&lt;/li&gt;
&lt;li&gt;실제 성능 비교와 코드 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사내 어드민 운영하시는 분들께&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비슷한 비용 vs 성능 딜레마를 겪고 계신가요? t4g.small로 BigQuery 작업을 시도해보신 적 있나요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러분의 경험과 고민을 댓글로 공유해주세요!&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;이 글은 실제 마케팅 프로젝트에서 겪은 경험을 바탕으로 작성되었습니다.&lt;/i&gt;&lt;/p&gt;
&lt;div id=&quot;mttContainer&quot; class=&quot;notranslate&quot; style=&quot;transform: translate(876px, 7430px);&quot; aria-expanded=&quot;true&quot;&gt;
&lt;div id=&quot;tippy-1&quot; style=&quot;z-index: 100000200; visibility: visible; position: absolute; inset: auto auto 0px 0px; margin: 0px; transform: translate3d(297px, -20px, 0px);&quot; data-tippy-root=&quot;&quot;&gt;
&lt;div class=&quot;tippy-box&quot; style=&quot;max-width: 350px; transition-duration: 300ms;&quot; tabindex=&quot;-1&quot; role=&quot;mtttooltip&quot; data-state=&quot;visible&quot; data-theme=&quot;custom&quot; data-animation=&quot;fade&quot; data-placement=&quot;top&quot;&gt;
&lt;div class=&quot;tippy-content&quot; style=&quot;transition-duration: 300ms;&quot; data-state=&quot;visible&quot;&gt;&lt;span&gt;흐름도 LR Pub [ &quot;Publisher \ n (이벤트 발행 발행)&quot;] 버스 (( &quot;이벤트 버스&quot;)) 서브 그래프 가입자 sub1 [ &quot;가입자 a&quot;] sub2 [ &quot;가입자 b&quot;] Sub3 [ &quot;가입자 C&quot;] 끝 술집 -&amp;gt; 버스 버스 -&amp;gt; sub1 버스 -&amp;gt; sub2 버스 -&amp;gt; sub3&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;tippy-arrow&quot; style=&quot;position: absolute; left: 0px; transform: translate3d(181px, 0px, 0px);&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>⚡ Performance &amp;amp; Optimization/ ️ Server Optimization</category>
      <author>KilPenguin</author>
      <guid isPermaLink="true">https://penguin-dev.tistory.com/33</guid>
      <comments>https://penguin-dev.tistory.com/33#entry33comment</comments>
      <pubDate>Mon, 30 Jun 2025 20:32:13 +0900</pubDate>
    </item>
    <item>
      <title>TypeORM Gzip을 활용한 데이터 압축 및 최적화</title>
      <link>https://penguin-dev.tistory.com/32</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eJsZJC/btsNo3U8iDa/To3Ge5uqLuK0dWkCeDh4B0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eJsZJC/btsNo3U8iDa/To3Ge5uqLuK0dWkCeDh4B0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eJsZJC/btsNo3U8iDa/To3Ge5uqLuK0dWkCeDh4B0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeJsZJC%2FbtsNo3U8iDa%2FTo3Ge5uqLuK0dWkCeDh4B0%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;1024&quot; height=&quot;1024&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;사내 프로젝트에서 구글 빅쿼리 데이터를 조회하고 특정 시점의 스냅샷을 저장해야 하는 요구사항이 있었습니다. 처음엔 MySQL을 선택했지만 향후 S3 연동도 고려하고 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배경 및 문제점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 방식&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL의 JSON 타입 컬럼으로 데이터를 저장했습니다.&lt;/li&gt;
&lt;li&gt;다수의 부동소수점(float) 값이 포함된 JSON 데이터에서 부동소수점 오차로 인한 데이터 불일치가 발생했습니다.&lt;/li&gt;
&lt;li&gt;이 문제는 TDD를 통해 조기에 발견되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방안&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 일관성을 위해 JSON 데이터를 문자열로 변환하고, MySQL 컬럼 타입을 longtext로 변경했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;새로운 문제점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빅쿼리에서 조회한 테이블 중 하나가 약 66MB 크기로, MySQL의 max_allowed_packet(64MB) 한계를 초과하여 에러가 발생했습니다.&lt;/li&gt;
&lt;li&gt;단순한 설정 조정(max_allowed_packet 증가)은 근본적인 해결책이 아닙니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터 압축을 통한 최적화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무손실 압축 방식인 gzip을 활용하여 데이터를 효율적으로 압축했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;gzip 선택 이유&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;Node.js의 zlib 라이브러리를 활용한 쉬운 구현&lt;/li&gt;
&lt;li&gt;네트워크 전송과 저장 효율성이 뛰어나 외부 스토리지 연동에도 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TypeORM의 transformer를 사용하여 데이터 저장 시 자동 압축하고 조회 시 자동 복원하도록 했습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transformer 주의사항: TypeORM의 transformer는 동기 함수로 정의해야 하며, 비동기일 경우 예기치 않은 오류가 발생합니다 (&lt;a href=&quot;https://github.com/typeorm/typeorm/issues/1050&quot;&gt;관련 GitHub 이슈&lt;/a&gt;).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;엔티티 구현 예시&lt;/h3&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@Entity()
export class BigqueryResult extends BaseEntity {
  @Column({
    type: 'longtext',
    transformer: CompressionTransformer,
    comment: '쿼리 결과 데이터',
  })
  public readonly resultData!: string;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Transformer 코드&lt;/h3&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;import { gunzipSync, gzipSync } from 'zlib';

export const CompressionTransformer = {
  to: (value: string) =&amp;gt; {
    if (!value) return value;
    return gzipSync(Buffer.from(value, 'utf-8')).toString('base64');
  },
  from: (value: string) =&amp;gt; {
    if (!value) return value;
    return gunzipSync(Buffer.from(value, 'base64')).toString('utf-8');
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열, JSON 객체, 배열, 부동소수점, 다양한 날짜 형식 및 다국어 데이터를 포함한 압축 및 복원 테스트를 수행했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;537&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbXP9v/btsNnXA2HnX/p8k9lWf1CVIPfC3ak2w6B0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbXP9v/btsNnXA2HnX/p8k9lWf1CVIPfC3ak2w6B0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbXP9v/btsNnXA2HnX/p8k9lWf1CVIPfC3ak2w6B0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbXP9v%2FbtsNnXA2HnX%2Fp8k9lWf1CVIPfC3ak2w6B0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;537&quot; height=&quot;336&quot; data-origin-width=&quot;537&quot; data-origin-height=&quot;336&quot;/&gt;&lt;/span&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;압축 시간: 2 ms&lt;/li&gt;
&lt;li&gt;압축 해제 시간: 1 ms&lt;/li&gt;
&lt;li&gt;대용량 JSON 배열 압축률: 약 82.61%&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 빅쿼리 데이터 테스트 결과&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;압축 시간: 339 ms&lt;/li&gt;
&lt;li&gt;압축 해제 시간: 115 ms&lt;/li&gt;
&lt;li&gt;원본 크기: 66MB &amp;rarr; 압축 후: 1MB (압축률 약 97.46%)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apple M2 Max 32GB 사내 노트북 기준으로 측정한 결과이며, 서버 환경에서는 차이가 있을 수 있습니다.&lt;/p&gt;
&lt;/blockquote&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;JSON 문자열 변환으로 부동소수점 오차를 해결했으나, 데이터 크기 문제를 유발했습니다.&lt;/li&gt;
&lt;li&gt;gzip 무손실 압축을 도입하여 데이터 크기를 획기적으로 줄이고 네트워크 전송 및 저장 효율성을 높였습니다.&lt;/li&gt;
&lt;li&gt;설정 조정보다 압축 도입이 더 근본적인 해결책이며, 향후 S3 등 외부 스토리지 연동 시에도 매우 효과적입니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>⚡ Performance &amp;amp; Optimization/ ️ Database Tuning</category>
      <author>KilPenguin</author>
      <guid isPermaLink="true">https://penguin-dev.tistory.com/32</guid>
      <comments>https://penguin-dev.tistory.com/32#entry32comment</comments>
      <pubDate>Thu, 17 Apr 2025 21:00:40 +0900</pubDate>
    </item>
  </channel>
</rss>