<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Babitalk - Medium]]></title>
        <description><![CDATA[아름답고 건강한 삶을 만드는 뷰티 파트너, 바비톡 - Medium]]></description>
        <link>https://medium.com/babitalk-blog?source=rss----4433725e2350---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Babitalk - Medium</title>
            <link>https://medium.com/babitalk-blog?source=rss----4433725e2350---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Tue, 26 May 2026 07:12:39 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/babitalk-blog" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[마케팅이 기술을 만났을 때: 초자동화와 안정성을 모두 잡은 MarTech 마이크로서비스, crm_box]]></title>
            <link>https://medium.com/babitalk-blog/%EB%A7%88%EC%BC%80%ED%8C%85%EC%9D%B4-%EA%B8%B0%EC%88%A0%EC%9D%84-%EB%A7%8C%EB%82%AC%EC%9D%84-%EB%95%8C-%EC%B4%88%EC%9E%90%EB%8F%99%ED%99%94%EC%99%80-%EC%95%88%EC%A0%95%EC%84%B1%EC%9D%84-%EB%AA%A8%EB%91%90-%EC%9E%A1%EC%9D%80-martech-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4-crm-box-0d734b954c4f?source=rss----4433725e2350---4</link>
            <guid isPermaLink="false">https://medium.com/p/0d734b954c4f</guid>
            <category><![CDATA[digital-marketing]]></category>
            <category><![CDATA[바비톡]]></category>
            <category><![CDATA[martech]]></category>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[babitalk]]></category>
            <dc:creator><![CDATA[Kychoi]]></dc:creator>
            <pubDate>Tue, 31 Mar 2026 02:22:42 GMT</pubDate>
            <atom:updated>2026-03-31T02:22:43.401Z</atom:updated>
            <content:encoded><![CDATA[<p>AWS Serverless, Braze, 그리고 AI로 구축한 바비톡의 마케팅 엔지니어링 여정</p><p>마케팅의 패러다임이 바뀌었습니다. 과거의 마케팅이 “최대한 많은 사람에게 메시지를 뿌리는 것(Mass Marketing)”이었다면, 현대의 마케팅은 “데이터가 예측한 특정 사용자에게, 가장 적절한 타이밍에, 개인화된 메시지를 전달하는 것(Hyper-Personalization)”입니다.</p><p>바비톡은 글로벌 CRM 솔루션인 <strong>Braze</strong>를 도입하여 다음과 같이 성공적인 성과를 거두고 있습니다.</p><ul><li><a href="https://cshub.ab180.co/ko/case-studies/braze-webhook-banner"><strong>브레이즈 Webhook 캠페인으로 맞춤형 배너 기반 A/B 테스트 운영하기</strong></a></li><li><a href="https://cshub.ab180.co/ko/case-studies/braze-webhook-babitalk-coupon"><strong>브레이즈 Webhook으로 최적의 쿠폰 조건 발굴을 위한 A/B 테스트 운영하기</strong></a></li><li><a href="https://cshub.ab180.co/ko/case-studies/braze_ai-campaign_babitalk"><strong>자체 AI 예측 모델로 브레이즈에서 초개인화된 재시술 카카오톡 알림 메시지 발송하기</strong></a></li></ul><p>하지만 강력한 외부 솔루션만으로는 부족합니다. 이를 뒷받침할 <strong>내부의 기술적 인프라</strong>가 없다면, 마케팅은 개발팀의 리소스를 잡아먹는 하마가 되거나, 서비스 장애를 유발하는 폭탄이 될 수 있습니다.</p><p>오늘 소개할 이야기는 바비톡 기술팀이 마케팅의 <strong>속도(Automation)</strong>와 <strong>안정성(Stability)</strong>이라는 두 마리 토끼를 잡기 위해 구축한 서버리스 기반의 <strong>MarTech 엔진, </strong><strong>crm_box</strong>에 대한 기록입니다.</p><h3>1. What is crm_box?</h3><p>crm_box는 바비톡의 마케팅 테크놀로지를 지탱하는 <strong>AWS Serverless 기반의 통합 마이크로서비스</strong>입니다. 이 시스템은 크게 3가지 기술적 특징을 갖고 있습니다.</p><ol><li><strong>Hyper-Automation:</strong> 개발자 개입 없는 마케팅 운영 자동화 (S3 → Lambda) → 마케팅 운영자를 위한 별도의 운영 웹콘솔을 매번 만드는 것은 유지보수 측면에서 매우 번거로운 일입니다. 따라서 바비톡에서 운영 정보를 추상화하여 json으로 작성 후, s3 bucket에 upload하면 자동으로 반영되는 방식을 채택하였습니다.</li><li><strong>Traffic Control:</strong> 마케팅 트래픽 폭주 제어 및 시스템 보호 (Braze → SQS → Lambda) → 타겟팅되어 쿠폰이나 채널 발송이 이뤄질 때 바비톡 시스템으로 그 트래픽이 짧은 시간에 그대로 유입되면 서버가 감당할 수 없습니다. 그래서 바비톡은 처리할 수 있는 만큼만 처리할 수 있도록 큐기반 비동기 방식을 채택하였습니다.</li><li><strong>Auto Data Load</strong>: 바비톡 가공 데이터를 Braze와 연동 → 바비톡 aws 데이터 계정에서 분석 완료된 개인 프로파일 정보 및 모델링 정보를 자동으로 Braze에 적재할 수 있도록 자동화된 Pipeline을 구축하였습니다.</li></ol><p>우리는 이 세축을 통해 마케터에게는 무한한 자유를, 엔지니어에게는 견고한 안정성을 제공합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JH3C7_k5c9QAYwcGBAx8dw.png" /></figure><h3>2. Pillar 1: 초자동화(Hyper-Automation)로 개발자 의존성 끊기</h3><h3>Challenge: “배너 하나 바꾸는데 개발자가 매번 지원해야 하나요?”</h3><p>마케터가 앱 내 배너를 수정하거나 특정 타겟에게 문자를 보내려 할 때마다 개발자에게 티켓을 발행하고 배포를 기다려야 한다면, 마케팅의 골든타임은 이미 지나가 버립니다.</p><h3>Solution: JSON 기반의 Self-Service (S3 Trigger)</h3><p>우리는 “<strong>설정(Configuration)이 곧 실행(Action)이다</strong>”라는 컨셉을 도입했습니다. 복잡한 어드민 페이지 대신, 약속된 포맷의 JSON 파일을 AWS S3 버킷에 업로드하는 것만으로 모든 마케팅 액션이 트리거됩니다.</p><ul><li><strong>앱 배너 Live Ops:</strong> 마케터가 banner_config.json을 S3에 올리면, crm_box 람다가 즉시 트리거되어 DB를 갱신합니다. 앱 재배포 없이 실시간으로 운영이 가능합니다.</li><li><strong>옴니채널 발송:</strong> 타겟 유저 리스트(target.json)를 S3에 넣으면, 람다가 유저별 최적 채널(알림톡/SMS/이메일)을 판별하여 메시지를 발송합니다.</li></ul><p>이로써 마케팅 팀은 개발자의 일정에 구애받지 않고 <strong>스스로(Self-Service)</strong> 캠페인을 운영하는 ‘Marketing as Code’를 실현했습니다.</p><blockquote><em>“개발자는 채널 연동 API만 유지보수합니다. 캠페인 운영은 마케터가 데이터(JSON)만 준비하면 끝나는 구조입니다. 이를 통해서 서버를 계속 유지할 필요도 없고 마케팅 요청에 아삽으로 지원할 필요도 없게 되었습니다.”</em></blockquote><h3>3. Pillar 2: 성공한 마케팅은 디도스(DDoS)와 같다</h3><h3>Challenge: 트래픽 스파이크의 공포</h3><p>CRM 팀이 Braze를 통해 “전 국민 대상 푸시 발송” 버튼을 누르는 순간, 서버 개발자들은 긴장합니다. 수십만 명의 유저가 동시에 앱을 켜고 접속을 시도하기 때문입니다. 이러한 <strong>Traffic Spike</strong>는 예측이 불가능하며, 몇 초 만에 0에서 수만 RPS로 치솟아 메인 서비스를 마비시킬 수 있습니다.</p><h3>Solution: 유연한 완충 지대 (Braze → API Gateway→ SQS → Lambda)</h3><p>우리는 crm_box 내에 트래픽을 제어하는 댐(Dam)을 건설했습니다.</p><ol><li><strong>Ingest (API Gateway):</strong> Braze Webhook이나 앱의 마케팅 반응 요청을 가장 먼저 받아냅니다. 서버 관리 없이 무제한에 가까운 트래픽을 수용합니다.</li><li><strong>Buffer (Amazon SQS):</strong> 받은 요청을 즉시 처리하지 않고 <strong>SQS(Queue)</strong>에 쌓습니다. 여기가 핵심입니다. SQS는 갑자기 밀려드는 요청을 안전하게 저장하여 뒷단 시스템을 보호합니다.</li><li><strong>Process (Throttling):</strong> 람다 함수가 SQS에서 메시지를 하나씩 꺼내 처리합니다. 이때 **동시 실행 수(Concurrency)**를 조절하여, 레거시 DB나 API 서버가 감당할 수 있는 속도로만 요청을 흘려보냅니다.</li></ol><p>결과적으로 마케팅 트래픽이 아무리 폭주해도, 메인 서비스는 평온함을 유지합니다.</p><h3>4. Pillar 3: 룰(Rule)을 넘어 AI로 (Data &amp; Intelligence)</h3><p>자동화와 안정성 위에 ‘<strong>지능</strong>’을 더했습니다. 단순한 조건문(“20대 여성”)으로는 고객의 마음을 움직일 수 없기 때문입니다.</p><ul><li><strong>Automated Data Pipeline:</strong> 운영 DB의 데이터는 매일 AWS 데이터 분석 계정으로 자동 적재(ETL)되어 데이터의 사일로(Silo) 현상을 막습니다.</li><li><strong>MLOps &amp; Personalization:</strong> 머신러닝 모델이 “이탈할 것 같은 유저”, “성형 정보에 관심 있는 유저”를 예측하여 그룹핑합니다.</li><li><strong>Actionable Insight:</strong> 이 예측 데이터는 다시 crm_box와 Braze로 동기화되어, “이탈 고위험군에게만 할인 쿠폰 자동 발송”과 같은 정교한 시나리오를 가능하게 합니다.</li></ul><h3>5. Impact: 기술이 증명한 비즈니스 성과</h3><p>이러한 crm_box 중심의 아키텍처는 단순한 ‘시스템 구축’을 넘어 놀라운 비즈니스 지표로 증명되었습니다.</p><ul><li><strong>Performance:</strong> AI 모델 기반 타겟팅 캠페인의 경우, 기존 룰 베이스 대비 <strong>전환율(CVR) 13.4% 상승, 조회율(CTR) 6.5%, 결제 26.5% 상승</strong></li><li><strong>Stability:</strong> 수백만 건의 대규모 푸시 발송 시에도 메인 서비스 <strong>다운타임 0%</strong> 달성</li><li><strong>Efficiency:</strong> 개발자 도움 없이 마케터가 스스로 배너를 수정하고 캠페인을 집행 (Time-to-Market 단축)</li></ul><p><em>(참고: </em><a href="https://cshub.ab180.co/ko/case-studies/braze_ai-campaign_babitalk"><em>Ab180 Customer Case Studies — Babitalk</em></a><em>)</em></p><h3>마치며: Marketing Engineering</h3><p>지금까지의 crm_box를 정리하면 다음과 같습니다. MarTech는 결과적으로 data에서 시작하여 channel로 끝나는 여정을 얼마나 데이터 기반으로 자동화하면서 그 정교함을 AI로 극대화할 수 있느냐로 정리할 수 있는 것 같습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/744/1*2aWC6qvAtGiwQG7UcLXuDQ.png" /></figure><p>바비톡의 마케팅은 더 이상 ‘감’으로 하는 예술이 아닙니다. <strong>데이터로 검증하고 기술로 실행하는 엔지니어링</strong>입니다.</p><p>우리는 crm_box를 통해 마케터에게는 강력한 무기를, 개발자에게는 평화로운 주말을 선물했습니다. 서버리스(Serverless)의 유연함과 데이터(Data)의 정확성으로 무장한 바비톡의 MarTech 도전은 계속됩니다.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0d734b954c4f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/babitalk-blog/%EB%A7%88%EC%BC%80%ED%8C%85%EC%9D%B4-%EA%B8%B0%EC%88%A0%EC%9D%84-%EB%A7%8C%EB%82%AC%EC%9D%84-%EB%95%8C-%EC%B4%88%EC%9E%90%EB%8F%99%ED%99%94%EC%99%80-%EC%95%88%EC%A0%95%EC%84%B1%EC%9D%84-%EB%AA%A8%EB%91%90-%EC%9E%A1%EC%9D%80-martech-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4-crm-box-0d734b954c4f">마케팅이 기술을 만났을 때: 초자동화와 안정성을 모두 잡은 MarTech 마이크로서비스, crm_box</a> was originally published in <a href="https://medium.com/babitalk-blog">Babitalk</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AI와 함께한 레거시 탈출: 테스트라는 용기로 레거시를 해체하기]]></title>
            <link>https://medium.com/babitalk-blog/ai%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C-%ED%83%88%EC%B6%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%9D%BC%EB%8A%94-%EC%9A%A9%EA%B8%B0%EB%A1%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C%EB%A5%BC-%ED%95%B4%EC%B2%B4%ED%95%98%EA%B8%B0-90d40938e109?source=rss----4433725e2350---4</link>
            <guid isPermaLink="false">https://medium.com/p/90d40938e109</guid>
            <dc:creator><![CDATA[Mjgo]]></dc:creator>
            <pubDate>Mon, 30 Mar 2026 09:08:18 GMT</pubDate>
            <atom:updated>2026-03-31T07:26:36.831Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/860/1*GHO_u7hxAmjEPxKtTl6log.png" /></figure><p><a href="https://medium.com/babitalk-blog/ai%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C-%ED%83%88%EC%B6%9C-%ED%8C%8C%ED%8E%B8%ED%99%94%EB%90%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81-%EA%BF%B0%EB%9A%AB%EA%B8%B0-7bf32bd81c7f">1편</a>에서는 AI를 활용해 4개 레포지토리에 파편화된 비즈니스 로직을 분석하는 방법을 다뤘고, <a href="https://medium.com/babitalk-blog/ai%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C-%ED%83%88%EC%B6%9C-%EC%95%88%EC%A0%84%EB%A7%9D-%EA%B9%94%EA%B8%B0-pytest-%EB%8F%84%EC%9E%85%EA%B8%B0-2a6b09d8f35b">2편</a>에서는 테스트 코드를 자유롭게 작성할 수 있는 환경을 구축했습니다.</p><p>이제 본격적으로 <strong>테스트라는 안전망 위에서 레거시 코드를 리팩터링</strong>할 차례입니다. 이번 글에서는 하나의 버그 수정에서 출발해, AI와 함께 단계적으로 코드를 개선해 나간 실제 과정을 공유합니다.</p><p>단 하나의 API 엔드포인트를 리팩터링하는 과정이지만, 여기에 레거시 코드를 도메인 중심의 견고한 구조로 바꾸는 과정을 담았습니다.</p><p>모든 것은 간단한 버그 리포트에서 시작되었습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/760/1*3O7_YRdLhOFA1XRMC_xsrw.png" /><figcaption>버그는 잡았다. 그런데 이 코드, 괜찮은 건가?</figcaption></figure><blockquote><em>“미리결제 후기가 블라인드 처리되었는데, 후기 사진 리스트에서는 여전히 노출됩니다”</em></blockquote><p>블라인드된 리뷰의 이미지가 필터링되지 않는 버그였습니다. 수정 자체는 간단했지만, 해당 코드를 열어보니 문제의 규모가 보였습니다.</p><pre># 기존 코드: prepayment_events_detail.py<br># 이 하나의 파일에 10개 이상의 API 클래스가 존재<br>class PrepaymentEventsReviewsBrief(Resource):<br>    @auth.check_token<br>    def get(self, prepayment_event_id):<br>        # 100줄이 넘는 하나의 메서드 안에:<br>        # - DB 쿼리 직접 실행<br>        # - 비즈니스 로직 처리<br>        # - 이미지 URL 변환<br>        # - 응답 데이터 조립<br>        # 모든 것이 한 곳에 혼재<br>        base_query = (<br>            db.session.query(BTPrepaymentEventReviewImage)<br>            .join(BTPrepaymentEventReview, ...)<br>            .filter(...)<br>        )<br>        total_image_count = base_query.count()<br>        # ... 100줄의 로직 ...<br>        return {&quot;data&quot;: result}</pre><p>여러분은 요즘 버그픽스를 어떻게 하시나요? 처음부터 눈으로 찾아가면 쉽지만, AI에게 에러 메시지를 붙여넣으면 바로 원인을 찾아줍니다. 물론 수정 후에 이 수정으로 인한 사이드이펙트가 있을지 확인하는것은 필수적입니다.</p><pre>or_(<br>        BTPrepaymentEventReview.blind_type == BlindType.DEFAMATION_BLIND,<br>        BTPrepaymentEventReview.blind_type.is_(None),<br>    ),<br>)</pre><p>이제 버그를 수정하는 것은 쉽지만, <strong>이 코드가 계속 이 상태로 남아 있으면 같은 종류의 버그가 반복될 것</strong>이 분명했습니다. 하지만 덩어리진 코드를 함부로 건드리면 기존 동작이 깨질 수 있습니다.</p><p>우선 <strong>버그 수정 커밋은 오직 버그만 수정합니다.</strong> 리팩터링은 별도의 커밋으로 분리합니다. 이렇게 하면 문제가 생겼을 때 버그 수정과 리팩터링 중 어디서 문제가 발생했는지 명확하게 파악할 수 있습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/760/1*qE1Gmp5elrwIG9zW2wJudA.png" /><figcaption>수정은 한줄, 하지만 구조는 그대로</figcaption></figure><h3>2. 테스트 코드로 안전망 구축</h3><p>버그를 수정한 뒤, 리팩터링을 시작하기 전에 가장 먼저 한 일은 <strong>기존 동작을 보장하는 테스트 코드를 작성</strong>하는 것이었습니다. 2편에서 구축한 테스트 환경이 여기서 빛을 발합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/760/1*Y6c8_5IpEhMKj6-F9EiNnA.png" /><figcaption>안전망 완성. 리팩터링 전 필수 단계</figcaption></figure><p>AI에게 다음과 같이 요청했습니다:</p><blockquote><em>“이 API의 전체 응답값을 검증하는 테스트를 작성해줘. 이미지가 없는 경우, 이미지가 있는 경우, 블라인드된 리뷰가 제외되는 경우를 모두 커버해줘”</em></blockquote><p>AI는 코드를 분석하여 모든 시나리오를 커버하는 테스트를 생성했습니다. 외부 의존성이 존재하는 코드들은 우선 mocking을 통해서 테스트를 돌아가게 해야합니다.</p><pre>@pytest.mark.usefixtures(&quot;db_session&quot;)<br>class TestPrepaymentEventsReviewsBrief:<br>  def test_default_get(self, db_session, test_client, monkeypatch):<br>          &quot;&quot;&quot;기본 미리결제 이벤트 후기 요약 조회 테스트 (이미지 없는 경우)&quot;&quot;&quot;<br>          # Given: 테스트 데이터 준비<br>          self._setup_base_data(db_session)<br>          with patch(<br>              &#39;prepayment_events.prepayment_events_detail.PrepaymentReviewReport&#39;<br>          ) as mock_review_report:<br>              mock_review_report.return_value.get_report_coupon_book_ids.return_value = []<br>              # When: API 호출<br>              result = test_client.get(&quot;prepayment_events/1/reviews/brief&quot;)<br>          # Then: 전체 응답 데이터 검증<br>          assert result.get_json() == {<br>              &#39;data&#39;: {<br>                  &#39;title&#39;: &#39;실경험 인증 후기&#39;,<br>                  &#39;sub_title&#39;: &#39;미리결제로 구매하여,\\n실제 병원에서 시술받은 바비들의 후기입니다&#39;,<br>                  &#39;review_count&#39;: 0,<br>                  &#39;review_score&#39;: 0.0,<br>                  &#39;total_image_count&#39;: 0,<br>                  &#39;remain_image_count&#39;: None,<br>                  &#39;images&#39;: None,<br>              }<br>          }<br>      def test_blinded_reviews_are_excluded(self, db_session, test_client):<br>          &quot;&quot;&quot;블라인드 처리된 리뷰는 후기 이미지에서 제외되는지 검증&quot;&quot;&quot;<br>          # Given: 블라인드된 리뷰 포함 데이터 준비<br>          # When: API 호출<br>          # Then: 블라인드된 리뷰의 이미지는 응답에 포함되지 않음</pre><p>여기서 중요한 포인트는 API의 <strong>전체 응답값을 비교하는 assert</strong>입니다. 단순히 status_code만 검증하는 것이 아니라, 응답의 모든 필드를 검증합니다. 하나의 테스트를 이렇게 만들어 놓으면:</p><ul><li><strong>회귀 테스트</strong>: 리팩터링 후에도 응답이 동일한지 자동으로 검증됩니다</li><li><strong>API 계약 보장</strong>: 클라이언트가 기대하는 응답 형식이 유지됩니다</li><li><strong>비즈니스 로직 검증</strong>: 내부 구현이 바뀌어도 결과는 같아야 합니다</li></ul><p>이 테스트가 통과하는 한, 안심하고 코드를 리팩터링 할 수 있습니다.</p><h3>3. 클래스 분리: 단일 책임 원칙</h3><p>테스트가 준비되었으니, 이제 본격적인 리팩터링을 시작합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/760/1*Fmc36XFSJwnoRhnztoz7Fg.png" /><figcaption>테스트 통과 -&gt; 분리 성공 즉시 확인</figcaption></figure><p>첫 번째 단계는 <strong>클래스를 별도 파일로 분리</strong>하는 것입니다. 기존에는 prepayment_events_detail.py 하나의 파일에 10개 이상의 API 클래스가 몰려 있었습니다.</p><p>AI에게 요청했습니다:</p><blockquote><em>“PrepaymentEventsReviewsBrief 클래스를 별도 파일로 분리해줘. import 관계도 정리해줘&quot;</em></blockquote><p>AI는 import 의존성을 분석하여 안전하게 클래스를 분리했습니다.</p><pre>Before:<br>prepayment_events/<br>  └── prepayment_events_detail.py  # 10개+ 클래스, 800줄+<br><br>After:<br>prepayment_events/<br>  ├── prepayment_events_detail.py          # 나머지 클래스들<br>  └── prepayment_events_reviews_brief.py   # 분리된 단일 클래스</pre><p>분리 후 테스트를 실행합니다:</p><pre>$ pytest tests/unit/prepayment_events/ -v<br>PASSED test_default_get<br>PASSED test_get_with_images<br>PASSED test_blinded_reviews_are_excluded</pre><p>모든 테스트가 통과합니다. <strong>테스트가 있으니 분리가 정상적으로 되었다는 것을 즉시 확인</strong>할 수 있습니다.</p><h3>4. API 스키마 분리: Pydantic으로 계약 정의</h3><p>다음 단계는 <strong>API의 요청/응답 스키마를 명시적으로 정의</strong>하는 것입니다. 기존 코드에서는 딕셔너리로 응답을 직접 조립하고 있었습니다:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/760/1*BmQpk14h_slpBkOiengRXQ.png" /><figcaption>타입이 생기면 버그도 사라집니다.</figcaption></figure><pre># Before: 응답 구조가 코드 속에 숨어 있음<br>result = {<br>    &quot;title&quot;: &quot;실경험 인증 후기&quot;,<br>    &quot;sub_title&quot;: &quot;미리결제로 구매하여,\\n...&quot;,<br>    &quot;review_count&quot;: prepayment_event.total_review_count,<br>    &quot;images&quot;: images,  # 이 images의 구조는?<br>}<br>return {&quot;data&quot;: result}</pre><p>이 코드만 보고는 API 응답의 정확한 구조를 파악하기 어렵습니다. AI에게 기존 응답 구조를 분석하여 Pydantic 스키마로 변환해달라고 요청했습니다:</p><pre># After: Pydantic으로 명확한 응답 계약 정의<br>class ReviewImageSchema(BaseModel):<br>    type: str<br>    image: str<br>    small_image: str<br>    surgery: Optional[int]<br>    order: Optional[int]<br>    scheme: str<br><br>class ImagesSchema(BaseModel):<br>    data: List[ReviewImageSchema]<br><br>class PrepaymentEventsReviewsBriefResponse(BaseModel):<br>    title: str<br>    sub_title: str<br>    review_count: int<br>    review_score: float<br>    total_image_count: int<br>    remain_image_count: Optional[int]<br>    images: Optional[ImagesSchema]<br><br>class PrepaymentEventsReviewsBriefApiResponse(BaseModel):<br>    data: PrepaymentEventsReviewsBriefResponse</pre><p><strong>FastAPI와 연동되는 Pydantic으로 작성된</strong> 스키마를 정의하면:</p><ul><li>API의 요청/응답 구조를 <strong>코드 자체가 문서</strong>가 됩니다</li><li>향후 Swagger 연동 시 바로 활용할 수 있습니다</li><li>타입 검증이 자동으로 이루어집니다</li></ul><p>또한 이미지 처리 로직도 별도 함수로 분리했습니다:</p><pre>def create_images_schema(review_images):<br>    &quot;&quot;&quot;후기 이미지 리스트를 ImagesSchema로 변환&quot;&quot;&quot;<br>    if not review_images:<br>        return None<br>    image_list = []<br>        for image in review_images:<br>            image_schema = ReviewImageSchema(<br>                type=&quot;raw.image&quot;,<br>                image=make_image_url(image.url),<br>                small_image=make_image_url(image.url, size=&quot;small&quot;),<br>                surgery=image.surgery,<br>                order=image.order,<br>                scheme=PrepaymentEventsScreen.reviews_images_scheme(<br>                    review_id=image.review_id,<br>                    query_parameter=f&quot;?order={image.order}&amp;all_review=1&quot;,<br>                ),<br>            )<br>            image_list.append(image_schema)<br>        return ImagesSchema(data=image_list) if image_list else None</pre><p>UI 표현 로직을 분리하면, <strong>비즈니스 로직의 재사용성이 높아지고 UI 변경이 비즈니스 로직에 영향을 주지 않습니다.</strong></p><h3>5. 서비스 레이어 분리</h3><p>이제 핵심입니다. API 컨트롤러에 혼재되어 있던 <strong>비즈니스 로직을 서비스 레이어로 분리</strong>합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/760/1*c8mq_72n1R9sssppkVTgmw.png" /><figcaption>API 메서드 100줄 -&gt; 20줄</figcaption></figure><p>AI에게 다음과 같이 요청했습니다:</p><blockquote><em>“현재 get 메서드에 있는 비즈니스 로직을 서비스 클래스로 추출해줘. DTO를 만들어서 서비스와 뷰 사이의 데이터 전달을 명확하게 해줘”</em></blockquote><h3>DTO(Data Transfer Object) 도입</h3><p>서비스와 뷰 사이에 데이터를 전달하기 위한 불변 객체를 정의합니다:</p><pre>@dataclass(frozen=True)<br>class PrepaymentEventReviewBriefDTO:<br>    prepayment_event: BTPrepaymentEvent<br>    total_image_count: int<br>    remain_image_count: Optional[int]<br>    review_images: List[BTPrepaymentEventReviewImage]</pre><h3>서비스 클래스</h3><pre># Before: API 메서드 안에 비즈니스 로직이 100줄+<br>class PrepaymentEventsReviewsBrief(Resource):<br>    def get(self, prepayment_event_id):<br>        # DB 쿼리 + 비즈니스 로직 + 응답 조립이 모두 여기에...<br><br><br># After: 서비스 클래스에서 비즈니스 로직만 담당<br>class PrepaymentEventReviewBriefService:<br>    def get_review_brief_data(<br>        self,<br>        prepayment_event_id: int,<br>        blocked_user_list: List[int],<br>        report_coupon_book_ids: List[int],<br>    ) -&gt; PrepaymentEventReviewBriefDTO:<br>        &quot;&quot;&quot;후기 요약 데이터를 조회하고 DTO로 반환&quot;&quot;&quot;<br>        prepayment_event = self.__prepayment_repository.get_prepayment_event_by_id(<br>            prepayment_event_id<br>        )<br>        total_image_count = self.__prepayment_repository.get_review_image_total_count(<br>            prepayment_event_id, blocked_user_list, report_coupon_book_ids<br>        )<br>        review_images = []<br>        remain_image_count = None<br>        if total_image_count &gt; 0:<br>            review_images = self.__prepayment_repository.get_representative_review_images(<br>                prepayment_event_id, blocked_user_list, report_coupon_book_ids<br>            )<br>            review_image_count = self.__prepayment_repository.get_representative_review_image_count(<br>                prepayment_event_id, blocked_user_list, report_coupon_book_ids<br>            )<br>            if total_image_count &gt; 10 and review_image_count &gt; 10:<br>                remain_image_count = self.__prepayment_repository.get_remain_review_image_count(<br>                    prepayment_event_id, blocked_user_list, report_coupon_book_ids<br>                )<br>        return PrepaymentEventReviewBriefDTO(<br>            prepayment_event=prepayment_event,<br>            total_image_count=total_image_count,<br>            remain_image_count=remain_image_count,<br>            review_images=review_images,<br>        )</pre><p>API 컨트롤러는 이제 <strong>요청/응답 처리에만 집중</strong>합니다:</p><pre>class PrepaymentEventsReviewsBrief(Resource):<br>    def get(self, prepayment_event_id):<br>        # 1. 요청 파싱<br>        user_context = PrepaymentEventsReviewsBriefRequest.from_request()<br><br>        # 2. 서비스 호출  <br>        report_ids = review_report_service.get_report_coupon_book_ids(user_context.user_id)<br>        review_data = review_brief_service.get_review_brief_data(<br>            prepayment_event_id, user_context.blocked_user_list, report_ids<br>        )<br>        # 3. 응답 조립<br>        images = create_images_schema(review_data.review_images)<br>        response = PrepaymentEventsReviewsBriefResponse(<br>            title=&quot;실경험 인증 후기&quot;,<br>            review_count=review_data.prepayment_event.total_review_count,<br>            total_image_count=review_data.total_image_count,<br>            images=images,<br>            # ...<br>        )<br>        return PrepaymentEventsReviewsBriefApiResponse(data=response).model_dump()</pre><p>각 레이어의 역할이 명확해졌습니다:</p><ul><li><strong>View</strong>: HTTP 요청/응답 처리, 인증/권한 확인</li><li><strong>Service</strong>: 비즈니스 로직, 도메인 규칙 적용</li><li><strong>Repository</strong>: 데이터 접근 (다음 단계에서 도입)</li></ul><h3>6. DI 컨테이너와 Repository 패턴 도입</h3><p>마지막 단계입니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/760/1*bDc1qm9ENMj-DkvZ8QSnHA.png" /><figcaption>의존성 분리 완성. 테스트 모킹도 단순해짐</figcaption></figure><p>수년간 기능이 쌓이면서 서비스 클래스들의 외부 의존성이 덕지덕지 붙어갔고, 이를 정리할 수단으로 DI를 실험적으로 도입했습니다. 전체를 한 번에 바꾸는 게 아니라, 의존성 분리가 필요한 지점부터 점진적으로 적용해나가는 방식입니다.</p><p>서비스 클래스가 db.session을 직접 참조하고 있던 것을 <strong>Repository 패턴으로 추상화</strong>하고, <strong>DI(Dependency Injection) 컨테이너</strong>로 의존성을 관리합니다.</p><h3>Repository 패턴: 데이터 접근 추상화</h3><pre># 인터페이스 정의<br>class IPrepaymentRepository(ABC):<br>    @abstractmethod<br>    def get_prepayment_event_by_id(<br>        self, prepayment_event_id: int<br>    ) -&gt; Optional[BTPrepaymentEvent]:<br>        pass<br><br>    @abstractmethod  <br>    def get_review_image_total_count(<br>        self, prepayment_event_id: int, blocked_user_list: List[int],<br>        report_coupon_book_ids: List[int]<br>    ) -&gt; int:<br>        pass<br>    # ...<br><br># 구현체<br>class PrepaymentRepository(IPrepaymentRepository):<br>    def __init__(self, session):<br>        self.session = session<br>    def get_review_image_total_count(self, ...):<br>        return self._get_base_review_image_query(...).count()<br>    def _get_base_review_image_query(self, prepayment_event_id, blocked_user_list, report_coupon_book_ids):<br>        &quot;&quot;&quot;공통 필터링 쿼리를 중앙화&quot;&quot;&quot;<br>        return (<br>            self.session.query(BTPrepaymentEventReviewImage)<br>            .join(BTPrepaymentEventReview, ...)<br>            .filter(<br>                BTPrepaymentEventReview.prepayment_event_id == prepayment_event_id,<br>                BTPrepaymentEventReview.coupon_book_id.notin_(report_coupon_book_ids),<br>                BTPrepaymentEventReview.user_id.notin_(blocked_user_list),<br>                # 블라인드 필터링도 여기서 일괄 처리<br>            )<br>        )</pre><p>기존에 API 메서드 곳곳에 흩어져 있던 쿼리 로직이 Repository에 <strong>중앙화</strong>되었습니다. _get_base_review_image_query라는 공통 메서드로 필터링 조건을 한 곳에서 관리하기 때문에, 블라인드 필터링 같은 조건을 추가할 때도 <strong>한 곳만 수정</strong>하면 됩니다.</p><h3>DI 컨테이너 설정</h3><pre># configs/Container.py<br>class Container(containers.DeclarativeContainer):<br>    config = providers.Configuration()<br>    db_session = providers.Factory(lambda: db.session())<br><br>    # Repository<br>    prepayment_repository = providers.Factory(<br>        PrepaymentRepository, session=db_session<br>    )<br>    # Services<br>    prepayment_review_report_service = providers.Singleton(<br>        PrepaymentReviewReportService, prepayment_repository=prepayment_repository<br>    )<br>    prepayment_review_brief_service = providers.Singleton(<br>        PrepaymentEventReviewBriefService, prepayment_repository=prepayment_repository<br>    )</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/680/1*Y_FREbfHzluoGHYKNet0jQ.png" /><figcaption>의존성은 아래로만 흐른다 — Service → Repository → db.session</figcaption></figure><h3>@inject 데코레이터로 의존성 주입</h3><pre>class PrepaymentEventsReviewsBrief(Resource):<br>    @auth.check_token<br>    @inject<br>    def get(<br>        self,<br>        prepayment_event_id: int,<br>        review_report_service: PrepaymentReviewReportService = Provide[<br>            Container.prepayment_review_report_service<br>        ],<br>        review_brief_service: PrepaymentEventReviewBriefService = Provide[<br>            Container.prepayment_review_brief_service<br>        ],<br>    ):<br>        # 서비스들이 DI 컨테이너에서 자동으로 주입됨</pre><h3>테스트 코드가 간결해지다</h3><p>DI 도입의 가장 체감되는 변화는 <strong>테스트 코드의 간소화</strong>입니다:</p><pre># Before: 복잡한 패치와 목킹 필요<br>with patch(<br>    &#39;prepayment_events.prepayment_events_detail.PrepaymentReviewReport&#39;<br>) as mock:<br>    mock.return_value.get_report_coupon_book_ids.return_value = []<br>    result = test_client.get(&quot;prepayment_events/1/reviews/brief&quot;)<br><br># After: 의존성이 분리되어 단순한 API 호출만으로 테스트 가능<br>result = test_client.get(&quot;prepayment_events/1/reviews/brief&quot;)</pre><p>기존에는 테스트 대상 코드가 다른 API 클래스를 직접 참조하고 있어서 복잡한 목킹이 필요했습니다. DI를 통해 의존성을 분리하니, 테스트 환경에서는 테스트용 의존성이 자동으로 주입되어 <strong>테스트 코드가 비즈니스 로직 검증에만 집중</strong>할 수 있게 되었습니다.</p><h3>7. 리팩터링 전후 비교</h3><h3>코드 구조 변화</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/760/1*mjI3KNoChZrdHxopP8C__A.png" /><figcaption>하나의 버그 수정에서 시작된 7개의 커밋 여정</figcaption></figure><pre># Before:<br>prepayment_events/<br>  └── prepayment_events_detail.py    # 800줄+, 10개 API 클래스와 비즈니스 로직 혼재<br><br># After:<br>prepayment_events/<br>  ├── prepayment_events_detail.py                    # 나머지 API들<br>  ├── prepayment_events_reviews_brief.py             # View (요청/응답 처리)<br>  ├── services/<br>  │   ├── prepayment_event_review_brief_service.py   # Service (비즈니스 로직)<br>  │   └── prepayment_review_report_service.py        # Service (리포트 로직)<br>  ├── repository.py                                  # Repository (데이터 접근)<br>  └── base.py                                        # Repository 인터페이스</pre><h3>각 레이어의 역할</h3><p><strong>레이어</strong> <strong>역할</strong> <strong>의존성</strong> <strong>View</strong> HTTP 요청/응답, 인증, 스키마 변환 Service <strong>Service</strong> 비즈니스 로직, 도메인 규칙 Repository <strong>Repository</strong> DB 쿼리, 데이터 접근 추상화 DB Session</p><h3>핵심 개선 지표</h3><p><strong>항목</strong> <strong>Before</strong> <strong>After</strong> <strong>API 메서드 길이</strong> 100줄+ 20줄 <strong>비즈니스 로직 위치</strong> API 메서드 내부 전용 서비스 클래스 <strong>DB 쿼리 위치</strong> API 메서드 내부 Repository로 추상화 <strong>응답 스키마</strong> 딕셔너리 직접 조립 Pydantic 모델 <strong>테스트 목킹</strong> 복잡한 패치 필요 DI로 자동 주입 <strong>블라인드 필터링</strong> 쿼리마다 직접 추가 Repository에서 중앙 관리</p><h3>8. AI와 함께한 리팩터링, 무엇이 달랐나</h3><p>이번 리팩터링의 모든 단계에서 AI가 핵심적인 역할을 했습니다. 각 단계에서 AI가 도운 것들을 정리하면:</p><ul><li><strong>테스트 코드 작성</strong>: 기존 API의 응답 구조를 분석하여 모든 시나리오를 커버하는 테스트를 생성</li><li><strong>클래스 분리</strong>: import 의존성을 분석하여 안전하게 클래스를 별도 파일로 분리</li><li><strong>Pydantic 스키마 생성</strong>: 기존 딕셔너리 응답을 분석하여 타입이 정의된 스키마로 자동 변환</li><li><strong>서비스/Repository 생성</strong>: 비즈니스 로직을 서비스로 추출하고, 쿼리 로직을 Repository로 분리</li><li><strong>DI 설정</strong>: Container 설정, 와이어링, @inject 데코레이터 적용</li><li><strong>테스트 업데이트</strong>: 매 단계마다 변경된 구조에 맞게 테스트 코드를 업데이트</li></ul><p>개발자가 한 일은 <strong>아키텍처 방향을 결정하고, AI의 결과물을 검증하는 것</strong>이었습니다. “서비스 레이어로 분리해줘”, “Repository 패턴을 도입해줘”와 같이 방향을 지시하면, AI가 기존 코드를 분석하여 구조를 변환하고, 테스트 코드까지 업데이트합니다.</p><p>특히 레거시 코드처럼 <strong>복잡한 의존성이 얽힌 코드를 리팩터링할 때</strong> AI의 가치가 극대화됩니다. 사람이 100줄짜리 메서드에서 어떤 부분이 비즈니스 로직이고, 어떤 부분이 데이터 접근이며, 어떤 부분이 응답 조립인지 분류하는 데는 상당한 시간과 집중력이 필요합니다. AI는 이 분류를 빠르고 정확하게 수행합니다.</p><p>단, 중요한 전제가 있습니다. <strong>테스트 코드가 있어야 AI의 리팩터링 결과를 신뢰할 수 있습니다.</strong> AI가 아무리 잘 분리해도, 기존 동작이 유지되는지는 테스트로 검증해야 합니다. 2편에서 구축한 테스트 환경이 없었다면, 이 리팩터링은 불가능했을 것입니다.</p><h3>9. 다음 단계: AI 워크플로우 자동화</h3><p>이 시리즈에서 다룬 분석 → 테스트 → 리팩터링의 전 과정에서 AI는 단순한 도구를 넘어 <strong>개발 파트너</strong>로 활약했습니다. 그런데 여기서 한 가지 의문이 생깁니다. 매번 같은 맥락을 AI에게 처음부터 설명해야 할까요?</p><p>답은 <strong>skills와 agent 파일로 자동화</strong>하는 것입니다.</p><h3>과정에서 얻은 지식을 재사용 가능한 자산으로</h3><p>1편에서 4개 레포지토리의 비즈니스 로직을 분석하면서, 그동안 몰랐던 도메인 지식들이 대량으로 발견되었습니다. 이런 발견들을 단순히 머릿속에 남겨두는 것이 아니라, <strong>AI가 참조할 수 있는 문서와 규칙 파일로 체계화</strong>할 수 있습니다. 다음에 같은 도메인을 다룰 때, AI가 이 문서를 컨텍스트로 활용하면 분석 단계를 크게 단축할 수 있습니다.</p><p>2편에서 구축한 테스트 환경과 작성 패턴(Given-When-Then, factory 패턴, fixture 설계 등)은 <strong>테스트 코드 작성용 skill 파일</strong>로 만들 수 있습니다. “이 프로젝트에서 테스트를 작성할 때는 이런 패턴을 따라”라고 AI에게 알려주면, 일관된 품질의 테스트 코드가 자동으로 생성됩니다.</p><p>3편의 리팩터링 패턴(SRP, 서비스 레이어, Repository, DI)도 마찬가지입니다. <strong>프로젝트의 아키텍처 규칙을 agent 파일에 정의</strong>해두면, AI가 새로운 코드를 작성하거나 기존 코드를 수정할 때 자동으로 이 규칙을 따릅니다.</p><h3>AI 시대의 문서화</h3><p>과거에는 문서화가 번거로운 추가 작업이었습니다. 하지만 AI와 함께라면 <strong>작업 과정 자체가 문서화</strong>됩니다. 코드를 분석하며 나눈 대화, 리팩터링하며 정한 규칙들을 AI가 정리하여 문서로 만들어줍니다. 문서화의 비용이 획기적으로 낮아진 것입니다.</p><p>이제는 “문서화할 시간이 없다”가 아니라, **”문서화하지 않을 이유가 없다”**는 시대입니다. 그리고 그 문서들이 다시 AI의 컨텍스트가 되어, 더 정확하고 일관된 결과를 만들어내는 <strong>선순환</strong>이 시작됩니다.</p><pre>분석 → 문서화 → skills/agent 파일 → AI 자동화 → 더 나은 분석 → ...</pre><h3>마무리</h3><p>하나의 버그 수정에서 시작해, 7개의 커밋을 거쳐 레거시 코드를 견고한 구조로 탈바꿈시켰습니다.</p><pre>버그 수정 → 테스트 작성 → 클래스 분리 → 스키마 정의 → 서비스 분리 → 구조 개선 → DI 도입</pre><p>이 과정에서 얻은 가장 중요한 교훈은 ”리팩터링은 한 번에 하는 것이 아니라, 작은 단계를 반복하는 것” 이라는 점입니다. 각 단계마다 테스트를 실행하여 기존 동작이 유지되는지 확인하고, 문제가 없으면 다음 단계로 넘어갑니다.</p><p>AI는 이 반복 과정에서 강력한 파트너입니다. 방향을 정해주면 코드 분석, 구조 변환, 테스트 업데이트를 빠르게 수행합니다. 하지만 AI에게 자신감을 주는 것은 결국 <strong>테스트 코드</strong>입니다. 테스트라는 안전망이 있기에 AI도, 개발자도 과감하게 코드를 바꿀 수 있습니다.</p><p>레거시 코드 앞에서 막막함을 느끼고 있다면, 이 글이 도움이 되었으면 합니다. 시작은 작은 버그 수정이었지만, 끝은 도메인 중심의 견고한 아키텍처였습니다.</p><p><strong>[시리즈]</strong></p><ul><li><strong>1편: </strong><a href="https://medium.com/@mjgo_68337/ai%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C-%ED%83%88%EC%B6%9C-%ED%8C%8C%ED%8E%B8%ED%99%94%EB%90%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81-%EA%BF%B0%EB%9A%AB%EA%B8%B0-7bf32bd81c7f">AI와 함께한 레거시 탈출: 파편화된 비즈니스 로직 꿰뚫기</a></li><li><strong>2편: </strong><a href="https://medium.com/@mjgo_68337/ai%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C-%ED%83%88%EC%B6%9C-%EC%95%88%EC%A0%84%EB%A7%9D-%EA%B9%94%EA%B8%B0-pytest-%EB%8F%84%EC%9E%85%EA%B8%B0-2a6b09d8f35b">AI와 함께한 레거시 탈출: 안전망 깔기 (pytest 도입기)</a></li><li><strong>3편: 기술 부채 청산: AI와 함께 도메인 중심의 견고한 코드로 리팩터링하기 ← 현재 글</strong></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=90d40938e109" width="1" height="1" alt=""><hr><p><a href="https://medium.com/babitalk-blog/ai%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C-%ED%83%88%EC%B6%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%9D%BC%EB%8A%94-%EC%9A%A9%EA%B8%B0%EB%A1%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C%EB%A5%BC-%ED%95%B4%EC%B2%B4%ED%95%98%EA%B8%B0-90d40938e109">AI와 함께한 레거시 탈출: 테스트라는 용기로 레거시를 해체하기</a> was originally published in <a href="https://medium.com/babitalk-blog">Babitalk</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AI와 함께한 레거시 탈출: 안전망 깔기 (pytest 도입기)]]></title>
            <link>https://medium.com/babitalk-blog/ai%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C-%ED%83%88%EC%B6%9C-%EC%95%88%EC%A0%84%EB%A7%9D-%EA%B9%94%EA%B8%B0-pytest-%EB%8F%84%EC%9E%85%EA%B8%B0-2a6b09d8f35b?source=rss----4433725e2350---4</link>
            <guid isPermaLink="false">https://medium.com/p/2a6b09d8f35b</guid>
            <dc:creator><![CDATA[Mjgo]]></dc:creator>
            <pubDate>Mon, 30 Mar 2026 09:05:04 GMT</pubDate>
            <atom:updated>2026-03-31T01:12:03.071Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*rfWMxuwuk_wY15br.png" /></figure><p>AI로 코드를 작성하는 시대가 되면서 테스트코드가 더 중요해지고 있습니다. 테스트코드를 먼저 작성하고 테스트코드가 통과할 때까지 코드를 고치는 TDD 방법이 AI 도구를 통해서 코딩을 할 때 이전보다 훨씬 안정성을 더해주죠. 이전 포스팅에서 말씀드렸듯, 바비톡의 레거시 서버는 Python 기반의 Django, Flask로 되어있습니다.</p><p><strong>프레임워크</strong> <strong>역할</strong> <strong>ORM</strong> Flask 앱 API 서버 SQLAlchemy Flask 배치/크론 서버 SQLAlchemy Django 병원 관리자 서버 Django ORM Django 운영 관리자 서버 Django ORM</p><p>이 4개 프로젝트 모두 <strong>테스트코드가 전무한 상태</strong>에서 출발해 pytest 기반의 테스트 환경을 구축한 과정을 공유합니다.</p><h3>1. 공통 전략</h3><ul><li><strong>SQLite 인메모리 데이터베이스</strong>를 테스트 DB로 사용합니다.</li><li>가볍고, 의존성이 없는 테스트 환경을 구축할 수 있습니다.</li><li>pytest를 사용합니다.</li><li>Django 프로젝트의 경우 <a href="https://github.com/pytest-dev/pytest-django">pytest-django</a>를 추가로 설치해야 합니다. Django의 DB 트랜잭션 관리, 설정 파일 연동 등을 pytest에서 자연스럽게 쓸 수 있게 해주는 플러그인입니다.</li><li>pytest를 설치한 후 간단한 테스트코드를 생성해서 실행시켜 보면서 의존성 문제를 먼저 해결하는 것이 좋습니다.</li><li>pytest 설치 → 테스트코드 없이 pytest 명령어를 돌려서 시스템 의존성 문제 확인 (import 문제 등)</li><li>간단한 테스트코드 작성 후 pytest 실행 → 코드 의존성 확인 및 제거</li></ul><h3>2. 시스템 의존성 제거하기</h3><p>서버를 정상적으로 실행할 때는 오류가 안 나는데, 테스트코드를 실행시키면 오류가 나는 경우가 있습니다. 일반적으로 main 함수로 서버를 돌릴 때와는 실행 순서가 달라져서 순환참조 같은 의존성 문제가 발생하는 것인데요, 이를 위해 테스트 환경 설정을 먼저 잡아줘야 합니다.</p><p>핵심은 conftest.py입니다. pytest가 테스트를 실행하기 전 가장 먼저 로드하는 파일이기 때문에, 여기서 <strong>외부 의존성을 차단하고 테스트 환경을 구성</strong>합니다.</p><pre># conftest.py (Flask 프로젝트 예시)<br>import os, sys<br>from unittest.mock import MagicMock, patch<br>os.environ[&#39;APP_ENV&#39;] = &#39;testing&#39;<br>os.environ[&#39;AUTH_BYPASS&#39;] = &#39;true&#39;<br># ... 기타 환경변수 설정<br>sys.modules[&#39;DB.orm_config&#39;] = MagicMock()<br>sys.modules[&#39;DB.elasticsearch&#39;] = MagicMock()<br>sys.modules[&#39;utils.aws_handler&#39;] = MagicMock()<br>sys.modules[&#39;utils.datadog_apm&#39;] = MagicMock()</pre><p>Django 프로젝트의 경우, 프레임워크 자체가 설정 파일 기반으로 동작하기 때문에 settings_test.py를 분리하는 방식으로 같은 목적을 달성합니다.</p><pre># settings_test.py (Django 프로젝트 예시)<br>from babitalk_admin_backend.settings import *<br><br>DATABASES = {<br>    &quot;default&quot;: {<br>        &quot;ENGINE&quot;: &quot;django.db.backends.sqlite3&quot;,<br>        &quot;NAME&quot;: &quot;:memory:&quot;,<br>    },<br>}<br># 멀티 DB 라우터 비활성화 (테스트에서는 단일 DB 사용)<br>DATABASE_ROUTERS = []<br>REPLICA_DATABASES = []<br># 인증/권한 체크 비활성화<br>REST_FRAMEWORK = {<br>    **REST_FRAMEWORK,<br>    &quot;DEFAULT_PERMISSION_CLASSES&quot;: [&quot;rest_framework.permissions.AllowAny&quot;],<br>    &quot;DEFAULT_AUTHENTICATION_CLASSES&quot;: [],<br>}<br># 외부 서비스 비활성화<br>DATADOG_TRACE = {&#39;ENABLED&#39;: False}</pre><p>방식은 다르지만 목적은 같습니다: <strong>프로덕션 리소스에 접근하지 않고, 외부 의존성을 차단하는 것.</strong></p><h3>3. 테스트 DB 구성</h3><p>기본적으로 테스트코드는 아래와 같은 흐름으로 실행됩니다.</p><ol><li>서버에 의존하는 모든 테이블 생성</li><li>테스트코드에 의존하는 데이터 생성</li><li>테스트코드에서 데이터 조회 및 생성</li><li>데이터가 일치하는지 확인</li></ol><p>여기서 1번, 테이블을 생성하는 방법이 프레임워크마다 다릅니다.</p><p><strong>Flask + SQLAlchemy</strong>에서는 fixture로 SQLite 인메모리 엔진을 만들고, 모델의 메타데이터로부터 테이블을 직접 생성합니다.</p><pre># conftest.py<br>@pytest.fixture(scope=&#39;class&#39;)<br>def db_engine(app):<br>    &quot;&quot;&quot;클래스 단위로 공유되는 데이터베이스 엔진&quot;&quot;&quot;<br>    with app.app_context():<br>        engine = create_engine(&quot;sqlite:///:memory:&quot;, echo=False)<br>        db.metadata.create_all(bind=engine)<br>        yield engine<br>        engine.dispose()</pre><p><strong>바비톡에서 사용하는 Django</strong>에서는 한 가지 더 넘어야 할 허들이 있습니다. 바비톡의 Django 모델은 기존 MySQL 테이블을 참조만 하는 구조라 managed=False로 선언되어 있습니다. 이 설정은 Django에게 &quot;이 모델의 테이블은 Django가 관리하지 않는다&quot;는 뜻이기 때문에, 테스트 DB를 만들 때도 <strong>해당 모델의 테이블이 생성되지 않습니다.</strong> 마이그레이션 파일도 없고, 테이블 자동 생성도 안 되는 상황인 거죠.</p><p>이를 해결하기 위해 conftest.py에 세션 스코프 fixture를 작성했습니다. 테스트 시작 시점에 모든 managed=False 모델을 찾아 managed=True로 변경하고, 테이블을 직접 생성합니다.</p><pre># conftest.py<br>@pytest.fixture(scope=&quot;session&quot;, autouse=True)<br>def enable_db_access_for_all_unmanaged_models(django_db_setup, django_db_blocker):<br>    &quot;&quot;&quot;<br>    테스트 실행 시 모든 unmanaged 모델(managed=False)을 managed=True로 변경.<br>    이렇게 하면 테스트 DB에 테이블이 생성됩니다.<br>    &quot;&quot;&quot;<br>    from django.apps import apps<br>    with django_db_blocker.unblock():<br>            from django.db import connection<br>            unmanaged_models = []<br>            for model in apps.get_models():<br>                if not model._meta.managed:<br>                    model._meta.managed = True<br>                    unmanaged_models.append(model)<br>            # 테이블 생성<br>            with connection.schema_editor() as schema_editor:<br>                for model in unmanaged_models:<br>                    try:<br>                        schema_editor.create_model(model)<br>                    except Exception:<br>                        pass  # 이미 존재하는 경우 무시</pre><p>autouse=True이기 때문에 모든 테스트에 자동으로 적용되고, scope=&quot;session&quot;이라 전체 테스트 세션에서 한 번만 실행됩니다. 프로덕션 코드의 managed=False는 건드리지 않고, <strong>테스트 런타임에서만 동적으로 변경</strong>하는 것이 포인트입니다.</p><p>여기에 pytest-django의 --nomigrations 플래그를 함께 사용합니다. 마이그레이션 파일 자체가 존재하지 않기 때문에, 이 플래그로 마이그레이션 단계를 건너뛰고 <strong>모델 클래스의 필드 정의를 직접 읽어 테이블을 생성</strong>하도록 합니다.</p><pre># pytest.ini<br>[pytest]<br>DJANGO_SETTINGS_MODULE = babitalk_admin_backend.settings_test<br>addopts = --nomigrations</pre><h3>4. 테스트 데이터 팩토리</h3><p>테이블이 생성되면 다음은 테스트에 필요한 데이터를 만드는 과정입니다. “given, 이러한 데이터가 있을 때, 함수를 실행시키면, 어떠한 결과가 나와야 한다” — 여기서 <strong>“이러한 데이터가 있을 때”의 조건</strong>을 만들 때 사용됩니다.</p><p>레거시 모델들은 필드 수가 수십 개에 달하는 경우가 많습니다. 테스트마다 모든 필드를 일일이 채워야 한다면 테스트코드의 복잡성만 늘어나겠죠. 그래서 각 모델마다 <strong>기본값이 채워진 팩토리</strong>를 만들어두고, 테스트에서는 필요한 필드만 오버라이드하여 사용합니다.</p><p>Flask 프로젝트에서는 make_user(), make_hospital() 같은 팩토리 함수를 직접 작성했고, Django 프로젝트에서는 factory_boy의 DjangoModelFactory를 활용했습니다.</p><pre># Django - factory_boy 예시<br>class BtEventFactory(DjangoModelFactory):<br>    class Meta:<br>        model = BtEvent<br>        cid = 123<br>        hid = 1<br>        text = &#39;Default Event Description&#39;<br>        tim = factory.LazyFunction(get_naive_now)<br>        # ... 각 필드의 기본값 정의</pre><p>AI를 활용하면 모델 정의를 기반으로 Factory를 빠르게 생성할 수 있어 큰 부담 없이 만들 수 있었습니다.</p><h3>5. 실제 테스트 코드 (Given-When-Then)</h3><p>환경이 갖춰지면, 실제 테스트 코드는 <strong>Given-When-Then</strong> 패턴으로 간결하게 작성할 수 있습니다.</p><pre># Flask 프로젝트 예시<br>@pytest.mark.usefixtures(&quot;db_session&quot;)<br>class TestPrepaymentReviewService:<br>    def test_get_review_and_event_info_no_mapping(self, db_session):<br>        &quot;&quot;&quot;맵핑된 일반 이벤트가 없을 때 미리결제 이벤트 정보를 올바르게 반환하는지 테스트&quot;&quot;&quot;<br>        # given: make_xxx() 함수로 테스트 객체 생성<br>        user = make_user(id=1, name=&quot;test_user&quot;)<br>        hospital = make_hospital(id=1, name=&quot;테스트 병원&quot;)<br>        prepayment_event = make_prepayment_event(<br>            id=101, client_id=1, hospital_id=1,<br>            name=&quot;오리지널 미리결제 이벤트&quot;, matching_event_id=None,<br>        )<br>        review = make_prepayment_event_review(<br>            id=1001, user_id=user.id, prepayment_event_id=prepayment_event.id,<br>        )<br>        db_session.add_all([user, hospital, prepayment_event, review])<br>        db_session.commit()<br># when: 서비스 메소드 호출<br>        service = PrepaymentReviewService()<br>        review_result, event_data = service.get_review_and_event_info(<br>            review_id=review.id, user_id=user.id<br>        )<br>        # then: 결과 검증<br>        assert review_result.id == review.id<br>        assert event_data[&quot;data&quot;][&quot;name&quot;] == &quot;오리지널 미리결제 이벤트&quot;</pre><pre># Django 프로젝트 예시<br>@pytest.mark.django_db<br>def test_create_category(api_client, mocker):<br>    &quot;&quot;&quot;카테고리 생성 API가 SQLite 테스트 DB에서 정상 동작하는지 검증&quot;&quot;&quot;<br>    # given: 외부 의존성 Mock 처리 후 테스트 데이터 준비<br>    dummy_redis = mocker.Mock()<br>    mocker.patch(&quot;api.category.service.redis&quot;, dummy_redis)<br>    parent = Category.objects.create(<br>        id=1, name=&quot;Parent Category&quot;, type=1, parent=None, order=0, depth=0,<br>    )<br>    # when: API 호출<br>        payload = {<br>            &quot;id&quot;: 2, &quot;name&quot;: &quot;Child Category&quot;, &quot;type&quot;: 1,<br>            &quot;parent_id&quot;: parent.id, &quot;order&quot;: 1, &quot;depth&quot;: 1,<br>        }<br>        response = api_client.post(&quot;/categories/&quot;, data=payload, format=&quot;json&quot;)<br>        # then: 응답 및 DB 상태 검증<br>        assert response.status_code == 201<br>        assert response.json() == {&quot;message&quot;: &quot;카테고리가 생성되었습니다.&quot;}<br>        child = Category.objects.get(id=2)<br>        assert child.name == &quot;Child Category&quot;<br>        assert child.parent_id == parent.id</pre><p>Django에서는 @pytest.mark.django_db 데코레이터만 추가하면 테스트마다 독립된 트랜잭션이 자동으로 보장됩니다. Flask에서는 db_session fixture를 통해 직접 세션을 관리해야 하는 차이가 있지만, 테스트 코드 자체의 구조는 동일합니다.</p><h3>6. CI/CD 통합: GitHub Actions</h3><p>테스트 코드를 작성하는 것만으로는 충분하지 않습니다. <strong>코드가 배포되는 환경에서 기존 테스트가 항상 통과하는지 자동으로 확인</strong>해야 합니다. 그렇지 않으면 테스트가 깨진 채로 머지되거나, 아무도 테스트를 실행하지 않게 됩니다. GitHub Actions를 통한 CI 연동은 <strong>선택이 아니라 필수</strong>입니다.</p><p>기본적인 흐름은 <strong>checkout → Python 설정 → 의존성 캐싱 → 테스트 실행 → 커버리지 코멘트</strong>입니다. push 시에는 테스트만, PR 시에는 커버리지 리포트까지 생성하도록 분기합니다. <a href="https://github.com/py-cov-action/python-coverage-comment-action">py-cov-action/python-coverage-comment-action</a>을 사용하면 PR에 커버리지 코멘트를 자동으로 달 수 있습니다.</p><p>이렇게 설정하면:</p><ul><li><strong>PR을 올릴 때마다</strong> 자동으로 전체 테스트가 실행됩니다</li><li><strong>커버리지 리포트</strong>가 PR 코멘트로 표시됩니다</li><li>테스트가 실패하면 <strong>머지가 차단</strong>됩니다</li></ul><h3>7. 마주친 문제들과 해결법</h3><h3>MySQL 종속성 제거</h3><p>테스트 환경을 구축하면서 가장 많이 마주친 문제는 <strong>MySQL에 종속된 스키마와 코드</strong>였습니다. SQLite에서 테스트를 돌리려면 이 종속성을 제거해야 했고, 실제로 상당한 양의 모델 수정이 필요했습니다.</p><p><strong>MySQL 전용 데이터 타입을 표준 타입으로 변경:</strong></p><pre># Before: MySQL 전용 타입<br>text: Mapped[str] = mapped_column(db.Text(collation=&quot;utf8mb4_unicode_ci&quot;))<br>bulk_reporter: Mapped[int] = mapped_column(TINYINT)<br>story: Mapped[str] = mapped_column(MEDIUMTEXT)<br><br># After: 표준 타입으로 통일<br>text: Mapped[str] = mapped_column(db.Text)<br>bulk_reporter: Mapped[int] = mapped_column(db.SmallInteger)<br>story: Mapped[str] = mapped_column(db.Text)</pre><p><strong>SQLite에서 중복 불가능한 인덱스명 개선:</strong></p><p>SQLite는 데이터베이스 전체에서 인덱스명이 유일해야 합니다. MySQL에서는 테이블 단위로 인덱스명이 관리되기 때문에 &quot;cid_2&quot;, &quot;suid&quot; 같은 모호한 이름이 허용되지만, SQLite에서는 충돌합니다.</p><pre># Before: 모호하고 중복 가능한 인덱스명<br>db.Index(&quot;cid_2&quot;, &quot;cid&quot;, &quot;mid&quot;)<br>db.Index(&quot;suid&quot;, &quot;suid&quot;, &quot;duid&quot;)<br><br># After: 테이블명을 포함한 명확한 인덱스명<br>db.Index(&quot;idx_bt_backuped_model_cid_mid&quot;, &quot;cid&quot;, &quot;mid&quot;)<br>db.Index(&quot;idx_bt_memo_suid_duid&quot;, &quot;suid&quot;, &quot;duid&quot;)</pre><p><strong>복합 PrimaryKey를 UniqueKey로 변경:</strong></p><pre># Before: 복합 PrimaryKey<br>class BTBanner(db.Model):<br>    __tablename__ = &quot;BT_Banner&quot;<br>    __table_args__ = (<br>        UniqueConstraint(&quot;id&quot;, &quot;isall&quot;),<br>    )<br>    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)<br>    isall = db.Column(db.Integer, nullable=False, primary_key=True)  # 복합 PK</pre><pre># After: 단일 PrimaryKey + UniqueConstraint<br>class BTBanner(db.Model):<br>    __tablename__ = &quot;BT_Banner&quot;<br>    __table_args__ = (<br>        UniqueConstraint(&quot;id&quot;, &quot;isall&quot;, name=&quot;uq_bt_banner_id_isall&quot;),<br>    )<br>    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)<br>    isall = db.Column(db.Integer, nullable=False)  # primary_key=True 제거</pre><p>SQLite는 복합 PrimaryKey를 지원하지 않기 때문에, id 컬럼에 PrimaryKey를 걸고 기존 복합키는 UniqueConstraint로 변경했습니다.</p><p>이런 작업은 단순히 테스트를 통과시키기 위한 조치가 아닙니다. ORM을 사용하는 이유 중 하나가 DB 벤더 독립성인데, MySQL 전용 타입이나 문법을 사용하면 그 이점을 스스로 포기하는 셈입니다. 테스트 환경 구축은 이런 숨어있던 종속성을 발견하고 정리하는 좋은 기회였습니다.</p><h3>멀티 데이터베이스 라우터</h3><p>프로덕션에서는 읽기/쓰기 DB가 분리되어 있지만, 테스트에서는 단일 SQLite DB를 사용합니다. Django에서는 DATABASE_ROUTERS = []로 라우터를 비활성화하고, Flask에서는 slave_session도 같은 세션으로 교체했습니다.</p><h3>8. 중요한 것들</h3><h3>완벽한 환경보다 돌아가는 환경이 먼저</h3><p>처음부터 높은 커버리지를 목표로 하지 않았습니다. 먼저 pytest가 에러 없이 실행되는 환경을 만드는 것에 집중했고, 이후 하나씩 테스트를 추가해나갔습니다.</p><h3>레거시 코드의 테스트는 격리가 핵심</h3><p>10년 가까이 운영된 코드베이스에는 수많은 외부 의존성이 얽혀 있습니다. 이 모든 것을 실제로 연결하려 하면 끝이 없습니다. 대신 <strong>외부 의존성을 철저히 Mock/Fake로 교체</strong>하고, <strong>비즈니스 로직만 집중적으로 테스트</strong>하는 전략을 택했습니다.</p><h3>GitHub Actions로 안전장치 만들기</h3><p>이렇게 테스트 환경을 구축한 뒤에 꼭 중요한 것이 있습니다. 새로운 코드를 추가할 때마다 테스트를 돌려봐야 한다는 것이죠. 그런데 사람이 하는 일이다 보니 놓칠 수도 있고, 검증 없이 커밋하는 경우도 생깁니다. 그래서 GitHub Actions로 안전장치를 꼭 구축해놔야 합니다. PR마다 자동으로 테스트가 돌고, 실패하면 머지가 차단되도록 설정하면 검증을 빠뜨릴 일이 없습니다.</p><h3>마무리</h3><p>4개 프로젝트에 테스트 환경을 구축하면서 느낀 점은, <strong>테스트 환경 구축 자체가 코드베이스에서 의존성 문제를 확인할 수 있다는 점입니다.</strong></p><p>하지만 한번 구축되면, 이후의 개발 속도와 안정성은 확연히 달라집니다. 새로운 기능을 추가할 때 기존 코드가 깨지지 않는지 자동으로 확인할 수 있고, 리팩토링도 자신감 있게 진행할 수 있습니다. 실제로 다음 편에서 다룰 대규모 리팩토링은 이 테스트 환경이 없었다면 시도조차 하기 어려웠을 것입니다.</p><p>레거시 프로젝트에 테스트를 도입하는 것은 쉽지 않지만, <strong>가장 가치 있는 기술 부채 상환</strong> 중 하나입니다.</p><p>다음 글에서는 이렇게 구축한 테스트 환경을 기반으로, <strong>AI를 활용해 레거시 코드를 도메인 중심의 견고한 구조로 리팩터링한 과정</strong>을 공유합니다. 테스트 코드라는 안전망 위에서 어떻게 자신감 있게 코드를 바꿔나갔는지, 그 여정을 함께 살펴보겠습니다.</p><p><strong>[시리즈]</strong></p><ul><li><strong>1편:</strong> <a href="https://medium.com/@mjgo_68337/ai%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C-%ED%83%88%EC%B6%9C-%ED%8C%8C%ED%8E%B8%ED%99%94%EB%90%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81-%EA%BF%B0%EB%9A%AB%EA%B8%B0-7bf32bd81c7f">AI와 함께한 레거시 탈출: 파편화된 비즈니스 로직 꿰뚫기</a></li><li><strong>2편:</strong> AI와 함께한 레거시 탈출: 안전망 깔기 (pytest 도입기) <strong>← 현재 글</strong></li><li><strong>3편:</strong> <a href="https://medium.com/@mjgo_68337/ai%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C-%ED%83%88%EC%B6%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%9D%BC%EB%8A%94-%EC%9A%A9%EA%B8%B0%EB%A1%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C%EB%A5%BC-%ED%95%B4%EC%B2%B4%ED%95%98%EA%B8%B0-90d40938e109">AI와 함께한 레거시 탈출: 테스트라는 용기로 레거시를 해체하기</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2a6b09d8f35b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/babitalk-blog/ai%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C-%ED%83%88%EC%B6%9C-%EC%95%88%EC%A0%84%EB%A7%9D-%EA%B9%94%EA%B8%B0-pytest-%EB%8F%84%EC%9E%85%EA%B8%B0-2a6b09d8f35b">AI와 함께한 레거시 탈출: 안전망 깔기 (pytest 도입기)</a> was originally published in <a href="https://medium.com/babitalk-blog">Babitalk</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AI와 함께한 레거시 탈출: 파편화된 비즈니스 로직 꿰뚫기]]></title>
            <link>https://medium.com/babitalk-blog/ai%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C-%ED%83%88%EC%B6%9C-%ED%8C%8C%ED%8E%B8%ED%99%94%EB%90%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81-%EA%BF%B0%EB%9A%AB%EA%B8%B0-7bf32bd81c7f?source=rss----4433725e2350---4</link>
            <guid isPermaLink="false">https://medium.com/p/7bf32bd81c7f</guid>
            <dc:creator><![CDATA[Mjgo]]></dc:creator>
            <pubDate>Mon, 30 Mar 2026 09:02:15 GMT</pubDate>
            <atom:updated>2026-03-30T09:02:14.593Z</atom:updated>
            <content:encoded><![CDATA[<h3>1. 서론: AI, 백엔드 개발자의 가장 강력한 무기</h3><p>여러분들은 AI를 실무에서 어떻게 활용하고 계신가요? 백엔드 개발자라면 최근 실무 환경이 놀라울 정도로 편해졌음을 체감하고 계실 겁니다. 모든 비즈니스 로직이 백엔드에 집중되어 있기 때문에, 아무리 복잡하고 사람이 읽기 어려운 <strong>레거시 코드</strong>일지라도 AI가 그 맥락을 정확하게 파악해주기 때문입니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*44LdJDgl_gHtm9gCMiSvNw.png" /></figure><p>바비톡은 현재 <a href="https://medium.com/babitalk-blog/%EB%B0%94%EB%B9%84%ED%86%A1-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EA%B8%B0%EB%B0%98-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-msa-%EC%A0%84%EB%9E%B5%EA%B3%BC-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%97%AC%EC%A0%95-9c7873901227"><strong>MSA(Microservice Architecture)로의 전환</strong></a>이라는 중요한 기술적 도전을 진행하고 있습니다. 이 과정에서 우리는 어쩔 수 없이 이전의 레거시 코드와 마주해야 합니다.</p><h3>2. 바비톡의 도전: 비즈니스의 세 축을 잇는 4개의 레포지토리</h3><p>바비톡의 비즈니스는 사용자, 병원, 그리고 플랫폼(바비톡)이라는 세 축을 중심으로 돌아갑니다. 하지만 이를 뒷받침하는 기존 코드는 특별한 아키텍처 없이 빠르게 구현하는 데 집중했던 Django 기반의 레거시로 이뤄져 있습니다. 하나의 클래스에 모든 비즈니스 로직과 DB 연결까지 포함된 거대한 코드들이 주요 네 개의 레포지토리에 분산되어 있죠.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vR_z-DvwDbA5Cqr8gor2MQ.png" /></figure><ul><li><strong>App</strong>: 모바일 앱에서 사용하는 API를 담당하는 서버.</li><li><strong>Client</strong>: 병원 관계자들이 사용하는 관리자 페이지.</li><li><strong>Admin</strong>: 바비톡 운영팀(직원)이 사용하는 내부 관리 도구 및 정산 로직.</li><li><strong>Cron</strong>: 주기적인 데이터 계산 및 배치 스크립트가 모여있는 레포지토리.</li></ul><p>정책 하나를 파악하려면 이 네 개의 레포지토리를 모두 확인 해봐야 합니다. 신규 입사자에게는 그야말로 거대한 미로와 같았습니다. 어떠한 기능이 어떻게 동작하는지 알기 위해 네 개의 레포지토리를 하나하나 다 확인 해야 했으니까요.</p><h3>3. 사례: ‘이벤트 결제’와 ‘인기 점수’의 연결고리</h3><h3>도메인 파악의 시작</h3><p>입사 후 처음 맡은 업무는 인기점수 산정 방식의 변경 구현이었습니다.</p><p>인기점수는 바비톡의 이벤트의 노출 순서를 보여주는데에 사용되고 있습니다. 이를 구현하기 위해서는 당연히 기존 인기점수가 어떻게 산정되고 있는지부터 확인해야 했죠. 기존대로라면 코드의 구현을 확인하고, ERD를 확인하고, 그 관계를 하나하나 이해해야 했습니다. 하지만 이제는 도메인 파악을 Cladue와 함께합니다. 크론 스크립트가 위치한 경로에서 Claude에게 이렇게 물었습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Iap0RQRUtlPqbq7QbxS5XA.png" /><figcaption>“@babitalk_cron 인기점수 로직은 현재 어떤 비즈니스 로직으로 구현되어 있는지 파악해줘”</figcaption></figure><p>파일 경로를 포함한 이 간단한 질문만으로도 Claude는 코드 간의 관계를 분석하여 비즈니스 로직을 체계적으로 정리해 줍니다.</p><h3>관련 로직 전체 탐색</h3><p>이전 방식이라면 테이블 명세서를 펼쳐놓고 모델 간의 관계, 흩어져있는 비즈니스 로직을 수동으로 하나하나 추적해야 했습니다. 하지만 Claude에게는 아래와 같이 질문하는 것만으로 충분합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*i7F0dWjRVZukoUSzUgYxhA.png" /><figcaption>“이벤트 결제 로직을 확인하려고 해. <em>event_payment </em>모델과<em> </em>관련된 모든 비즈니스 로직을 확인해줘&quot;</figcaption></figure><p>미처 확인하지 못한 로직이 존재할 수 있으므로, 해당 테이블을 참조하는 모든 로직을 빠짐없이 분석하는 것이 중요합니다.</p><h3>실행 위치 선정의 중요성</h3><p>대략적인 관계를 파악한 뒤, 구현 방법을 모색하는 단계로 넘어갔습니다. 이때 <strong>Claude의 실행 위치를 어디로 잡느냐</strong>가 핵심입니다.</p><p>바비톡의 주요 비즈니스 로직은 약 4개의 레포에 걸쳐 있습니다. 평소에는 하나의 레포 내부에서 AI를 돌리지만, 도메인 단위로 분리되지 않은 구조에서는 하나의 도메인에 대한 로직이 여러 레포에 흩어져 있을 수 있습니다. 따라서 4<strong>개의 레포가 모두 포함된 상위 경로에서 Claude를 실행</strong>하는 것이 좋습니다. Claude가 참조할 수 있는 범위가 넓어질수록, 더 정확한 분석과 제안을 받을 수 있기 때문입니다.</p><h3>구현 방향 설계</h3><p>충분한 컨텍스트가 확보된 상태에서 다음과 같이 요청했습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oQvAACdRx2UEIcDLGqz9zg.png" /><figcaption>이벤트 결제를 어떻게 구현하는게 좋을지 방법을 소개해줘</figcaption></figure><p>Claude는 여러 가지 구현 플랜을 제시합니다. 이 중 가장 적합한 방법을 골라 구현을 진행하면 됩니다. 만약 선택한 방법이 복잡하거나 이해가 어렵다면, 반복적인 질문을 통해 구현 세부 사항을 충분히 파악한 뒤 진행하는 것이 좋습니다.</p><h3>4. 기획 문서부터 코드까지, AI로 연결하기</h3><p>여러분은 기획 문서 관리에 어떤 툴을 사용하고 계신가요? 바비톡은 <strong>Notion</strong>을 적극적으로 활용하고 있습니다. PO분들이 Notion에 요구사항을 상세히 적어주면, 저희는 이를 단순히 읽는 데 그치지 않고 AI와 함께 분석합니다.</p><h3>Notion MCP를 활용한 요구사항과 코드 검증</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/564/1*LQGjysQPt0RypaW5d0a96A.png" /><figcaption>Notion MCP를 통해서 코드를 검증</figcaption></figure><p><strong>MCP(Model Context Protocol)</strong>를 활용하면 Notion에 기록된 기획 문서와 실제 코드를 실시간으로 비교할 수 있습니다.</p><ul><li><strong>기획 확인</strong>: 기획자가 정의한 새로운 이벤트 결제 정책이 무엇인지 AI가 Notion에서 읽어옵니다.</li><li><strong>코드 대조</strong>: 해당 정책이 현재 레거시 코드의 도메인 로직과 충돌하는 지점은 없는지, 혹은 이미 구현된 로직 중 재사용 가능한 부분은 무엇인지 AI가 분석합니다.</li></ul><p>바비톡에서는 개발을 시작 하기전에, <a href="https://medium.com/babitalk-blog/%EC%82%AC%EC%9D%BC%EB%A1%9C-%ED%98%91%EC%97%85-%EA%B0%9C%EB%B0%9C%EC%9A%A9-%EB%A6%AC%EB%B7%B0-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EB%8F%84%EC%9E%85-5bada6b6b5ba">스코프리뷰</a>를 통해서 영향범위를 사일로멤버들(목적 조직) 및 백엔드팀(기능 조직)에게 공유하고 있습니다. 원래는 노션과 피그마를 통해서 하나하나 요구사항을 파악해야 했는데요, 이제는 Claude를 통해서 그 과정을 간소화 하고있습니다. 그리고 아래와 같이 SKILL을 만들어서 그 과정을 자동화 하고 있습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*InKNM7b7dg1B9LWblvv49Q.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gSVPrEWqzo9kidtNjzPGGw.png" /><figcaption>스코프리뷰 작성을 위한 claude skill(좌), 스코프리뷰 노션 템플릿(우)</figcaption></figure><p>이 과정을 통해 기획과 개발 사이의 간극을 줄이고, “코드는 이렇게 되어 있는데 기획은 왜 이렇지?”라는 의문을 개발 착수 전 단계에서 빠르게 해소할 수 있었습니다.</p><h3>5. 결과: 자연어 파악을 넘어 코드로 녹여내기</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*m_6gn_EP2TJNrC7nOCgcTg.png" /></figure><p>AI를 통해 도메인 로직을 자연어로 파악하는 것은 첫 단추일 뿐입니다. 진짜 중요한 것은 파악된 내용을 바탕으로 <strong>새로운 코드를 추가하고, 기존 코드를 비즈니스 로직에 맞게 정리하여 녹여내는 과정</strong>입니다.</p><p>이 과정에서 <strong>리팩터링</strong>은 필수적입니다. 하지만 레거시 코드를 건드리는 것은 언제나 두려운 일이죠. 리팩터링을 하기 전에는 반드시 기존 로직이 여전히 잘 돌아간다는 것을 보장해야 합니다. 즉, <strong>테스트 코드</strong>가 필수적이라는 뜻입니다.</p><p>사실 바비톡의 주요 레포지토리들에는 아직 테스트 코드가 충분히 갖춰져 있지 않은 상태였습니다. 도메인을 파악하고 리팩터링을 하려니, 이 안전장치의 부재가 가장 큰 걸림돌이 되었습니다.</p><h3>6. 마무리: 다음 여정을 향하여</h3><p>AI 덕분에 우리는 레거시라는 거대한 미로에서 빠르게 탈출할 수 있었습니다. AI로 코드를 파악하고, 기획자의 의도를 쉽게 파악하고, 코드와 비교할 수 있었습니다. 하지만 파악된 지식을 코드로 안전하게 옮기기 위해서는 또 다른 도전이 필요했습니다.</p><p>다음 글에서는 바비톡의 주요 레포지토리에 <strong>테스트 코드 환경을 구축한 생생한 구축기</strong>를 담아보겠습니다. 또한, 테스트 코드를 기반으로 어떻게 안심하고 코드를 수정하고 리팩터링했는지 그 과정도 함께 공유해 드릴게요.</p><p><strong>[시리즈]</strong></p><ul><li><strong>1편:</strong> <a href="https://medium.com/@mjgo_68337/ai%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C-%ED%83%88%EC%B6%9C-%ED%8C%8C%ED%8E%B8%ED%99%94%EB%90%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81-%EA%BF%B0%EB%9A%AB%EA%B8%B0-7bf32bd81c7f">AI와 함께한 레거시 탈출: 파편화된 비즈니스 로직 꿰뚫기</a> <strong>← 현재 글</strong></li><li><strong>2편:</strong> <a href="https://medium.com/@mjgo_68337/ai%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C-%ED%83%88%EC%B6%9C-%EC%95%88%EC%A0%84%EB%A7%9D-%EA%B9%94%EA%B8%B0-pytest-%EB%8F%84%EC%9E%85%EA%B8%B0-2a6b09d8f35b">AI와 함께한 레거시 탈출: 안전망 깔기 (pytest 도입기)</a></li><li><strong>3편:</strong> <a href="https://medium.com/@mjgo_68337/ai%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C-%ED%83%88%EC%B6%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%9D%BC%EB%8A%94-%EC%9A%A9%EA%B8%B0%EB%A1%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C%EB%A5%BC-%ED%95%B4%EC%B2%B4%ED%95%98%EA%B8%B0-90d40938e109">AI와 함께한 레거시 탈출: 테스트라는 용기로 레거시를 해체하기</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7bf32bd81c7f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/babitalk-blog/ai%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C-%ED%83%88%EC%B6%9C-%ED%8C%8C%ED%8E%B8%ED%99%94%EB%90%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81-%EA%BF%B0%EB%9A%AB%EA%B8%B0-7bf32bd81c7f">AI와 함께한 레거시 탈출: 파편화된 비즈니스 로직 꿰뚫기</a> was originally published in <a href="https://medium.com/babitalk-blog">Babitalk</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[서버리스 기반 마이크로 서비스(MSA) 전략과 마이그레이션 여정]]></title>
            <link>https://medium.com/babitalk-blog/%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4-%EA%B8%B0%EB%B0%98-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-msa-%EC%A0%84%EB%9E%B5%EA%B3%BC-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%97%AC%EC%A0%95-1ab12eec26c4?source=rss----4433725e2350---4</link>
            <guid isPermaLink="false">https://medium.com/p/1ab12eec26c4</guid>
            <category><![CDATA[바비톡]]></category>
            <category><![CDATA[microservice-architecture]]></category>
            <category><![CDATA[aws-lambda]]></category>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[babitalk]]></category>
            <dc:creator><![CDATA[Kychoi]]></dc:creator>
            <pubDate>Fri, 02 Jan 2026 16:40:39 GMT</pubDate>
            <atom:updated>2026-02-27T05:38:55.862Z</atom:updated>
            <content:encoded><![CDATA[<h4>AWS Lambda와 Event Bridge와 같은 AWS Serverless를 활용한 실용주의적 MSA를 소개합니다.</h4><p><a href="https://medium.com/babitalk-blog/%EB%B0%94%EB%B9%84%ED%86%A1-%EC%95%B1-%ED%98%84%EB%8C%80%ED%99%94-app-modernization-%EC%A0%84%EB%9E%B5%EA%B3%BC-%EC%A0%84%EC%88%A0-8ed200b0281e">이전 글</a>에서 바비톡의 앱 현대화 여정의 큰 그림(Macro View)을 보여드렸다면, 이번 글에서는 그 중심에 있는 <strong>서버리스 기반 마이크로서비스(MSA) 전환의 구체적인 전략과 실행 과정(Micro View)</strong>을 공유하려 합니다.</p><p>많은 스타트업이 서버리스의 장밋빛 미래만 보고 섣불리 도입했다가, 예상치 못한 비용 증가나 Cold Start로 인한 성능 저하, 그리고 디버깅의 어려움 때문에 다시 기존 방식으로 회귀하곤 합니다. 이번 포스팅에서는 바비톡이 이러한 시행착오 없이, 어떻게 안정적이고 실용적으로 서버리스를 도입하여 안착시켰는지 그 치열한 기술적 여정(Migration Journey)을 상세히 공유합니다.</p><h3>[Technical Deep Dive] 적재적소에 서버리스를 적용하여 이벤트 기반 마이크로서비스 생태계 구축</h3><p><a href="https://medium.com/babitalk-blog/%EB%B0%94%EB%B9%84%ED%86%A1-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EA%B8%B0%EB%B0%98-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-msa-%EC%A0%84%EB%9E%B5%EA%B3%BC-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%97%AC%EC%A0%95-9c7873901227">지난 포스팅</a>에서는 거대한 모놀리스를 <strong>AWS ECS 기반의 마이크로서비스</strong>로 분해하는 ‘주력 부대’의 이야기를 다뤘습니다. 하지만 현대화된 시스템에서 모든 서비스가 24시간 내내 실행되는 무거운 컨테이너일 필요는 없습니다.</p><p>사용 빈도가 불규칙하거나, 메인 비즈니스 로직과 비동기로 분리되어야 하는 ‘보조 서비스’들은 더 가볍고 유연해야 합니다. 오늘은 <strong>AWS Lambda</strong>와 <strong>EventBridge</strong>를 활용해 구축한 <strong>서버리스 기반의 이벤트 구동형(Event-Driven) 아키텍처</strong>와, 자칫 파편화되기 쉬운 서버리스 생태계를 “CDK와 CDD(Configuration Driven Development)”로 어떻게 플랫폼 엔지니어링했는지 공유합니다.</p><h3>1. Why Serverless? (왜 서버리스인가)</h3><p>우리가 메인 서비스는 컨테이너(ECS)로, 보조 서비스는 서버리스(Lambda)로 이원화 전략을 취한 이유는 명확합니다.</p><ol><li><strong>동적 트래픽 대응 (Elasticity):</strong> 마케팅 알림 발송(Message)나 데이터 분석 동기화 트리거 같은 기능은 트래픽이 0이었다가 순간적으로 수만 건이 몰리는 ‘스파이크’ 패턴을 가집니다. 서버리스는 별도의 오토스케일링 설정 없이도 즉각적으로 반응합니다.</li><li><strong>비용 효율성 (Cost):</strong> 항상 켜져 있어야 하는 EC2/ECS와 달리, 요청이 없을 때는 비용이 정확히 ‘0원’입니다.</li><li><strong>운영 해방 (NoOps):</strong> 조직이 커감에 따라 소수의 DevOps 인원으로 모든 서버를 관리하고 업데이트하기는 불가능합니다. 서버리스는 OS 관리나 보안 패치 없이 비즈니스 로직(Code)에만 집중할 수 있습니다. 즉 클릭 몇번이면 관련된 리소스들을 생성하고 오토스케일링할 수 있습니다.</li></ol><h4>💡 Kafka vs MSK vs EventBridge?</h4><p>PubSub 기반으로 이벤트 메시지를 주고 받기 위해서 <strong>이벤트 버스</strong>는 필연적으로 선택해야 합니다. 직접 설치하거나 관리형 서비스를 사용하거나 클릭 한번으로 생성 가능한 서버리스를 고려할 수 있습니다. 바비톡은 아래 솔루션들의 장단점 비교를 통해서 <strong>AWS EventBridge</strong>를 선택했습니다. 선택은 그리 어렵지 않았습니다. 왜냐하면 바비톡은 Serverless First 전략을 취하고 있기 때문입니다.</p><p>따라서 시스템적인 기능 제약이나 트래픽/성능에 대한 성능 제약이 없다면 서버리스를 먼저 고민합니다. AWS EventBridge는 클릭 한번으로 수분만에 이벤트 버스를 생성할 수 있으며, 추가적인 복잡한 유지보수(오토스케일링, 서버 구축, 보안 설정, 패치 업데이트)를 할 필요가 없습니다. 특히 바비톡의 트래픽 범위와 유스케이스를 고려한다면 단연 최고의 선택이었습니다.</p><ul><li><strong>Kafka/MSK:</strong> 인프라 관리가 필요하고 기본 비용이 높습니다. 물론 최적화 측면에서 비용과 보안 그리고 성능에 대한 노하우가 많이 필요합니다.</li><li><strong>EventBridge:</strong> 완전 관리형(Serverless)이며, 사용한 만큼만 지불합니다. 무엇보다 AWS 생태계(Lambda, S3, DynamoDB)와의 연동성이 압도적입니다.</li></ul><h3>2. Implementation Approach: 아키텍처 개요</h3><p>우리는 Pub/Sub 이벤트 모델에 몇가지 규칙을 정해서 시스템을 구성했습니다.</p><ul><li><strong>Publisher:</strong> ECS 컨테이너나 다른 Lambda 함수가 이벤트를 발행합니다. 주로 앞단의 메인 API 서버들이 이벤트를 발생시킵니다.</li><li><strong>Event Bus:</strong> <strong>AWS EventBridge</strong>가 이벤트를 받아 규칙(Rule)에 따라 라우팅합니다. 이때 규칙 설정은 외부에서 쉽게 json으로 일관된 규약을 선언형으로 지정하면 내부적으로는 AWS CDK로 모듈화되어 동작합니다.</li><li><strong>Subscriber: </strong>가장 일반적인 케이스들은 Lambda 함수로<strong> </strong>이벤트를 직접 받아 로직을 수행합니다. 람다 서비스 내에서 직접 이벤트 구독 기능을 제공하기 때문에 구독 관리를 할 필요가 없어서 매우 편리하고 안정적입니다. 물론 복잡한 로직을 수행해야할 경우는 Lagacy Conterner들이 이벤트를 직접 구독 관리해야 합니다. 하지만 이럴 경우에 직접 이벤트 구독 관리를 해야하기 때문에 Proxy Lambda를 앞단에 정의하고 이 람다 함수가 이벤트를 먼저 받은 후 API 호출로 컨테이너에 중계합니다.</li></ul><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*98P6kxUWGxYdjDT2h6gkbw.png" /></figure><h3>3. 핵심 전략 1: 이벤트 메시지의 표준화 (Standardization)</h3><p>이벤트 기반 아키텍처(EDA)의 가장 큰 위험은 “메시지 포맷의 파편화”입니다. 각 팀이 제멋대로 이벤트를 던지면 소비(Subscribe)하는 쪽은 지옥이 됩니다. 우리는 <strong>Strict한 JSON 스키마</strong>를 정의하여 이를 해결했습니다.</p><h4>💡표준 메시지 구조</h4><p>EventBridge의 기본 포맷(DetailType, Source) 안에 우리만의 Detail 구조를 심었습니다.</p><ul><li><strong>Source:</strong> 마이크로서비스 이름 (enum 관리, 예: app_api)</li><li><strong>DetailType:</strong> 1차 구독 토픽으로써 Usecase나 Event 기반으로 정의 (예: batch_completed message_requested)</li><li><strong>Detail: </strong>커스톰하게 정의할 수 있는 영역으로써 우리는 내부에 Kind와 Body를 정의하였습니다. Kind는 2차 분류 토픽 (촘촘한 구독 필터링용)으로 동작하고, Body는 Payload로써 다시common(공통 필드) + specific(이벤트 특화 필드)으로 이뤄집니다.</li></ul><pre>{<br>    &quot;version&quot;: &quot;0&quot;,<br>    &quot;id&quot;: &quot;5fb1f9a2-6e7d-b60d-7332-801d5dd3e46b&quot;,<br>    &quot;detail-type&quot;: &quot;1st_level_topic_completed&quot;,<br>    &quot;source&quot;: &quot;xxx_yyy_service&quot;,<br>    &quot;account&quot;: &quot;account_number&quot;,<br>    &quot;time&quot;: &quot;2024-07-04T16:01:18Z&quot;,<br>    &quot;region&quot;: &quot;region_name&quot;,<br>    &quot;resources&quot;: [],<br>    &quot;detail&quot;: {<br>        &quot;Kind&quot;: &quot;2nd_level_topic_v1&quot;,<br>        &quot;Body&quot;: {<br>            &quot;common&quot;: {<br>                &quot;com_key1&quot;: &quot;xxx&quot;<br>                &quot;com_key2&quot;: &quot;yyy&quot;<br>            },<br>            &quot;specific&quot;: {<br>                &quot;spec_key1&quot;: &quot;xxx&quot;,<br>                &quot;spec_key2&quot;: &quot;yyy&quot;<br>            }<br>        }<br>    }<br>}</pre><h3>4. 핵심 전략 2: IaC와 CDD를 통한 서버리스 인프라 템플릿화 (Serverless Infrastructure Templates)</h3><p>수백 개의 서버리스 함수를 관리하면서 가장 큰 문제는 “구성의 파편화”였습니다. 어떤 함수는 S3 업로드를 감지해야 하고, 어떤 함수는 EventBridge 메시지를 받아야 하며, 또 어떤 함수는 VPC 내부의 RDS에 접근해야 합니다.</p><p>우리는 이 모든 요구사항을 단 하나의 CDK 스택 클래스(One Stack Source)로 통합하고, 외부 설정 파일(JSON) 주입만으로 “다양한 형상의 람다(Multiple Configurations)”를 찍어낼 수 있는 <strong>CDD(Configuration Driven Development)</strong> 환경을 구축했습니다. 즉 아래 그림과 같이 람다 함수를 추상화하고 이를 트리거하는 외부 요소들을 표준화하여 <strong>JSON</strong> 규격에 선언하여 주입만하면 다양 조합의 람다 함수를 구성 및 배포할 수 있도록 하였습니다.</p><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*tIOWrCWXM-RZk-2Q90gV9g.png" /></figure><h4>① 필수 이벤트 트리거의 표준화</h4><p>우리는 가장 빈번하게 사용되는 4대 이벤트 트리거를 표준화하여 CDK 모듈에 내재화했습니다. 이들은 자유롭게 조합 가능합니다.</p><ol><li><strong>EventBridge:</strong> BusArn, Source, DetailType만 입력하면 자동 연결</li><li><strong>S3 Bucket:</strong> 버킷 이름과 Prefix만 지정하면 파일 이벤트 감지</li><li><strong>DynamoDB Stream:</strong> 테이블 변경 사항(Stream) 감지</li><li><strong>Time Schedule</strong>: Cron 방식이나 일정한 Rate로 감지</li></ol><h4>② One Source, Multiple Configurations (실전 CDD 예시)</h4><p>아래는 실제로 우리가 사용 중인 <strong>복합 구성 JSON</strong>의 전체 예시입니다. 개발자는 복잡한 인프라 코드를 한 줄도 건드리지 않고, 이 JSON 파일 하나만 작성하면 [VPC + 복합 트리거 + IAM 권한 + Layer]이 완벽하게 세팅된 서버리스 환경을 배포할 수 있습니다.</p><pre>{<br>      &quot;BaseName&quot;: &quot;BannerOperation&quot;,<br>      &quot;VpcEnable&quot;: true,<br>      &quot;Triggers&quot;: [<br>          {<br>              &quot;TriggerType&quot;: &quot;TriggerS3Bucket&quot;,<br>              &quot;BucketName&quot;: &quot;xxx-yyy-zzz&quot;,<br>              &quot;TriggerPrefix&quot;: &quot;feature_name/live/config/&quot;<br>          },<br>          {<br>              &quot;TriggerType&quot;: &quot;TriggerEventBridge&quot;,<br>              &quot;BusArn&quot;: &quot;arn:aws:events:region:account:event-bus/bus_name&quot;,<br>              &quot;Source&quot;: [&quot;app_api&quot;, &quot;cron_batch&quot;],<br>              &quot;DetailType&quot;: &quot;1st_level_topic&quot;,<br>              &quot;Detail&quot;: {<br>                &quot;Kind&quot;: &quot;2nd_level_topic&quot;<br>              }<br>          },<br>          {<br>              &quot;TriggerType&quot;: &quot;TriggerS3Bucket&quot;,<br>              &quot;BucketName&quot;: &quot;babitalk-resource-crm&quot;,<br>              &quot;TriggerPrefix&quot;: &quot;white_sentence/live/config/&quot;<br>          },<br>          {<br>              &quot;TriggerType&quot;: &quot;TriggerTimeScheduler&quot;,<br>              &quot;Type&quot;: &quot;CronExpression&quot;,<br>              &quot;CronExpression&quot;: &quot;0 1* * ? *&quot;,<br>              &quot;Description&quot;: &quot;매일 새벽 1시에 실행&quot;<br>          }<br>      ],<br>      &quot;LambdaFunction&quot;: {<br>          &quot;BaseName&quot;: &quot;XxxYyyFunc&quot;,<br>          &quot;CodePath&quot;: &quot;repo_name/domain_name/feature_name/src&quot;,<br>          &quot;Environment&quot;: {<br>              &quot;DdbTableName&quot;: &quot;table_name&quot;,<br>              &quot;StageName&quot;: &quot;Live&quot;<br>          },<br>          &quot;LayerVersionArns&quot;: [<br>              &quot;BBT-DevOpsCommon-Dev-XxxYyy1&quot;,<br>              &quot;BBT-DevOpsCommon-Dev-XxxYyy2&quot;<br>          ],<br>          &quot;ReservedConcurrentExecutions&quot;: 1,<br>          &quot;Timeout&quot;: 10,<br>          &quot;JsonIamPolicies&quot;: [<br>              {<br>                  &quot;Effect&quot;: &quot;Allow&quot;,<br>                  &quot;Action&quot;: [<br>                      &quot;dynamodb:PutItem&quot;,<br>                      &quot;dynamodb:GetItem&quot;,<br>                      &quot;dynamodb:Query&quot;,<br>                      &quot;dynamodb:BatchWriteItem&quot;<br>                  ],<br>                  &quot;Resource&quot;: &quot;arn:aws:dynamodb:region:account:table/table_name&quot;<br>              }<br>          ]<br>      },<br>      &quot;SubnetIds&quot;: [<br>          &quot;subnet-0000000aaaaaaaaaa&quot;,<br>          &quot;subnet-1111111bbbbbbbbbb&quot;<br>      ],<br>      &quot;SecurityGroupIds&quot;: [<br>          &quot;sg-0000000000000000&quot;<br>      ]<br>  }</pre><h4>③ 이 설정 파일이 처리하는 마법</h4><p>위 JSON이 환경 변수를 통해 위부 주입되면, AWS CDK 내부에서 다음과 같은 작업을 자동으로 수행합니다.</p><ul><li><strong>Multi-Trigger Binding:</strong> Triggers 배열을 순회하며 각 조건에 맞는 Trigger들을 동록합니다.</li><li><strong>Secure Networking:</strong> VpcEnable: true를 감지하여 람다를 인터넷이 차단된 Private Subnet에 배치하고 RDS 접근 경로를 엽니다.</li><li><strong>Least Privilege IAM:</strong> JsonIamPolicies를 파싱하여 정확히 필요한 테이블에 대한 접근 권한만 부여합니다.</li></ul><h3>5. 핵심 전략 3: 비즈니스 로직 템플릿화</h3><p>인프라가 자동화되었다면, 그 안에서 돌아갈 <strong>애플리케이션 코드</strong>의 생산성을 높여야 합니다. 바비톡은 python을 기본 언어로 사용하기 때문에 python 템플릿 코드를 구성하여 다음과 같이 기본 기능을 제공합니다. 각 백엔드 개발자는 핵심 로직만 템플릿에 추가하는 형태로 애플리케이션 코드 작업을 하게 됩니다.</p><ul><li><strong>기본 뼈대 코드: </strong>람다 입력값에 대한 유효성을 검증한 후, topic 별 라우팅 기능을 제공합니다. 구독하는 topic 별로 tempalte method pattern을 적용하여 실제 비즈니스 로직을 구현합니다. 이때 입출력 데이터들을 Pydantic을 이용하여 전달됩니다.</li><li><strong>Utility Pack:</strong> AWS Secrets Manager 연동(DB 암호 관리), MySQL Connection Pool, Event Publish 기능을 헬퍼로 제공합니다.</li><li><strong>Local Test Environment:</strong> pipenv와 pytest를 통해 AWS 배포 없이 로컬에서 완벽하게 디버깅할 수 있는 환경을 제공합니다.</li></ul><h3>6. 적용 사례 (Use Cases)</h3><p>서버리스 적용은 철저히 메인 서비스보다는 보조 서비스들에 적용되고 있습니다. 여기서 보조 서비스들의 특징은 트래픽 증가가 순식간에 수백배 늘어나 예측하기 어렵거나 반대로 트래픽 자체가 매우 간헐적으로 호출되어 서버를 항시 준비할 필요가 없는 케이스들입니다.</p><ul><li><strong>💬 message_box (통합 메시징):</strong> API 서버나 배치 서버가 “메시지 발송 요청” 이벤트를 던지면, 람다가 이를 받아 알림톡/SMS/이메일/Push 등 채널 별로 발송합니다. 이렇게 이벤트 기반 비동기 처리로 메시지들을 핸들링하면 메인 서비스 서버 요청의 응답 속도를 단축하고 3rd Party 솔루션의 종속성을 제거하기 때문에 매우 안정적으로 서비스를 운영할 수 있게 됩니다.</li></ul><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*W2F6JnVCyi4zuoYXUTendQ.png" /></figure><ul><li><strong>🎯 crm_box (마케팅 자동화):</strong> CRM 팀이 타겟 유저를 설정하면, 동시 다발적으로 웹훅이 발동되어 람다가 동작하며, 이를 통해서 맞춤형 쿠폰 지급이나 배너 광고를 세팅합니다. 이런 마케팅 트래픽은 매우 다이나믹하여 기본적으로 오토스케일링이 내장된 람다에서 매우 유지보수가 편합니다. 관련된 사항은<a href="https://cshub.ab180.co/ko/case-studies/braze-webhook-babitalk-coupon"> 브레이즈 고객 사례</a>에 소개되었습니다.</li></ul><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*uqMXZPks9xsh6kpIn9L3yg.png" /></figure><ul><li><strong>📊 data_box (데이터 파이프라인):</strong> 데이터 분석 팀의 통계/추론/추천 작업이 끝나면 이벤트를 트리거하여, 람다가 운영 DB에 통계 데이터를 적재합니다. 이 경우는 아주 간헐적으로 트리거되기 때문에 굳이 서버를 7/24 내내 운영할 필요가 없는 케이스라서 비용 절감에 큰 장점이 있습니다.</li></ul><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*Eai1l6tYZICfyeTcBxl_9g.png" /></figure><h3>마치며: 기술의 변화는 문화의 변화를 동반한다!</h3><p>이번 서버리스 마이그레이션 프로젝트를 통해 얻은 가장 큰 인사이트는 두 가지입니다.</p><p><strong>첫째, “Serverless와 CDK는 최고의 파트너”라는 점입니다.</strong> 서버리스 아키텍처는 수많은 작은 리소스(Function, Role, Rule, Trigger, Layer)들의 집합입니다. 이를 사람이 손으로 관리하는 것은 불가능에 가깝습니다. 특히 유사한 구성을 알파/배타/감마와 같은 배포 단계에 구성해야할 경우에는 더욱 그렇습니다. <strong>AWS CDK</strong>를 통해 인프라를 코드로 정의하고 모듈화했기에, 우리는 복잡한 서버리스 생태계를 통제 가능한 수준으로 유지할 수 있었습니다.</p><p><strong>둘째, 기술보다 어려웠던 것은 “팀의 인식 변화(Mindset Shift)”였습니다.</strong> 우리 개발팀은 오랫동안 “서버는 24시간 켜져 있고(EC2/Container), 내가 접속해서 로그를 볼 수 있는(Stateful) 대상”이라는 인식이 강했습니다.</p><ul><li><em>“서버가 없는데 로그는 어디서 봐요?”</em></li><li><em>“요청이 없을 때 꺼지면, 다시 켜질 때 느리지 않나요?(Cold Start)”</em></li></ul><p>이런 의구심을 해소하기 위해 수많은 기술 세션과 핸즈온(Hands-on)을 진행했습니다. 서버리스가 주는 ‘비용 효율성’과 ‘운영 해방(NoOps)’의 가치를 팀원들이 체감하고, 아키텍처에 자연스럽게 녹여내기까지는 꽤 긴 설득과 학습의 시간이 필요했습니다. 예를 들어 구체적으로 다음과 같은 세션들을 Server Community Session을 통해서 BE 개발자, 플랫폼 개발자, DevOps 개발자분들에게 전파하였습니다.</p><ul><li>AWS CDK 소개</li><li>현재 우리의 서버 구성 상태</li><li>서버리스 크론 도입 소개</li><li>서버리스와 자동화</li><li>이벤트 기반 비동기 처리</li><li>람다 함수 템플릿 로직과 헬퍼 제공</li></ul><p>하지만 지금, 우리 팀은 메인 로직은 단단한 컨테이너로, 보조 로직은 유연한 서버리스로 구현하는 ‘하이브리드 마이크로서비스’를 자유자재로 구사하고 있습니다. 이것이 진정한 앱 현대화(App Modernization)의 완성이 아닐까 합니다.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1ab12eec26c4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/babitalk-blog/%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4-%EA%B8%B0%EB%B0%98-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-msa-%EC%A0%84%EB%9E%B5%EA%B3%BC-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%97%AC%EC%A0%95-1ab12eec26c4">서버리스 기반 마이크로 서비스(MSA) 전략과 마이그레이션 여정</a> was originally published in <a href="https://medium.com/babitalk-blog">Babitalk</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[사용자가 페이지 로딩을 눈치채지 못하게 만들기 — RSC 도입 이야기]]></title>
            <link>https://medium.com/babitalk-blog/%EC%82%AC%EC%9A%A9%EC%9E%90%EA%B0%80-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A1%9C%EB%94%A9%EC%9D%84-%EB%88%88%EC%B9%98%EC%B1%84%EC%A7%80-%EB%AA%BB%ED%95%98%EA%B2%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0-rsc-%EB%8F%84%EC%9E%85-%EC%9D%B4%EC%95%BC%EA%B8%B0-ae28a7c5f669?source=rss----4433725e2350---4</link>
            <guid isPermaLink="false">https://medium.com/p/ae28a7c5f669</guid>
            <category><![CDATA[frontend-development]]></category>
            <category><![CDATA[react]]></category>
            <category><![CDATA[tech]]></category>
            <dc:creator><![CDATA[DongHyunKim]]></dc:creator>
            <pubDate>Fri, 02 Jan 2026 09:03:45 GMT</pubDate>
            <atom:updated>2026-02-27T05:36:55.959Z</atom:updated>
            <content:encoded><![CDATA[<h3>사용자가 페이지 로딩을 눈치채지 못하게 만들기 — RSC 도입 이야기</h3><h3>도입 배경</h3><p>이 글에서는 Next.js App Router 기반의 글로벌 서비스에서 RSC(React Server Components)와 TanStack Query를 함께 활용해 이벤트 상세 페이지의 LCP를 약 38% 개선한 경험을 공유하려고 합니다.</p><p>문제 상황을 어떻게 정의했고, 어떤 기술적 선택을 통해 성능을 개선했는지 과정을 중심으로 정리해보았습니다.</p><p>현재 바비톡 글로벌 서비스는 Next.js 13부터 도입된 App Router 기반으로 구성되어 있습니다. 기본적으로 SPA 환경에서는 페이지 이동 시 CSR(Client-Side Rendering) 방식으로 동작하기 때문에, 서버 컴포넌트를 사용하지 않거나 별도의 대응이 없다면 데이터 페칭 과정에서 레이아웃 시프트가 발생할 가능성이 있습니다.</p><h3>레이아웃 시프트(Layout Shift)에 대한 고민</h3><figure><img alt="바비톡 테크 블로그 레이아웃 시프트 예시" src="https://cdn-images-1.medium.com/max/642/1*zdlMJ9tT98oIL25gmpeDQQ.png" /><figcaption>레이아웃 시프트 예시</figcaption></figure><p>실제 예시로 데이터 패칭 관련해서는 현재 TanStack Query를 사용중입니다. TanStack Query를 사용할 경우 클라이언트 훅(useQuery)을 포함하므로 ‘use client’를 명시해 주어야 합니다. 이러한 과정에서 서버에서 렌더링되어 내려올 때 useQuery 훅이 클라이언트에서만 실행되므로, SSR 시점에는 데이터가 없어 isLoading 상태로 사용자에게 첫 페이지가 보여지게 됩니다.</p><p>사용자 입장에서는 Skeleton ui처럼 별도의 loading 처리가 없는 경우에 페이지가 로딩되는 과정을 그대로 노출되게 되고 레이아웃 시프트가 일어나기 때문에 사용자 경험 측면에서 상당히 부정적인 경험으로 받아 들여지게 됩니다. 이러한 부정적 경험이 쌓여 서비스의 품질을 결정하고 나아가서는 서비스의 이미지에 영향을 줄 수 있다고 생각하기 때문에 새로 오픈하는 글로벌 서비스에서 사용자들에게 좋은 인상을 남겨주고 싶었습니다.</p><h3>사용자가 불편함을 느끼지 못하게 사이트 탐색을 할 순 없을까?</h3><p>위에 고민과 더불어 서비스를 운영하는 개발자라면 사용자가 서비스를 이용함에 있어 불편함 없이 페이지 렌더링이나 진입 속도를 최적화하고 0.1초라도 빠르게 서비스를 구성하려는 목표를 갖고 있을 것입니다. 렌더링 1초 차이에 따라 사업적인 관점에서는 페이지가 빠르면 사용자는 자연스럽게 더 많은 페이지를 둘러보고, 사이트에 더 오래 머무르게 됩니다. 이는 곧 <strong>콘텐츠 소비량 증가</strong>로 이어지는 과정이라고 생각합니다. 반대로 사용자에게 빠른 페이지 전환은 서비스 이용 만족도를 높이는 중요한 요소가 됩니다.</p><p>따라서 사용자 입장에서 가장 중요하다고 볼 수 있는 이벤트 상세 페이지 렌더링 관련하여 기술적으로 변경이 필요하다고 생각이 들었습니다.</p><h3>기술/프랙티스/방법론 검토</h3><p>레이아웃 시프트(Layout Shift)와 FCP 외에도 SEO도 고려해야 했기 때문에 서버 렌더링을 필수로 적용하기로 고려했고 결과적으로 2가지 방식으로 방법을 추릴 수 있었습니다.</p><h3>SSR(Server Side Rendering)을 활용한 방식</h3><p>첫번째로는 SSR(Server Side Rendering)을 활용하여 HTML을 서버에서 렌더링해서 완성된 형태로 클라이언트에 전달하는 방식으로 초기 HTML이 이미 완성된 형태로 전달받기 때문에 레이아웃 시프트를 방지할 수 있습니다.</p><pre>## CSR (React CRA 등)<br>1. HTML은 &lt;div id=&quot;root&quot;&gt;&lt;/div&gt; 뿐.<br>2. JS가 로드될 때까지 빈 화면.<br>3. JS가 실행되면 한꺼번에 렌더 → 레이아웃 시프트 발생 가능.<br><br>## SSR (Next.js 등)<br>1. 서버에서 완성된 HTML이 전달됨.<br>2. 브라우저가 즉시 레이아웃 계산 가능 → 초기 시프트 적음.</pre><p>또한 CSR과 비교했을 때 초기 렌더링이 빠르고, 콘텐츠가 즉시 보이기 때문에 FCP(First Contentful Paint)가 개선되어 사용자 입장에서도 빠른 페이지 렌더링을 통한 좋은 경험을 줄 수도 있습니다.</p><p>하지만 완전한 방지는 아쉽게도 불가능합니다. SSR을 해도 다음과 같은 이유로 레이아웃 시프트는 여전히 발생할 수 있습니다.</p><ol><li><strong>이미지 크기가 미리 지정되지 않음</strong></li></ol><ul><li>&lt;img&gt;에 width/height 속성이 없으면 로드 후 공간이 밀립니다.</li></ul><p><strong>2. 웹폰트 FOUT/FOIT</strong></p><ul><li>폰트가 늦게 로드되면 글씨 크기가 바뀌면서 시프트가 생깁니다.</li></ul><p><strong>3. 동적 콘텐츠 삽입</strong></p><ul><li>광고, Lazy-loaded 컴포넌트, 비동기 데이터 등으로 레이아웃이 바뀔 수 있습니다.</li></ul><p><strong>4. Hydration 시점 차이</strong></p><ul><li>SSR된 HTML과 클라이언트 렌더 결과가 다를 경우, React나 Next.js가 재조정(reconciliation)하면서 살짝 흔들릴 수 있습니다.</li></ul><h3>RSC(React Server Component)를 활용한 방식</h3><p>두번째로는 RSC(React Server Component)를 활용하여 FCP 속도 개선과 레이아웃 시프트를 방지하는 것입니다. RSC는 SSR와 동일하게 서버에서 동작하는 방식이지만 렌더링 방식 자체는 완전히 다릅니다. RSC 같은 경우는 SSR처럼 페이지 전체를 <strong>HTML로 만드는 게 아니라 React 컴포넌트 자체</strong>를 서버에서 실행하고, 결과를 클라이언트에 컴포넌트 트리 구조로 전달 합니다.</p><p>즉 페이지 단위 렌더링이 아닌 <strong>컴포넌트 단위로 렌더링</strong>이 이루어지며, RSC는 서버 컴포넌트를 렌더링한 결과를 Streaming SSR을 활용하여 SSR보다 빠른 렌더링을 제공합니다.</p><p>자세한 RSC의 렌더링 방식은 아래와 같이 동작하게 됩니다.</p><p><strong>서버에서의 렌더링 동작</strong></p><ol><li>RSC는 서버로 요청이 들어오면 React Server Component 트리를 렌더링한 뒤, 그 결과를 <strong>RSC Payload</strong>로 생성합니다. 이 Payload는 <strong>React Flight Protocol</strong>이라는 전용 스트리밍 프로토콜을 통해 직렬화되어 클라이언트로 전답 됩니다.</li><li>이 과정에서 서버는 컴포넌트 트리를 HTML로 통째로 변환하지 않고, <strong>클라이언트의 React 런타임이 컴포넌트 결과를 재구성할 수 있는 정보</strong>만을 스트리밍합니다. 결과적으로 브라우저는 서버에서 내려온 Payload를 바탕으로 필요한 DOM을 점진적으로 구성하게 됩니다.</li><li>생성한 RSC Payload 와 클라이언트 컴포넌트 자바스크립트 인스트럭션(<strong>웹페이지에 동적 기능을 추가하기 위해 사용되는 js 명령어, setState에 들어가는 기본 init값을 생각하면 쉬움</strong>)을 활용하여 최종 HTML을 스트리밍 형태로 생성합니다</li></ol><p><strong>RSC Payload 구성</strong></p><ul><li>서버 컴포넌트의 JSX 렌더링 결과</li><li>빈자리 (placeholder 처리) <br>서버 컴포넌트는 클라이언트 컴포넌트 포함이 가능하므로 해당 클라이언트 컴포넌트와 그에 필요한 자원을 응답받아야 하기 때문에 이를 기억해야 합니다. 즉 클라이언트의 렌더 파일 위치를 기억하게 됩니다.</li><li>props (서버 컴포넌트(SC)가 클라이언트 컴포넌트(CC)에 전달하는 props들)</li></ul><p>따라서 1~3번 과정이 서버에서 일어난 이후에 클라이언트에서는 아래와 같이 동작하게 됩니다.</p><p><strong>클라이언트에서의 렌더링 동작</strong></p><ol><li>html을 응답 받으면, 먼저 즉시 보여준다(Streaming SSR)</li><li>기존 SSR은 모든 준비가 끝나고 완성된 페이지를 보여주지만 스트리밍 기능을 활용하면 먼저 생성된 html을 보여주고 생성된 순서대로 순차적으로 페이지를 보여주게 됩니다. 렌더링이 끝나지 않은 부분은 Suspense로 대처 가능합니다.</li></ol><pre>&lt;Page&gt;<br>  &lt;Header /&gt; // 즉시 렌더링됨 → 먼저 전송             <br>  &lt;Suspense fallback={&lt;Skeleton /&gt;}&gt; // 데이터 대기 중<br>    &lt;Posts /&gt;             <br>  &lt;/Suspense&gt;<br>&lt;/Page&gt;</pre><p>3. RSC Payload 를 가지고 reconcile(재조정) 단계가 발생하게 됩니다. 왜냐하면 클라이언트 컴포넌트를 포함하고 있으면 이러한 빈자리를 채워서 트리 구조를 형성하기 때문입니다.</p><p>4. hydration 과정이 일어나게 됩니다. html에 리액트를 끼얹어서 상호작용이 가능하도록 합니다. 해당 과정에서 클라이언트 컴포넌트의 상호작용이 들어가게 됩니다.</p><p>하지만 RSC도 레이아웃 시프트를 완전히 방지할 수 있는 것은 아닙니다. 물론 next/fetch를 사용하여 서버에서 api를 호출하여 미리 데이터를 활용 할 수 있게 추가적인 세팅을 통해 해결할 수 있긴 합니다. 그럼 해당 방식을 조금 더 편안하게 핸들링할 수 있도록 TanStack Query를 통한 방법을 알아 보겠습니다.</p><h3>TanStack Query와 같이 RSC를 활용하는 방법</h3><p>fetch 대신에 TanStack Query를 사용하는 이유는 크게 3가지로 생각을 했었고 아래와 같습니다.</p><ol><li>fetch()는 “요청 한 번 → 응답 한 번” 단순한 네트워크 호출</li></ol><pre>const res = await fetch(&#39;/api/posts&#39;); const data = await res.json();</pre><p>이렇게 하면 데이터를 불러오긴 하지만 로딩 중이나 다른 상태값을 처리하려고 하면 useState와 useEffect등 직접 설정이 필요하고 에러 발생 시 try/catch 수동 처리, 다른 컴포넌트에서 같은 데이터를 다시 fetch 등 중복 요청이 발생하게 됩니다.</p><p>2. TanStack Query는 데이터 상태에 대하여 기본적으로 처리할 수 있는 기능이 있어 별도 설정이 필요 없습니다.</p><ul><li>로딩 / 에러 / 성공 상태 자동 관리</li><li>데이터 캐싱 + 중복 요청 방지</li><li>탭 전환 시 자동 리패칭</li><li>캐시 시간(TTL) 설정 가능</li><li>Optimistic Update, Infinite Query 등 기능 제공</li></ul><p>3. TanStack Query는 서버에서 데이터를 쿼리 캐시에 담은 뒤 dehydrate 상태로 직렬화하여 클라이언트로 전달하고, 클라이언트에서는 HydrationBoundary를 통해 해당 상태를 복원(hydrate)하는 구조로 서버 데이터를 클라이언트 캐시에 전달할 수 있습니다.</p><pre>// 서버에서 미리 fetch<br>const queryClient = new QueryClient();<br>await queryClient.prefetchQuery([&#39;posts&#39;], fetchPosts);<br><br>return (<br> &lt;HydrationBoundary state={dehydrate(queryClient)}&gt;<br>  &lt;Posts /&gt;<br> &lt;/HydrationBoundary&gt;<br>);</pre><p>가장 중요한 내용으로 서버에서 데이터 호출하고 그 캐시를 클라이언트로 **직렬화(dehydrate)**해서 전달하게 되면 클라이언트에서는 **재활성화(rehydrate)**해서 그대로 쓸 수 있습니다.</p><p>fetch와 <strong>TanStack Query는 간략하게</strong> 아래 표로 정리하였습니다.</p><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*8x9wDoIRxfSULXqaC0SstA.png" /></figure><h3>최종 선택/확정</h3><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*CR8H3ct5wcC4KL7P_4lnnQ.png" /></figure><p>최종적으로 위와 같은 내용을 토대로 RSC와 TanStack Query를 활용하여 prefetch하는 방식이 최적의 방법이라 생각을 했습니다.</p><p>따라서 바비톡 글로벌 이벤트 상세 페이지에 적용하기로 결정하였고 TanStack Query의 Hydration 부분은 HydrationBoundary, dehydrate, prefetch 호출단을 각각 기능에 따라 나눠서 관리하고 있습니다. 이유는 역할을 분리해서 RSC 데이터 파이프라인을 예측 가능하고 확장 가능하게 만들기 위해서 입니다.</p><p>실제로 해당 호출단을 분리해서 dehydrate 하는 부분에 Promise.all을 활용하여 여러 API 호출을 병렬 처리함으로써 서버 응답 시간을 단축시켰습니다.</p><h3>현재 모습</h3><p><strong>RSC로 변경 후 실제 렌더링 성능 차이</strong></p><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*jbJTNH9XCT8H3T6ToPcU9g.png" /></figure><p>기존 CSR 기반 구조에서는 클라이언트가 페이지를 요청한 뒤 API 응답을 기다리는 동안 <strong>빈 화면(white screen)</strong> 상태가 노출되었습니다. 이로 인해 초기 렌더링 지연과 <strong>레이아웃 시프트(Layout Shift)</strong> 문제가 발생했습니다.</p><p>하지만 RSC와 <strong>서버 prefetch(dehydrate)</strong> 를 결합한 이후에는 데이터가 서버에서 미리 패칭되어 HTML에 포함된 상태로 전달되므로, 클라이언트에서는 별도의 데이터 로딩 지연 없이 즉시 콘텐츠를 확인할 수 있었습니다.</p><p>특히 <strong>LCP가 약 7.7초 → 4.8초로 38% 개선</strong>된 점은 특히 의미가 있다고 생각합니다. Lighthouse의 LCP 측정 로직은 “브라우저가 인식 가능한 최종 페인트 타이밍” 기준이기 때문에 실제로 사용자 입장에서 <strong>페이지가 더 빠르고 안정적으로 로드되는 체감 개선</strong>이 이루어졌다고 볼 수 있습니다.</p><figure><img alt="바비톡 테크 블로그 개선전 트리맵" src="https://cdn-images-1.medium.com/max/1024/1*gKRd7ryHctaFrf7oW8w6ag.png" /><figcaption>개선 전 트리맵</figcaption></figure><figure><img alt="바비톡 테크 블로그 개선 후 트리맵" src="https://cdn-images-1.medium.com/max/1024/1*CXttgAQrYEQCw-oKG88HpA.png" /><figcaption>개선 후 트리맵</figcaption></figure><p>트리맵만 확인하더라도 개선 후에는 레이아웃 시프트 없이 보다 빨리 페이지가 렌더링 되는 부분을 확인할 수 있었습니다. 그럼 마지막으로 실제 live 서버에서 렌더링 속도를 확인 하는 것으로 마무리 하겠습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*pkEoYei6lTT5HopftOx5QQ.gif" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/500/1*vQeb_duVhiM_Z4ia5k22Eg.gif" /><figcaption>개선 전 / 개선 후 live 렌더링 속도 비교</figcaption></figure><p>두 비교 GIF에서 확인할 수 있듯이 개선 후에는 페이지 진입 시 첫 콘텐츠가 즉시 표시되고 주요 이미지와 텍스트가 자연스럽게 로드되어 사용자가 <strong>‘기다린다’는 느낌 없이 콘텐츠를 소비할 수 있는 흐름</strong>이 만들어졌습니다.</p><p>단순히 수치상의 개선뿐만 아니라, 실제 체감 속도와 상호작용 반응성이 모두 향상 되었으며 이는 곧 <strong>사용자 경험(UX) 품질의 실질적 향상</strong>으로 이어졌습니다. 앞으로도 이러한 성능 최적화 방향을 유지하며, 사용자 입장에서 더 빠르고 안정적인 웹 환경을 지속적으로 제공할 계획입니다.</p><h3>향후 진행</h3><p><strong>RSC는 공짜가 아니다. 성능과 비용 사이의 선택</strong></p><p>RSC 방식은 서버 사이드에서 컴포넌트 단위로 React를 실행하고, 이를 직렬화하여 클라이언트로 전달하기 때문에 일반 CSR 또는 SSR보다 <strong>서버 리소스를 상대적으로 더 많이 소모</strong>합니다.</p><p>특히 RSC는 요청이 발생할 때마다 서버에서 렌더링 로직이 수행되며, 서버에서의 CPU 연산 및 네트워크 I/O 부하가 증가하게 됩니다. 트래픽이 높은 서비스에서는 이러한 부하가 누적되면 서버 스케일링 비용과 응답 지연(latency) 문제로 이어질 수 있습니다.</p><p>따라서 모든 페이지에 RSC를 일괄 적용하는 것은 비효율적이라고 판단했습니다.</p><p>RSC의 주요 장점은 <strong>초기 렌더링 속도 개선</strong>과 <strong>JS 번들 사이즈 감소</strong>에 있지만, 모든 페이지가 이 최적화의 우선순위를 필요로 하는 것은 아닙니다. 예를 들어 사용자 진입 빈도가 낮거나, 서버 데이터 의존도가 낮은 정적 페이지는 기존 SSG(Static Site Generation)나 CSR만으로도 충분히 좋은 성능을 낼 수 있습니다.</p><p>이러한 이유로, <strong>서비스 트래픽 데이터 기반의 선택적 적용 전략</strong>을 계획하고 있습니다.</p><p>구체적으로는 아래와 같은 방식으로 진행할 예정입니다.</p><ol><li><strong>사용자 탐색 데이터 분석</strong></li></ol><ul><li>내부 로그를 활용해 <strong>사용자 진입 빈도</strong>와 <strong>체류 시간</strong>, <strong>전환율이 높은 페이지</strong>를 식별합니다.</li><li>예를 들어 이벤트 상세 페이지나 홈 피드처럼 유입률이 높고, 콘텐츠 노출 속도가 UX에 직접 영향을 주는 영역이 우선 적용 대상이 됩니다.</li></ul><p><strong>2. RSC 적용 기준 수립</strong></p><ul><li>서버 렌더링 비용 대비 렌더링 성능 향상 폭을 정량적으로 측정합니다.</li><li>CLS, FCP, TTFB 등의 <strong>Web Vitals 지표</strong>를 기준으로 개선 효과가 명확한 페이지에만 RSC를 적용합니다.</li><li>반대로, 정적 데이터 위주의 페이지나 잦은 서버 재요청이 필요 없는 페이지는 SSG로 유지합니다.</li></ul><p><strong>3. 서버 부하 관점의 모니터링 및 점진적 확장</strong></p><ul><li>RSC 적용 후에는 서버 자원 사용량(CPU, 메모리, 응답 시간)을 지속적으로 모니터링하며 <strong>서버 부하가 임계값을 넘지 않는 선에서 점진적으로 확장 적용</strong>할 계획입니다.</li><li>이를 통해 RSC의 장점을 극대화하면서도 서버 운영 비용을 안정적으로 유지할 수 있습니다.</li></ul><p>요약하자면, RSC는 분명히 성능과 사용자 경험 측면에서 강력한 장점을 제공합니다.</p><p>그러나 그만큼 <strong>서버 자원 소비와 인프라 비용</strong>도 함께 늘어나기 때문에, <strong>모든 페이지가 아닌 “렌더링 품질이 직접적인 UX 지표에 영향을 주는 주요 페이지” 중심으로 선택적 적용</strong>하는 전략이 가장 현실적이라고 판단했습니다.</p><p>이런 접근을 통해 RSC의 장점을 효율적으로 활용하면서도 서비스 안정성과 비용 효율성을 동시에 확보할 수 있을 것입니다.</p><h3>참고 문서</h3><p><a href="https://nextjs.org/docs/app/getting-started/server-and-client-components">Getting Started: Server and Client Components | Next.js</a></p><p><a href="https://tanstack.com/query/latest/docs/framework/react/guides/prefetching">https://tanstack.com/query/latest/docs/framework/react/guides/prefetching</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ae28a7c5f669" width="1" height="1" alt=""><hr><p><a href="https://medium.com/babitalk-blog/%EC%82%AC%EC%9A%A9%EC%9E%90%EA%B0%80-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A1%9C%EB%94%A9%EC%9D%84-%EB%88%88%EC%B9%98%EC%B1%84%EC%A7%80-%EB%AA%BB%ED%95%98%EA%B2%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0-rsc-%EB%8F%84%EC%9E%85-%EC%9D%B4%EC%95%BC%EA%B8%B0-ae28a7c5f669">사용자가 페이지 로딩을 눈치채지 못하게 만들기 — RSC 도입 이야기</a> was originally published in <a href="https://medium.com/babitalk-blog">Babitalk</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[컨테이너 기반 마이크로 서비스(MSA) 전략과 마이그레이션 여정]]></title>
            <link>https://medium.com/babitalk-blog/%EB%B0%94%EB%B9%84%ED%86%A1-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EA%B8%B0%EB%B0%98-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-msa-%EC%A0%84%EB%9E%B5%EA%B3%BC-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%97%AC%EC%A0%95-9c7873901227?source=rss----4433725e2350---4</link>
            <guid isPermaLink="false">https://medium.com/p/9c7873901227</guid>
            <category><![CDATA[aws-cdk]]></category>
            <category><![CDATA[msa]]></category>
            <category><![CDATA[babitalk]]></category>
            <category><![CDATA[바비톡]]></category>
            <category><![CDATA[aws-ecs]]></category>
            <dc:creator><![CDATA[Kychoi]]></dc:creator>
            <pubDate>Tue, 23 Dec 2025 03:14:31 GMT</pubDate>
            <atom:updated>2026-02-27T05:35:35.534Z</atom:updated>
            <content:encoded><![CDATA[<h4>AWS ECS와 Clean Architecture를 활용한 컨테이너 기반 실용주의적 MSA를 소개합니다.</h4><p><a href="https://medium.com/babitalk-blog/%EB%B0%94%EB%B9%84%ED%86%A1-%EC%95%B1-%ED%98%84%EB%8C%80%ED%99%94-app-modernization-%EC%A0%84%EB%9E%B5%EA%B3%BC-%EC%A0%84%EC%88%A0-8ed200b0281e">이전 글</a>에서 바비톡의 앱 현대화 여정의 큰 그림(Macro View)을 보여드렸다면, 이번 글에서는 그 중심에 있는 <strong>컨테이너 기반 마이크로서비스(MSA) 전환의 구체적인 전략과 실행 과정(Micro View)</strong>을 공유하려 합니다.</p><p>특히, 스타트업 환경에서 “이상적인 MSA”를 쫓다가 오버엔지니어링의 늪에 빠지지 않고, “현실적이고 실용적인 MSA”를 어떻게 구축했는지, 그 치열한 고민의 과정(Case Study)을 상세히 담았습니다.</p><h3>[Technical Deep Dive] 컨테이너 기반 실용주의적 MSA: AWS ECS와 Clean Architecture로 모놀리스 분해하기</h3><p>스타트업의 초기 시스템은 빠르게 기능을 찍어내기 위해 ‘역할 기반’의 거대한 모놀리스로 시작하는 경우가 많습니다. 우리도 예외는 아니었습니다. 각각의 앱 서버, 운영 관리자 서버, 병원 운영 서버 등 소위 ‘3대장 서버’가 모든 트래픽과 로직을 감당하고 있었습니다.</p><p>하지만 조직이 커지면서 이 구조는 한계에 봉착했습니다. 오늘은 우리가 이 거대한 모놀리스를 어떻게 <strong>AWS ECS 기반의 마이크로서비스</strong>로 쪼개고, 그 내부를 <strong>Clean Architecture</strong>로 단단하게 채웠는지 이야기합니다.</p><h3>1. 문제점: 고민의 시작</h3><h4>① “역할 기반” 서버의 한계</h4><p>우리의 레거시 시스템은 도메인(or 기능)이 아닌, 사용 주체(Role)에 따라 서버가 나뉘어 있었습니다. 즉 <strong>현재 상황은</strong> 앱 유저용 API 서버, 운영자용 Admin 서버, 병원용 Partner 서버가 각각 존재합니다.</p><p>이로 인해서 다음과 같은 Pain Point가 지속적으로 발생하게 되었습니다.</p><blockquote><strong>중복 개발:</strong> 예를 들어 ‘후기’ 기능은 유저도 쓰고 운영자도 관리해야 합니다. 하지만 서버가 다르니 프레임워크도 다르고, Database ORM 방식도 다르고, 결국 유사한 코드를 두벌 이상 개발(Double Coding)해야 했습니다.</blockquote><blockquote><strong>하드 카피의 악몽:</strong> 공통 로직을 공유할 방법이 마땅치 않아, 코드를 복사-붙여넣기(Hard Copy)하여 관리했습니다. 버그가 생기면 양쪽 다 수정해야 하는 유지보수의 지옥이 펼쳐졌습니다. 물론 프레임워크와 ORM이 서로 상이하여 구현과 쿼리 결과도 미세하게 다르게 적용되어 품질에도 이슈가 발생했습니다.</blockquote><blockquote><strong>불분명한 오너십:</strong> ‘충전금’ 기능이 터졌을 때, 앱 서버 담당자가 봐야 할지 관리자 서버 담당자가 봐야 할지 모호했습니다. 도메인 오너십이 산발적으로 흩어져 있었습니다.</blockquote><h4>② 왜 AWS ECS인가?</h4><p>우리의 레거시 시스템은 AWS Elastic Beanstalk으로 구성되어 있었습니다. 이 서비스는 DevOps 멤버가 없는 경우에 Backend 개발자가 선택할 수 있는 가장 쉽고 편리한 최선의 선택입니다. 특히 모놀리스 방식으로 로드밸런서와 EC2 인스턴스만 구성하는 심플한 경우에 매우 매력적입니다. 하지만 그만큼 구성 자체가 정형화되어 있어 커스토마이징하기가 어렵고 확장하는데 한계가 있습니다.</p><p>우리는 AWS ECS와 EKS 중에서 선택이 필요하였으며, 다음과 같은 이유로 최종적으로 AWS ECS를 선택하였습니다.</p><blockquote><strong>AWS 통합</strong>: 기존 AWS 서비스들과 손쉽게 통합 가능하여 AWS Cloud 친화적으로 구성 가능함</blockquote><blockquote><strong>기술적 요구사항</strong>: 핵심적인 기술적 요구 사항(안정성/확장성/유지보수성)을 총족하면서 동시에 필요 이상의 것들을 유지보수할 필요 없음</blockquote><blockquote><strong>간결함과 유지보수 편리함</strong>: 러닝 커브가 매우 낮으며, AWS CDK로 매우 쉽게 커스토마이징과 확장이 가능함</blockquote><p>물론 저희가 EKS를 사용하지 않는 것은 아니며, 오픈 소스 기반으로 에코 시스템들이 잘 제공되는 케이스들에 대해서는 자체적으로 서비스를 EKS 기반으로 운영하고 있습니다.</p><h3>2. 전략: 현실적인 분해 원칙 (Principles)</h3><p>우리는 “모든 것을 처음부터 다시 만들자”는 빅뱅 방식을 거부했습니다. 스타트업의 특성 상 기술 개발에 몰입하여 사업적인 개발을 뒤로 미룰 수 없기 때문입니다. 대신 “기존 것은 유지하되, 공통 기능부터 뜯어내자”는 실용적 원칙을 세웠습니다. 즉 우리는 무작정 서비스를 쪼개는 것이 아니라, “어떻게 안전하게 떼어내고(Decompose)”, “어떻게 효율적으로 연결할 것인가(Integrate)”에 대한 명확한 기술적 가이드라인을 세우고 시작했습니다. 이를 위해 검증된 MSA 패턴들을 우리 상황에 맞춰 재해석하고 조합했습니다.</p><h4>① 분해 전략 (Decomposition Strategy)</h4><ul><li><strong>비즈니스 능력에 따른 분해 (Decompose by Business Capability):</strong> 기존의 ‘앱/웹 서버’, ‘운영 서버’ 같은 기술적/역할적 경계를 허물고, ‘충전금’, ‘검색’, ‘인증’과 같이 비즈니스 가치를 창출하는 도메인 단위로 서비스를 정의했습니다. 이는 콘웨이 법칙(Conway’s Law)에 따라 조직 구조와 시스템 구조를 일치시키는 첫걸음이었습니다.</li><li><strong>도메인 주도 설계 (Domain Driven Design, DDD):</strong> 각 마이크로서비스 내부는 철저히 DDD 원칙을 따랐습니다. 서비스 간의 결합도를 낮추기 위해 Bounded Context(제한된 문맥)를 정의하고, 그 안에서 보편적 언어(Ubiquitous Language)를 사용하여 기획-개발-운영의 간극을 줄였습니다.</li></ul><h4>② 마이그레이션 전략 (Migration Strategy)</h4><ul><li><strong>스트랭글러 패턴 (Strangler Fig Pattern): </strong>거대한 레거시 시스템을 한 번에 교체하는 빅뱅 방식은 실패 확률이 높습니다. 우리는 덩굴 식물이 숙주 나무를 서서히 감싸 안아 결국 대체하듯, 점진적으로 두 단계로 나눠서 마이그레이션을 진행했습니다. 첫번째 단계에서는 기존 도메인/기능들을 공통의 마이크로서비스로 분리시켜 참조하도록 하고, 두번째 단계에서는 새로운 도메인/기능을 마이크로서비스로 추가하여 기존 트래픽을 점진적으로 이관했습니다.</li></ul><h4>③ 통합 및 라우팅 전략 (Integration &amp; Routing Strategy)</h4><p>MSA 도입 시 가장 큰 난관은 “수많은 서비스 엔드포인트를 클라이언트가 어떻게 관리할 것인가?”입니다. 우리는 다음 3가지 패턴을 복합적으로 고려했습니다.</p><ul><li><strong>API 게이트웨이 패턴 (API Gateway Pattern):</strong> 클라이언트가 수십 개의 마이크로서비스를 직접 호출하는 대신, 단일 진입점(Gateway)을 거치게 합니다. 이를 통해 인증(Auth), 로깅, 라우팅을 중앙화하여 클라이언트의 복잡성을 낮췄습니다.</li><li><strong>게이트웨이 집계 패턴 (Gateway Aggregation Pattern):</strong> 하나의 UI 화면을 그리기 위해 여러 서비스를 호출해야 할 때(예: 상품 정보 + 리뷰 + 추천), 클라이언트가 N번 요청하는 대신 게이트웨이가 이를 모아서(Aggregation) 한 번에 응답해 주는 구조를 지향했습니다. 이를 통해 네트워크 Latency를 획기적으로 줄였습니다.</li><li><strong>API 라우팅 패턴 (Path-Based Routing):</strong> 가장 현실적인 구현 전술로, AWS ALB(Application Load Balancer)의 Listener Rule을 활용했습니다. HTTP 요청의 URL Path를 기반으로 트래픽을 적절한 마이크로서비스(Target Group)로 분배하는 방식으로, 별도의 고비용 게이트웨이 솔루션 없이도 효과적인 라우팅을 구현했습니다.</li></ul><h3>3. 네트워크 아키텍처: 어떻게 연결할 것인가? (The Case Study)</h3><p>가장 치열하게 고민했던 것은 “클라이언트(App/Web)가 어떻게 마이크로서비스를 호출하게 할 것인가?”였습니다. 우리는 앞서 설계한 전략을 구현하기 위해 4가지 아키텍처를 검토했고, 비용과 효율성 그리고 실현 가능성을 모두 잡은 <strong>Case 4</strong>를 최종 선택했습니다.</p><h3>❌ Case 1: ALB to ALB Routing (기술적 불가)</h3><ul><li><strong>개념:</strong> 기존 Public ALB 뒤에 내부용 Internal ALB를 두어 라우팅하는 방식</li><li><strong>제약:</strong> AWS 아키텍처상 ALB가 다른 ALB를 Target으로 잡는 것을 직접 지원하지 않아 구현이 불가능했습니다.</li></ul><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*uOO1cWn23nBsB1S7DJ9UIg.png" /></figure><h3>❌ Case 2: 별도의 Public ALB 추가 (비용/복잡도 증가)</h3><ul><li><strong>개념:</strong> 마이크로서비스 전용 Public ALB를 하나 더 신설</li><li><strong>단점:</strong> 클라이언트(App/Web)가 두 개의 엔드포인트(Legacy용, MSA용)를 관리해야 하므로 코드 복잡도가 증가하고, ALB 비용이 이중으로 발생합니다.</li></ul><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/942/1*zSTQPbxTh7DMt5X7aTgofA.png" /></figure><h3>❌ Case 3: 별도의 API Gateway 추가 (성능/비용 이슈)</h3><ul><li><strong>개념:</strong> AWS API Gateway를 앞단에 두고 트래픽을 분기 처리</li><li><strong>단점:</strong> API Gateway는 강력하지만 단순 라우팅 용도로 쓰기엔 비용(Request당 과금)이 높고, 홉(Hop)이 늘어나면서 추가적인 Latency(지연)가 발생할 우려가 있었습니다.</li></ul><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*Lzle5SIBHxP2vYq7zqEbkA.png" /></figure><h3>✅ Case 4: Public ALB + Multi Target Group (최종 선택)</h3><p>우리는 기존 자원을 100% 재활용하면서 <strong>API Routing Pattern</strong>을 가장 효율적으로 구현한 <strong>Case 4</strong>를 선택했습니다. 이 방식의 가장 큰 특징은 내부 숨겨진 마이크로서비스들은 Internal ALB를 통해서 제공하는 동시에, 직접 외부 노출이 필요할 경우에는 Public ALB를 통해서 제공할 수 있다는 것입니다.</p><ul><li><strong>구현 방식:</strong></li></ul><ol><li>기존 Public ALB의 리스너 규칙(Listener Rule)을 활용합니다.</li><li>특정 Path(예: /api/v1/money/*)로 들어오는 요청을 감지합니다.</li><li>해당 요청을 기존 ECS Service가 아닌, <strong>신규 ECS Service가 연결된 Target Group</strong>으로 라우팅합니다.</li></ol><ul><li><strong>장점:</strong></li><li><strong>Zero Cost:</strong> 추가적인 로드밸런서나 게이트웨이 비용이 없습니다.</li><li><strong>Transparent Migration:</strong> 클라이언트는 엔드포인트 변경 없이 기존 도메인 그대로 호출하면 됩니다.</li><li><strong>High Performance:</strong> ALB의 고성능 라우팅을 그대로 이용하며 지연 시간을 최소화했습니다.</li></ul><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*kXzc7NkYAW8BtWh9L1W-3A.png" /></figure><h3>4. 내부 아키텍처: Clean Architecture &amp; Template Repo</h3><p>인프라가 준비되었다면, 그 안을 채울 코드의 ‘표준’이 필요합니다. 우리는 마이크로서비스가 난립하는 것을 막고 동일한 개발자 경험(DX: Developer Experience)을 제공하기 위해 표준 템플릿 프로젝트를 만들에 Git 저장소로 제공하였습니다. 따라서 신규 서비스 개발이 필요하면 템플릿 저장소를 Fork하여 빠르게 서비스 개발에 집중할 수 있게 되었습니다.</p><h4>🛠 Template Project: Python + Hexagonal/Layered Architecture</h4><ul><li><strong>Architecture Concept:</strong> 외부(Web, DB)와 내부(Domain Logic)를 분리하여, 프레임워크가 바뀌어도 비즈니스 로직은 보호받도록 설계</li><li><strong>Tech Stack:</strong> FastAPI, Pydantic, SQLAlchemy, Redis, AWS Adaptor, dependency-injector</li><li><strong>Logging &amp; APM</strong>: Sentry, Datadog, Hackle</li><li><strong>Build Tool:</strong> pipenv, uv</li><li><strong>Environment Configuration</strong>: pyenv + Pydantic Settings + AWS SecretsManager</li><li><strong>Deployment</strong>: Docker, AWS ECR &amp; ECS, AWS CodePipeline</li><li><strong>Naming Convention</strong>: PEP 8, snake_case</li></ul><h3>5. 실행 로드맵: 단계별 확산 (Milestone)</h3><p>우리는 이 전략을 5단계에 걸쳐 점진적으로 실행했습니다.</p><h4>Phase 1: 기존 서비스들로 기반 구축 (Foundation)</h4><ul><li>비즈니스 로직이 구현된 기존 서비스들을 독커 기반으로 재정비하고, 환변 변수들을 추출하여 외부에서 관리하도록 하였습니다.</li><li>AWS 클라우드에서 운영하기 위해 DevOps 측면에서는 다음과 같은 AWS Cloud Resource들을 추상화하여 AWS CDK로 템플릿화하여 스택 단위로 재사용가능하도록 했습니다.</li></ul><blockquote><strong>서비스 스택:</strong> Route53, WAF, ALB, TargetGroup, ECS, ECR</blockquote><blockquote><strong>배포 스택</strong>: CodePipeline, CodeBuild, CodeDeploy, GitHub, Slack Lambda</blockquote><blockquote><strong>모니터링 스택:</strong> CloudWatch, Metric, Alarm, Event Rule, Dashboard, Slack Lambda</blockquote><h4>Phase 2: 신규 서비스로 파일럿 수행 (Pilot)</h4><ul><li>MSA로 도입을 위해 “충전금 도메인 서비스”를 첫 타자로 선정했습니다. 회사의 매출과 직결되는 서비스인만큼 신중한 도입이 필요했습니다. 안전한 도입을 위해 두단계로 나눠서 진행했습니다.</li><li>첫 단계는 PoC 기반으로 빠르게 서비스를 런칭하기 위해서 비즈니스 로직을 마이그레이션하지 않고, 충전금이 사용되는 곳곳에서 이력만 로깅할 수 있도록 로깅 API만 제공하고 Microservice를 위한 전용 도메인 데이터용 테이블들만 정의하여 데이터를 쌓기만 하였습니다.</li><li>두번째 단계는 곳곳에 로깅하던 부분의 비즈니스 로직들을 신규 API를 만들고 마이그레이션하였습니다. 이 과정에서도 이미 만들어 놓은 로깅과 비교하면서 데이터 누락을 검증할 수 있었습니다.</li></ul><h4>Phase 3: 신규 서비스 확대 가속화 (Expansion)</h4><ul><li>새로운 서비스들도 쉽고 빠르게 MSA를 시작할 수 있도록 “충전금 도메인 서비스”에서 충전금 관련된 비즈니스 로직을 제거하고 기본 기능들만을 분리하여 template project를 만들고 git repository로 제공하도록 하였습니다.</li><li>이제 검증된 템플릿을 바탕으로 신규 서비스들는 template repository를 fork하는 방식으로 빠르게 찍어내기 시작했습니다. 검색 서비스를 필두로 AI 서비스, 인증 서비스 그리고 채팅 서비스들을 순차적으로 런칭했습니다. (마이그레이션 마이크로서비스: 충전금 서비스, 검색 서비스 / 신규 마이크로서비스: AI 서비스, 인증 서비스, 채팅 서비스)</li></ul><h4>Phase 4: 모놀리스 해체 (Decomposition)[진행 중]</h4><ul><li>이제 칼을 들어 기존 모놀리스 서버를 더욱 강하게 도려내기 시작했습니다. 각 컨텐츠 API 서버들과 관리자 백오피스 서버들에 공통으로 얽혀있던 로직들을 하나씩 떼어내어 새로운 서비스로 분리하기 시작했습니다.</li><li>또한 네트워크 구성적으로 ALB → Multiple Target Group →ECS Service 연결의 안정성을 검증했습니다.</li></ul><h4>Phase 5: 고도화 (Service Mesh)[진행 중]</h4><ul><li>서비스 간 통신이 복잡해짐에 따라, 향후 Istio나 App Mesh 같은 <strong>Service Mesh</strong> 도입을 고려하여 관측성(Observability)을 강화하고 있습니다.</li></ul><h3>6. 마치며: 기술은 비즈니스의 속도를 위해 존재한다!</h3><p>우리의 MSA 전환은 “넷플릭스가 하니까” 혹은 “멋있어 보여서” 시작한 것이 아닙니다. <strong>“중복 코드를 없애고, 재사용성을 높여 개발 속도를 높이고, 배포의 공포를 없애기 위해”</strong> 시작했습니다.</p><p>이 과정에서 현재 운영 및 개발 중인 <strong>사일로(스쿼드) 배포를 멈추지 않으면서 동시에 MSA화하는 것이 스타트업 비즈니스에서 매우 중요</strong>합니다. 바비톡이 선택한 방식은 비즈니스 속도를 유지하면서 안정적으로 진행되었다는 것입니다. 특히 무작정 모든 영역에 MSA화하는 것은 추후 유지보수가 더 어려워질 수 있기 때문에 조직의 성장과 조직 구성에 맞춰서 점진적으로 이뤄지고 있습니다.</p><p>AWS ALB/ECS와 CDK를 활용한 우리의 ‘Case 4 전략’은 교과서에 나오는 가장 우아한 아키텍처는 아닐지 모릅니다. 하지만 비용을 아끼고, 클라이언트의 변화를 최소화하며, 가장 빠르게 MSA로 넘어갈 수 있었던 <strong>가장 현실적인 정답</strong>이었습니다.</p><p>현재 1)AWS ECS 기반으로 서비스를 운영하고 계시고, 2)필요 이상의 DevOps 유지보수를 들이지 않으면서 동시에 3)빠르고 안정적으로 MSA화하고 싶으시다면 바비톡이 제안하는 아케텍처를 한번쯤 레퍼런스로 고민해보시면 좋을 것 같습니다.</p><h3>7. 레퍼런스</h3><ul><li>AWS ECS Multiple Target Group: <a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/register-multiple-targetgroups.html">https://docs.aws.amazon.com/AmazonECS/latest/developerguide/register-multiple-targetgroups.html</a></li><li>서비스 분해: <a href="https://medium.com/byungkyu-ju/%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4%ED%8C%A8%ED%84%B4-2-2%EC%9E%A5-2-2-53e7bd6c6ebf">https://medium.com/byungkyu-ju/마이크로서비스패턴-2-2장-2-2-53e7bd6c6ebf</a></li><li>Domain Driven Design: <a href="https://kkimsangheon.github.io/2019/06/06/msa1/">https://kkimsangheon.github.io/2019/06/06/msa1/</a></li><li>API Gateway Pattern: <a href="https://learn.microsoft.com/ko-kr/dotnet/architecture/microservices/architect-microservice-container-applications/direct-client-to-microservice-communication-versus-the-api-gateway-pattern">https://learn.microsoft.com/ko-kr/dotnet/architecture/microservices/architect-microservice-container-applications/direct-client-to-microservice-communication-versus-the-api-gateway-pattern</a></li><li>Gateway Aggregation Pattern: <a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/gateway-aggregation">https://learn.microsoft.com/en-us/azure/architecture/patterns/gateway-aggregation</a></li><li>Strangler Pattern: <a href="https://learn.microsoft.com/ko-kr/azure/architecture/patterns/strangler-fig">https://learn.microsoft.com/ko-kr/azure/architecture/patterns/strangler-fig</a></li><li>API Routing Pattern: <a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/api-routing-path.html">https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/api-routing-path.html</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9c7873901227" width="1" height="1" alt=""><hr><p><a href="https://medium.com/babitalk-blog/%EB%B0%94%EB%B9%84%ED%86%A1-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EA%B8%B0%EB%B0%98-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-msa-%EC%A0%84%EB%9E%B5%EA%B3%BC-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%97%AC%EC%A0%95-9c7873901227">컨테이너 기반 마이크로 서비스(MSA) 전략과 마이그레이션 여정</a> was originally published in <a href="https://medium.com/babitalk-blog">Babitalk</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[앱 현대화(App Modernization) 전략과 아키텍처 개선 여정]]></title>
            <link>https://medium.com/babitalk-blog/%EB%B0%94%EB%B9%84%ED%86%A1-%EC%95%B1-%ED%98%84%EB%8C%80%ED%99%94-app-modernization-%EC%A0%84%EB%9E%B5%EA%B3%BC-%EC%A0%84%EC%88%A0-8ed200b0281e?source=rss----4433725e2350---4</link>
            <guid isPermaLink="false">https://medium.com/p/8ed200b0281e</guid>
            <category><![CDATA[바비톡]]></category>
            <category><![CDATA[microservices]]></category>
            <category><![CDATA[software-architecture]]></category>
            <category><![CDATA[babitalk]]></category>
            <category><![CDATA[app-modernization]]></category>
            <dc:creator><![CDATA[Kychoi]]></dc:creator>
            <pubDate>Tue, 02 Dec 2025 09:28:32 GMT</pubDate>
            <atom:updated>2026-02-27T05:34:59.519Z</atom:updated>
            <content:encoded><![CDATA[<p>AWS Cloud Native로의 3년, 바비톡의 앱 현대화(App Modernization) 전략과 전술 경험을 소개합니다.</p><p>지난 3년, 우리 조직은 폭발적으로 성장했습니다. 하지만 비즈니스의 속도가 빨라질수록, 초기에 구축했던 시스템은 거대한 ‘기술 부채’가 되어 우리의 발목을 잡기 시작했습니다.</p><p>이 글은 거대하고 불투명했던 Monolithic 시스템을 <strong>AWS Cloud Native</strong> 환경으로 전환하며 겪었던 치열한 기술적 의사결정과 구체적인 실행 전술, 그리고 그 결과에 대한 기록입니다.</p><h3>1. 배경: “누가 배포했는지 아무도 모른다”</h3><p>3년 전, 우리의 백엔드 시스템은 거대한 <strong>Monolithic</strong> 구조였습니다. 하지만 진짜 문제는 구조 자체가 아니라 ‘확장 불가능한 운영 방식’에 있었습니다.</p><ul><li><strong>Shadow IT &amp; Manual Deployment:</strong> 개발자가 개인 PC에서 Elastic Beanstalk CLI를 통해 EC2에 직접 배포했습니다. 배포 이력은 남지 않았고, 누가 언제 무엇을 수정했는지 추적할 수 없는 &#39;블랙박스&#39; 상태였습니다.</li><li><strong>인프라의 한계:</strong> 개발/QA/Live 환경의 경계가 모호했고, 소수의 인원이 ‘감’으로 유지하는 구조였습니다. 트래픽이 급증하거나 동시 개발 인원이 늘어날 때마다 인프라 리소스 부족과 충돌 문제에 시달렸습니다.</li></ul><p>우리는 결단이 필요했습니다. 단순히 서버의 구성을 변경하거나 옮기는 것이 아니라, <strong>앱 현대화(App Modernization)</strong>를 통해 지속 가능한 성장 기반을 만들어야 했습니다. 비즈니스 성과를 만들내기 위해 성공적인 앱 현대화는 다음 항목들에 대한 총체적인 접근과 지원이 필요합니다.</p><ul><li><strong>비즈니스 민첩성:</strong> 비즈니스 요구사항을 실제 기능으로 전환하는 속도로써 개발 조직이 비즈니스 요청에 얼마나 빠르게 대응하는지, 기능을 프로덕션에 배포하는 과정을 비즈니스가 얼마나 통제할 수 있는지를 의미합니다.</li><li><strong>조직 민첩성:</strong> 애자일 방법론과 DevOps 문화를 포함하는 전달 프로세스 성숙도 정도로써 조직 전반에 걸친 명확한 역할 분담과 원활한 협업 및 커뮤니케이션을 지원합니다.</li><li><strong>엔지니어링 효율성: </strong>얼마나 빠르고 안전하고 효율적으로 개발할 수 있는지에 대한 척도로써 품질 보증, 테스트, CI/CD, 구성 관리, 애플리케이션 설계, 소스 코드 관리의 개선을 포함합니다.</li></ul><h3>2. 전략: 4가지 현대화 전술의 조화 (The 4 R’s)</h3><p>우리는 무작정 MSA로 직행하는 위험한 도박 대신, AWS의 앱현대화 전략을 우리 상황에 맞춰 4가지로 분류하고 적재적소에 배치했습니다.</p><ol><li><strong>Lift &amp; Shift:</strong> 데이터센터 자원을 클라우드로 옮기는 1차적 이동 → 이미 AWS Cloud에서 운영 중이라서 미대상</li><li><strong>Re-Platform (VMs → Containers):</strong> 비즈니스 로직 수정 없이 컨테이너화를 통해 운영 효율성을 높이는 단계 → 복잡하고 무거운 monolithic 서버들이 대상</li><li><strong>Refactor (Monolithic → Microservices):</strong> 핵심 도메인을 분리하여 결합도를 낮추는 단계 → 중요하고 여러 곳에서 반복 사용되는 관련도 높은 도메인들이 대상</li><li><strong>Re-Invent (Host fleets → Serverless):</strong> 클라우드 네이티브의 장점을 극대화하는 단계 → 운영 부담을 줄이면서 가볍고 독립적인 신규 서비스들 대상</li></ol><p>이 4가지 전략을 바탕으로 ‘어떤 서비스’를 ‘어떤 방식’으로 전환할지 결정하는 구체적인 전술을 수립했습니다.</p><h3>3. 전술: 악마는 디테일에 있다 (Specific Tactics)</h3><p>전략이 방향성이라면, 전술은 생존 방식입니다. 우리는 다음과 같은 <strong>5가지 핵심 전술</strong>을 통해 마이그레이션을 완수했습니다.</p><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*GgUD3eC2RnzFtWuiVspaIw.png" /></figure><h4>① Infra as Code: CDK를 통한 플랫폼 엔지니어링</h4><p>우리는 모든 인프라를 단순한 설정 파일(YAML)이 아닌, AWS CDK(Cloud Development Kit)를 사용하여 <strong>OOP(객체 지향 프로그래밍)</strong> 방식으로 구현했습니다.</p><ul><li><strong>추상화와 모듈화:</strong> 자주 사용하는 인프라 구성을 패턴으로 추상화시켜 OOP 기반으로 모듈화하고, 스택 기반으로 템플릿화하여 재사용성을 극대화했습니다. 따라서 바비톡의 DevOps엔지니어는 기본적으로 OOP 경험을 필수입니다.</li><li><strong>CDD (Configuration Driven Development):</strong> 서버마다 다른 리소스 스펙이나 환경 변수는 Configuration으로 주입받게 설계했습니다. 이를 통해 ‘One Source Multiple Deploy’를 실현, 하나의 코드로 다양한 환경을 손쉽게 재배포할 수 있는 플랫폼 엔지니어링 기반을 마련했습니다. 예를 들어 ECS 기반 컨테이너 서빙과 Lambda 기반 서버리스 서빙하는 모든 영역을 하나의 CDK Stack으로 서로 다른 프로젝트/서비스/팀에 배포 가능합니다.</li></ul><h4>② “Dual Track” 아키텍처: Container vs Serverless</h4><p>우리는 서비스의 성격에 따라 컴퓨팅 환경을 이원화했습니다.</p><ul><li><a href="https://www.notion.so/MSA-2b905786b64b8037b48afbf1a67afcb6?pvs=21"><strong>Track A. 안정성 중심 (ECS/Fargate)</strong></a>: 트래픽이 예측 가능하고 무거운 비즈니스 로직을 수행하는 <a href="https://medium.com/babitalk-blog/%EB%B0%94%EB%B9%84%ED%86%A1-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EA%B8%B0%EB%B0%98-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-msa-%EC%A0%84%EB%9E%B5%EA%B3%BC-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%97%AC%EC%A0%95-9c7873901227">핵심 서비스는 <strong>Container</strong> 기반으로 전환</a>했습니다.</li><li><a href="https://www.notion.so/MSA-2ba05786b64b80758b77c7b81639602f?pvs=21"><strong>Track B. 민첩성/효율성 중심 (Serverless)</strong></a>: 트래픽이 들쑥날쑥하거나 독립적인 보조 서비스(액세서리 기능)는 <strong>AWS Lambda + API Gateway/Event Bridge </strong>조합의 서버리스를 채택했습니다.</li></ul><h4>③ 스트랭글러(Strangler) 패턴의 정교한 적용</h4><p>시스템을 한 번에 끄고 새로 켜는 빅뱅 방식 대신, 기존 시스템 주위에 새로운 MSA를 덩굴처럼 키워나가는 방식을 택했습니다.</p><p>예를 들어 <strong>money Box(충전금 도메인)</strong>의 경우, 가장 민감한 돈 관련 로직은 여러 모놀리식 서버들에서 하드코딩되어 중복 코드들이 서로 미묘하게 다르게 구현되어 있었습니다. 이에 대한 마이그레이션 단계는 아래와 같이 2단계로 나눠서 진행되었습니다.</p><ol><li><strong>Logging Phase:</strong> 초기 릴리즈에서는 로직 실행 없이, 각 서버의 충전금 증감 내역을 무결하게 기록(Logging)하는 기능만 수행하며 데이터 누락과 동시성 검증을 거쳤습니다.</li><li><strong>Migration Phase:</strong> 검증 완료 후 가상충전 → CPA/CPV → 부가상품 구매 순으로 Usecase를 하나씩 이관했습니다.</li></ol><p>또한 <a href="https://medium.com/babitalk-blog/search-box-msa-%EC%97%AC%EC%A0%95-affc76dea87b"><strong>search Box (검색 도메인)</strong></a>의 경우, CRUD 중 데이터 변경 위험이 없는 <strong>Read(조회)</strong> 쿼리부터 분리하고, 이후 CUD(인덱싱/업데이트) 이벤트를 최종적으로 마이그레이션했습니다.</p><h4>④ 데이터 스토리지 최적화 (Polyglot Persistence)</h4><p>RDB 만능주의에서 벗어나 데이터의 목적에 맞는 저장소를 적재적소에 배치했습니다. 예를 들어 실시간 로깅을 위해서는 AWS Kinesis를, 대용량 데이터 수집을 위해서는 DynamoDB를, 데이터 캐싱은 ElasticCache와 memoryDB를 선택적으로 도입하였습니다.</p><h4>⑤ 거버넌스와 DevOps 역할의 재정의</h4><p>AWS 계정을 목적(운영/데이터/분석/관리)에 따라 물리적으로 분리하고, 개발자(CI 집중)와 DevOps(CD 전담)의 역할을 명확히 나누어 배포의 투명성과 추적성을 보장했습니다.</p><h3>4. 성과 및 임팩트</h3><p>3년의 치열한 여정 끝에 우리는 명확한 기술적 성취를 달성했습니다. 단순한 인프라 변경이 아닌, 비즈니스 속도를 가속화하는 엔진을 교체한 결과입니다.</p><h4>🛠️ 마이그레이션 현황 (Implementation Status)</h4><p>우리는 각 서비스의 특성에 맞춰 3가지 현대화 전략을 차별적으로 적용했습니다.</p><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*2pZPoh8A56Sn420kEDBUKQ.png" /></figure><p><strong>1. Re-Platform (VMs → Containers)</strong></p><ul><li><strong>대상:</strong> 주요 API 서버, Utility 서버, Batch 서버</li><li><strong>성과:</strong> 기존 Elastic Beanstalk(EC2)에서 운영되던 서비스들을 독커 컨테이너로 패키징한 후 AWS ECS(EC2/Fargate)로 이관을 완료했습니다. 가장 큰 수확은 ‘안전장치 확보’였습니다. 컨테이너 환경의 안정성 덕분에, 이후 내부 구조를 Layered Architecture로 리팩토링하고 재배포하는 과정을 두려움 없이 반복할 수 있었습니다.</li></ul><p><strong>2. Refactor (Monolithic → Microservices)</strong></p><p><strong>2.1 레거시 분리를 분리한 케이스들</strong></p><ul><li><strong>대상:</strong> 무겁고 중복이 심했던 핵심 도메인을 <strong>Clean Architecture</strong> 기반의 MSA로 분리했습니다.</li><li><strong>성과</strong>: money_box (충전금 전담), search_box (검색 전담)</li></ul><p><strong>2.2 신규 서비스로 런칭한 케이스들</strong></p><ul><li><strong>대상:</strong> 새로운 기획된 서비스들은 <strong>Microservice First</strong> 전략으로 설계하여 빠르게 런칭했습니다.</li><li><strong>성과</strong>: aiml_box (AI Agent), auth_box (인증), chat_box (채팅)</li></ul><p><strong>3. Re-Invent (Host fleets → Serverless)</strong></p><ul><li><strong>대상:</strong> 트래픽 예측이 불가능하고 메인 서비스와 의존성이 낮은 보조(Accessory) 서비스</li><li><strong>성과:</strong> message_box (알림톡/이메일), crm_box (마케팅 자동화), data_box (통계/분석)</li></ul><h4>📈 정량적 성과 (Quantitative Impact)</h4><ul><li><strong>배포 파이프라인:</strong> 0개(수동) → <strong>300개 이상의 자동화된 CI/CD 파이프라인</strong> 구축</li><li><strong>배포 속도:</strong> 1시간 이상 소요 → <strong>수 분 단위</strong>로 단축</li><li><strong>확장성:</strong> 3개의 Monolithic 서버 → <strong>5개의 API 서버 + 20여 개의 MSA 서버</strong>로 분화</li><li><strong>Observability:</strong> 인프라/운영 알람 수백 개 신설하여 장애 가시성 확보</li><li><strong>Time-to-Market:</strong> 신규 MSA 서비스 런칭 준비 기간 <strong>1주일 → 1일</strong>로 단축</li></ul><h4>🚀 정성적 성과 (Organizational Impact)</h4><p>가장 드라마틱한 변화는 조직의 ‘일하는 방식’에서 일어났습니다.</p><ul><li><strong>병렬 개발(Parallelism) 실현:</strong> 과거에는 하나의 코드를 수정하기 위해 모두가 대기해야 했지만, 이제는 앞단의 <strong>API 서버 팀</strong>과 뒷단의 <strong>공통 MSA 서버 팀</strong>이 병렬적으로 개발합니다. 전문성은 높아지고, 의존성은 낮아졌으며 생산성은 높아졌습니다.</li><li><strong>조직의 확장:</strong> 초기 2개의 사일로(Silo)였던 개발 조직이 9개 사일로(Silo)까지 늘어났음에도, 운영상의 복잡도 증가나 병목현상 없이 기민하게 움직이고 있습니다.</li></ul><h3>5. 회고, Lessons Learned</h3><p>물론 기술적 전환보다 더 어려웠던 것은 ‘문화’와 ‘관성’을 바꾸는 일이었습니다.</p><p><strong>1. 속도와 표준화의 균형</strong> MSA 도입 초기, 제로 베이스에서의 시작은 오히려 속도를 늦췄습니다. 이를 해결하기 위해 재사용 가능한 코드와 모듈을 템플릿화하여 레포지토리로 제공하는 <strong>‘내부 표준화’</strong> 작업에 집중했습니다. 파편화된 개발 경험을 하나로 묶는 것이 효율성의 핵심이었습니다.</p><p><strong>2. Conway의 법칙 (조직 구조의 재편)</strong> “시스템 구조는 조직 구조를 닮는다”는 말처럼, 아키텍처 변화에 맞춰 조직도 개편했습니다. 비즈니스 로직을 전담하는 ’<strong>서비스 개발팀</strong>’과 공통 모듈/인프라를 지원하는 ’<strong>MSA 플랫폼팀</strong>’으로 나누어 전문성을 강화하고 역할(R&amp;R)을 명확히 했습니다.</p><p><strong>3. 리더가 먼저 깨트리는 관성</strong> 익숙한 레거시 방식이 편한 것은 당연합니다. 이를 타파하기 위해 저는 <strong>Self-Reference(자기 참조)</strong> 전략을 택했습니다. CTO인 제가 먼저 새로운 기술을 직접 시도하고, 멤버들에게 전파하는 순환고리를 만들었습니다. 리더가 먼저 움직일 때, 팀은 변화를 ‘숙제’가 아닌 ‘즐거운 도전’으로 받아들였습니다.</p><h3>마치며</h3><p>이론적으로 완벽한 아키텍처는 많습니다. 하지만 “우리 비즈니스 상황과 우리 팀의 역량에 맞는가?”라는 질문에 답을 내리고, 결단력 있게 실행에 옮기는 것이 CTO의 역할이라 생각합니다. 우리의 현대화 여정은 이제 막 첫 단추를 끼웠을 뿐이며, 앞으로도 우리는 계속해서 진화할 것입니다.</p><p>본 글이 이미 스타트업으로써 서비스를 시작하였으나 정체 기간을 거쳐 서비스와 조직이 한 스텝 점프하길 원하시는 조직들에게 좋은 경험담이 되었으면 합니다. 또한 AWS 기반으로 Cloud Native(클라우드 친화적인) 전개를 고민하시는 조직들에게도 유익한 글이 되었으면 합니다.</p><p>후속 블로그들(<a href="https://medium.com/babitalk-blog/%EB%B0%94%EB%B9%84%ED%86%A1-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EA%B8%B0%EB%B0%98-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-msa-%EC%A0%84%EB%9E%B5%EA%B3%BC-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%97%AC%EC%A0%95-9c7873901227">컨테이너 기반 마이크로서비스</a>, <a href="https://medium.com/babitalk-blog/%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4-%EA%B8%B0%EB%B0%98-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-msa-%EC%A0%84%EB%9E%B5%EA%B3%BC-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%97%AC%EC%A0%95-1ab12eec26c4">서버리스 기반 마이크로서비스</a>)에서 관련된 상세 경험들이 시리즈 형태로 제공될 예정입니다.</p><h3>6. 레퍼런스</h3><ul><li>AWS 앱 현대화 권장 가이드: <a href="https://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/strategy-modernizing-applications/targeted-business-outcomes.html">https://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/strategy-modernizing-applications/targeted-business-outcomes.html</a></li><li>바비톡의 클라우드 친화적인 Data 기반 AI와 앱현대화 전략 : <a href="https://www.youtube.com/watch?v=FFYiSysy-o0">https://www.youtube.com/watch?v=FFYiSysy-o0</a></li><li>현대적 애플리케이션 길잡이, AWS Cloud Development Kit: <a href="https://www.youtube.com/watch?v=6i7DgQzYYAo">https://www.youtube.com/watch?v=6i7DgQzYYAo</a></li><li>AWS CDK Project Template for DevOps: <a href="https://github.com/aws-samples/aws-cdk-project-template-for-devops">https://github.com/aws-samples/aws-cdk-project-template-for-devops</a></li><li>Amazon Cognito and API Gateway based machine to machine authorization using AWS CDK: <a href="https://github.com/aws-samples/amazon-cognito-and-api-gateway-based-machine-to-machine-authorization-using-aws-cdk">https://github.com/aws-samples/amazon-cognito-and-api-gateway-based-machine-to-machine-authorization-using-aws-cdk</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8ed200b0281e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/babitalk-blog/%EB%B0%94%EB%B9%84%ED%86%A1-%EC%95%B1-%ED%98%84%EB%8C%80%ED%99%94-app-modernization-%EC%A0%84%EB%9E%B5%EA%B3%BC-%EC%A0%84%EC%88%A0-8ed200b0281e">앱 현대화(App Modernization) 전략과 아키텍처 개선 여정</a> was originally published in <a href="https://medium.com/babitalk-blog">Babitalk</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[바비톡 QA 팀 테스트 자동화 구축기]]></title>
            <link>https://medium.com/babitalk-blog/%EB%B0%94%EB%B9%84%ED%86%A1-qa-%ED%8C%80-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%90%EB%8F%99%ED%99%94-%EA%B5%AC%EC%B6%95%EA%B8%B0-d51ec930432b?source=rss----4433725e2350---4</link>
            <guid isPermaLink="false">https://medium.com/p/d51ec930432b</guid>
            <category><![CDATA[test-automation]]></category>
            <category><![CDATA[tech]]></category>
            <category><![CDATA[quality-assurance]]></category>
            <category><![CDATA[software-testing]]></category>
            <dc:creator><![CDATA[Babitalk]]></dc:creator>
            <pubDate>Thu, 27 Nov 2025 09:15:21 GMT</pubDate>
            <atom:updated>2026-02-27T05:34:29.536Z</atom:updated>
            <content:encoded><![CDATA[<h3>QA 테스트 자동화: Appium과 Selenium을 활용한 통합 테스트 구축기</h3><h3>테스트 자동화 도입 배경</h3><p>안녕하세요! 바비톡 QA팀의 곽선명 입니다.</p><p>바비톡은 2주 단위로 업데이트를 하며, 크고 작은 변화들이 생깁니다. <br>QA 팀은 업데이트된 앱에서 사용자가 문제를 겪지 않도록 배포 주기에 맞추어 stage 환경에서 통합 테스트를 수행하는데, 앱의 주요 기능들을 꼼꼼히 확인하다 보니 아래의 같은 문제들이 발생하였습니다.</p><ul><li>매뉴얼 테스트 수행 시간이 지나치게 길었습니다. 현재 바비톡에서 베타 배포 전 확인 하는 체크리스트 370여 개를 iOS/AOS 각각의 플랫폼에서 수행해야 하고, 다른 업무도 병행해야 하니 병목 현상을 일으켰습니다.</li><li>테스트 결과의 일관성이 저하될 리스크가 있었습니다. 테스터 마다 수행 방식과 결과 해석이 다를 수 있는 여지가 있었습니다.</li><li>휴먼 에러가 발생할 리스크가 있었습니다. 다수의 케이스를 매뉴얼로 테스트 하게되면 자연스럽게 집중력이 저하되어 버그를 놓칠 가능성이 없다고 할 수 없습니다.</li></ul><p>이와 같은 문제들은 테스트 자동화 도입을 고려하고 있거나, 생각해 본 적이 있다면 공감하실 부분이라고 생각합니다. 바비톡 QA 팀 역시 위와 같은 문제에 대해 인지하고 있었기 때문에 테스트 자동화를 도입하기로 결정 하였습니다.</p><p>이 글은 자동화 테스트 도입에 대해, 또는 도입을 결정하였으나 구축 방법에 대해 고민하는 분들을 위해 작성되었습니다.</p><h3>자동화 케이스 선정</h3><p>QA 팀은 바비톡 배포가 있을 때마다 stage 환경에서 서비스 핵심 기능 기준으로 작성된 통합 테스트 케이스를 수행합니다. 문제가 발생할 경우 사용자와 비즈니스적 측면에서 높은 영향도를 주는 케이스들로 선정되어 있고, 다른 업무와 병행하며 매뉴얼 테스트로 수행하는 경우 약 8시간 정도가 소요되는 것으로 측정되어 하루 업무 시간을 모두 소진해야 했습니다.</p><p>바비톡의 주요 기능들에 대한 케이스이면서 반복적으로 수행하는 케이스이기에, 자동화 케이스로 적절하다고 판단하였습니다.</p><h3>구현 방법</h3><p>바비톡 QA 팀에서 사용하는 툴 및 프레임워크에 대한 간단한 설명과 구현 방법에 대해 설명하겠습니다.</p><p>각 플랫폼별로 사용 하는 툴과 담당자가 다릅니다.</p><ul><li>AOS : 파이썬 기반의 appium + robotframework</li><li>iOS : 파이썬 기반의 appium + gague</li><li>Web : 파이썬 기반의 selenium</li></ul><p>환경 별 툴 및 프레임워크를 다르게 사용한 이유는 아래와 같습니다.</p><ul><li>플랫폼 별 담당자가 분리되어 있어 각자 선호도와 숙련도에 따른 툴 선택</li><li>플랫폼 별 독립적인 테스트 운영으로 유연성 확보</li><li>플랫폼 별 독립적인 유지보수로 코드 관리가 더 명확</li><li>각 플랫폼의 버전 업데이트나 변경사항에 더 유연하게 대응</li><li>플랫폼 별 테스트 코드의 책임 소재가 명확</li></ul><p>툴 및 프레임워크의 설정과 구현 방법은 macOS와 <a href="https://brew.sh/ko/">brew</a> 가 설치된 상태로 가정하였으며 실제 기기에서 동작하도록 작성되었습니다.</p><h3>iOS</h3><ul><li>Appium 설치</li></ul><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*LiwzEKAxeiLn29fJ8TXDUg.png" /><figcaption>Appium은 iOS, Android, Windows 애플리케이션의 자동화 테스트를 위한 오픈소스 도구입니다. 다양한 프로그래밍 언어를 지원하고 실제 디바이스와 시뮬레이터/에뮬레이터 모두에서 테스트가 가능하여 모바일 앱 테스트 자동화의 표준으로 자리잡았습니다.</figcaption></figure><pre># Node.js &amp; Appium 설치<br>brew install node<br>npm install -g appium<br># appium 상태 확인<br>npm install -g appium-doctor<br><br># Appium Inspector 설치<br>brew install --cask appium-inspector 또는<br>&lt;https://github.com/appium/appium-inspector&gt; 에서 설치</pre><ul><li>Xcode 에서 WebDriverAgent 설정</li></ul><pre># 1. Xcode 및 Command Line Tools 설치 확인<br>xcode-select --install<br><br># 2. WebDriverAgent 설정<br>cd /usr/local/lib/node_modules/appium/node_modules/appium-webdriveragent<br>open WebDriverAgent.xcodeproj<br><br># 3. Xcode에서 설정<br>- Signing &amp; Capabilities에서 Team 선택<br>- Bundle Identifier 설정<br>- 타겟 디바이스 선택<br><br># 4. 실제 디바이스에서 WDA 빌드 테스트<br>xcodebuild -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination &#39;id=&lt;device_udid&gt;&#39; test</pre><p>WebDriverAgent(WDA)는 iOS 앱 자동화 테스트를 위한 WebDriver 서버입니다. Facebook이 개발했으며, Appium이 iOS 디바이스와 통신하기 위해 사용하는 프록시 역할을 합니다. XCTest framework를 기반으로 하여 실제 디바이스나 시뮬레이터의 UI 요소를 찾고 조작할 수 있습니다.</p><ul><li>Gauge Framework 설정</li></ul><pre># Gauge CLI 설치<br>brew install gauge<br><br># 언어 러너 설치 (python 사용 시)<br>gauge install python<br><br># IDE 플러그인 설치 (VSCode 사용 시)<br>gauge install vscode</pre><p>Gauge Framework는 테스트 시나리오를 누구나 읽기 쉬운 문서 작성 방식으로 작성할 수 있게 해주는 테스트 자동화 도구입니다. 테스트 결과를 보기 좋은 HTML 리포트로 자동 생성하고 실패한 테스트에 대한 스크린샷과 로그를 자동으로 수집하여 문제 파악이 쉽다는 장점이 있습니다.</p><ul><li>Capabilities 예시</li></ul><pre>{<br>    &#39;platformName&#39;: &#39;iOS&#39;,<br>    &#39;deviceName&#39;: &#39;실제 디바이스명&#39;,<br>    &#39;udid&#39;: &#39;디바이스 UDID&#39;,<br>    &#39;automationName&#39;: &#39;XCUITest&#39;,<br>    &#39;app&#39;: &#39;앱 경로.app&#39;,<br>    &#39;bundleId&#39;: &#39;com.example.app&#39;,<br>    &#39;xcodeOrgId&#39;: &#39;개발자 계정 Team ID&#39;,<br>    &#39;xcodeSigningId&#39;: &#39;iPhone Developer&#39;<br>}</pre><p>Capability는 테스트 자동화 세션의 설정을 정의하는 키-값 쌍의 JSON 객체입니다. 테스트할 플랫폼, 디바이스, 앱 정보 등 테스트 환경과 동작 방식을 Appium 서버에 전달하는 역할을 합니다. 실제 Capability는 테스트 환경에 따라 달라질 수 있습니다.</p><h4><strong>AOS</strong></h4><ul><li>기본 도구 및 Appium 설치</li></ul><pre># 1. 기본 도구 설치<br>brew install node python openjdk android-sdk android-platform-tools ruby<br>pip install --upgrade pip<br><br># 2. Appium 관련 설치<br>npm install -g appium appium-doctor<br>npm install appium-uiautomator2-driver<br>brew install --cask appium-inspector</pre><ul><li>Robotframwork 설정 및 실행 방법</li></ul><pre># 1. Robot Framework 설치<br>pip install robotframework robotframework-appiumlibrary pytest robotframework-seleniumlibrary robotframework-debuglibrary<br><br># 2. 환경변수 설정 (~/.zshrc)<br>export ANDROID_HOME=$HOME/Library/Android/sdk<br>export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools<br>export JAVA_HOME=$(/usr/libexec/java_home)<br><br># 3. 디바이스 설정 및 테스트<br>adb devices  # 디바이스 확인<br>appium --base-path /wd/hub  # Appium 서버 실행<br>robot test.robot  # 테스트 실행</pre><ul><li>Capabilities 예시</li></ul><pre>{<br>    &#39;platformName&#39;: &#39;Android&#39;,<br>    &#39;deviceName&#39;: &#39;디바이스명&#39;,<br>    &#39;udid&#39;: &#39;디바이스 ID&#39;,<br>    &#39;automationName&#39;: &#39;UiAutomator2&#39;,<br>    &#39;app&#39;: &#39;앱 경로.apk&#39;,<br>    &#39;appPackage&#39;: &#39;com.example.app&#39;,<br>    &#39;appActivity&#39;: &#39;com.example.app.MainActivity&#39;,<br>    &#39;noReset&#39;: True,<br>    &#39;autoGrantPermissions&#39;: True<br>}</pre><p>Capability는 테스트 자동화 세션의 설정을 정의하는 키-값 쌍의 JSON 객체입니다. 테스트할 플랫폼, 디바이스, 앱 정보 등 테스트 환경과 동작 방식을 Appium 서버에 전달하는 역할을 합니다. 실제 Capability는 테스트 환경에 따라 달라질 수 있습니다.</p><h3>Selenium</h3><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*LwBAIt3L5Et6PoniXn7iSw.png" /><figcaption>selenium은 웹 브라우저 자동화를 위한 가장 널리 사용되는 오픈소스 테스트 도구로, 다양한 브라우저(Chrome, Firefox, Safari 등)와 프로그래밍 언어를 지원합니다. 웹 요소를 쉽게 찾고 조작할 수 있는 강력한 기능을 제공하며, 여러 브라우저에서 동일한 테스트를 실행할 수 있어 크로스 브라우저 테스트 자동화에 특히 유용합니다.</figcaption></figure><ul><li>기본 도구 및 라이브러리 설치</li></ul><pre># Python &amp; pip 설치<br>brew install python<br><br># Selenium 설치<br>pip install selenium<br><br># WebDriver 설치<br>brew install --cask chromedriver  # Chrome용<br>brew install --cask geckodriver   # Firefox용<br><br># 필요한 추가 라이브러리<br>pip install pytest  # 테스트 프레임워크<br>pip install pytest-html  # HTML 리포트<br>pip install selenium-page-factory  # Page Object 패턴 지원</pre><h3>구현</h3><p>Stage 통합 테스트 케이스 중 일부인 바비톡에서 이메일로 로그인 하는 flow를 자동화해 보겠습니다. 기본적으로 자동화 구현은 Appium Inspector 또는 개발자 도구에서 정의된 elements를 찾고 해당 elements를 원하는 flow로 구현하는 방식입니다.</p><ul><li>암호 등의 일부 민감 정보는 제외되어 있습니다.</li></ul><blockquote><strong>iOS</strong></blockquote><pre>import logging<br>import time<br><br>from appium import webdriver<br>from appium.webdriver.common.appiumby import AppiumBy<br>from appium.options.ios import XCUITestOptions<br>from getgauge.python import step<br>from selenium.webdriver.support.ui import WebDriverWait<br>from selenium.webdriver.support import expected_conditions as EC<br>from selenium.common.exceptions import TimeoutException, NoSuchElementException, StaleElementReferenceException<br><br># 로깅 설정<br>logging.basicConfig(level=logging.INFO, format=&#39;%(asctime)s - %(levelname)s - %(message)s&#39;)<br>logger = logging.getLogger(__name__)<br><br>class WebDriverHelper:<br>   def __init__(self):<br>       self.driver = None<br>       self.wait = None<br>       self.DEFAULT_WAIT_TIME = 6<br>       self.SHORT_WAIT_TIME = 3<br><br>   def initialize_driver(self):<br>       &quot;&quot;&quot;WebDriver 초기화 및 최적화된 설정&quot;&quot;&quot;<br>       options = XCUITestOptions()<br>       # 기본 capabilities<br>       base_caps = {<br>           &#39;platformName&#39;: &#39;iOS&#39;,<br>           &#39;platformVersion&#39;: &#39;18.1&#39;,<br>           &#39;automationName&#39;: &#39;XCUITest&#39;,<br>           &#39;deviceName&#39;: &#39;smgwakiphone&#39;,<br>           &#39;udid&#39;: &#39;-&#39;,<br>           &#39;xcodeSigningId&#39;: &#39;iOS Developer&#39;,<br>           &#39;xcodeOrgId&#39;: &#39;776KKAJ27S&#39;,<br>           &#39;bundleId&#39;: &#39;-&#39;,<br>           # 세션 안정성을 위한 추가 설정<br>           &#39;newCommandTimeout&#39;: 3600,<br>           &#39;wdaStartupRetries&#39;: 4,<br>           &#39;wdaStartupRetryInterval&#39;: 20000,<br>           &#39;shouldTerminateApp&#39;: False,<br>           &#39;webviewConnectTimeout&#39;: 90000,<br>           &#39;clearSystemFiles&#39;: True,<br>           &#39;“connectHardwareKeyboard&#39;:True<br><br>       }<br>       <br>       # 성능 최적화 capabilities<br>       performance_caps = {<br>           &#39;fullContext&#39;: False,<br>           &#39;autoAcceptAlerts&#39;: True,<br>           &#39;reduceMotion&#39;: True,<br>           &#39;useNewWDA&#39;: False,<br>           &#39;waitForQuiescence&#39;: False,<br>           &#39;useJSONSource&#39;: True,<br>           &#39;elementResponseAttributes&#39;: &#39;type,label&#39;,<br>           &#39;shouldUseSingletonTestManager&#39;: False,<br>           &#39;maxTypingFrequency&#39;: 10,<br>           &#39;includeNonModalElements&#39;: False<br>       }<br>       <br>       # Capabilities 설정<br>       for k, v in {**base_caps, **performance_caps}.items():<br>           options.set_capability(k, v)<br><br>       appium_server_url = &#39;&lt;http://localhost:4723&gt;&#39;<br>       self.driver = webdriver.Remote(appium_server_url, options=options)<br>       self.wait = WebDriverWait(self.driver, self.DEFAULT_WAIT_TIME)<br>       return self.driver<br><br>   def wait_and_find_element(self, locator, timeout=None):<br>       &quot;&quot;&quot;요소를 찾을 때까지 대기&quot;&quot;&quot;<br>       wait_time = timeout if timeout else self.DEFAULT_WAIT_TIME<br>       try:<br>           element = WebDriverWait(self.driver, wait_time).until(<br>               EC.presence_of_element_located(locator)<br>           )<br>           return element<br>       except Exception as e:<br>           logger.error(f&quot;요소가 노출되지 않음: {locator}&quot;)<br>           raise e<br><br>   def wait_and_click(self, locator, timeout=None):<br>       &quot;&quot;&quot;요소를 찾아서 클릭&quot;&quot;&quot;<br>       try:<br>           element = self.wait_and_find_element(locator, timeout)<br>           element.click()<br>       except Exception as e:<br>           logger.error(f&quot;element 선택 실패: {locator}&quot;)<br>           raise e<br><br># WebDriver 헬퍼 인스턴스 생성<br>helper = WebDriverHelper()<br>driver = helper.initialize_driver()<br><br>  @step(&quot;이메일 로그인&quot;)<br>def emaillogin():<br>   try:<br>       helper.wait_and_click((AppiumBy.IOS_CLASS_CHAIN,&#39;**/XCUIElementTypeStaticText[`name == &quot;다른 방법으로 시작하기&quot;`]&#39;))<br>       helper.wait_and_click((AppiumBy.ACCESSIBILITY_ID, &#39;icn email login&#39;))<br>       helper.wait_and_click((AppiumBy.ACCESSIBILITY_ID, &#39;로그인&#39;))<br>       <br>       # 이메일 입력<br>       email_field = helper.wait_and_find_element((AppiumBy.ACCESSIBILITY_ID, &#39;a_emaillogin_input_email&#39;))<br>       email_field.send_keys(&#39;-&#39;)<br>       <br>       # 비밀번호 입력<br>       password_field = helper.wait_and_find_element((AppiumBy.ACCESSIBILITY_ID,&#39;a_emaillogin_input_password&#39;))<br>       password_field.send_keys(- + &#39;\\n&#39;)  # \\n으로 리턴키 입력<br>       logger.info(&quot;이메일 로그인 성공&quot;)<br>   except Exception as e:<br>       logger.error(f&#39;이메일 로그인 실패: {e}&#39;)</pre><blockquote><strong>Android</strong></blockquote><pre>Click Element With Wait<br>    [Arguments]    ${locator_type}    ${locator_value}    ${TIMEOUT}=10    ${RETRY_COUNT}=3<br>    ${index}    Set Variable    0<br>    FOR    ${index}    IN RANGE    ${RETRY_COUNT}<br>        Run Keyword If    &#39;${locator_type}&#39; == &#39;id&#39;              Wait Until Element Is Visible    id=${locator_value}    ${TIMEOUT}<br>        Run Keyword If    &#39;${locator_type}&#39; == &#39;xpath&#39;           Wait Until Element Is Visible    xpath=${locator_value}    ${TIMEOUT}<br>        Run Keyword If    &#39;${locator_type}&#39; == &#39;text&#39;            Wait Until Page Contains Element    ${locator_value}    ${TIMEOUT}<br>        Run Keyword If    &#39;${locator_type}&#39; == &#39;resource-id&#39;     Wait Until Element Is Visible    id=${locator_value}    ${TIMEOUT}<br>        Run Keyword If    &#39;${locator_type}&#39; == &#39;accessibility_id&#39;     Wait Until Element Is Visible    accessibility_id=${locator_value}    ${TIMEOUT}<br><br>        ${result}    Run Keyword And Ignore Error    Click Element    ${locator_type}=${locator_value}<br>        Exit For Loop If    &#39;${result[0]}&#39; == &#39;PASS&#39;<br><br>        Log    StaleElementReferenceException occurred, retrying...    WARN<br>    END<br>    <br># 클릭 및 입력 액션<br>Click And Input Text With Wait<br>    [Arguments]    ${locator_type}    ${locator_value}    ${text_to_input}<br>    Run Keyword If    &#39;${locator_type}&#39; == &#39;id&#39;              Wait Until Element Is Visible    id=${locator_value}    ${TIMEOUT}<br>    Run Keyword If    &#39;${locator_type}&#39; == &#39;xpath&#39;           Wait Until Element Is Visible    xpath=${locator_value}    ${TIMEOUT}<br>    Run Keyword If    &#39;${locator_type}&#39; == &#39;accessibility_id&#39;           Wait Until Element Is Visible    accessibility_id=${locator_value}    ${TIMEOUT}<br>    Click Element    ${locator_type}=${locator_value}<br>    Input Text    ${locator_type}=${locator_value}    ${text_to_input}<br>    <br>    <br>로그인&gt; 이메일<br>    Click Element With Wait    id    com.sleet.beautyplastic.dev:id/email_login<br>    Click Element With Wait    xpath    //android.widget.TextView[@text=&quot;로그인&quot;]<br>    Click And Input Text With Wait  accessibility_id    a_emaillogin_input_email    ${email}<br>    Click And Input Text With Wait  accessibility_id    a_emaillogin_input_password    ${password}<br>    Hide Keyboard<br>    Click Element With Wait    id    com.sleet.beautyplastic.dev:id/login</pre><blockquote><strong>Selenium</strong></blockquote><pre>def click_login_join(wait): <br>    login_join_div = wait.until(<br>        EC.presence_of_all_elements_located((By.CSS_SELECTOR, &quot;div.flex.flex-none&quot;))<br>    )       <br>    #login_join_div = driver.find_elements(By.CSS_SELECTOR, &quot;div.flex.flex-none&quot;)<br>    # &quot;로그인 및 회원가입&quot; 버튼 클릭<br>    for login_signup_button in login_join_div:<br>        if login_signup_button.text == &quot;로그인 및 회원가입&quot;:<br>            login_signup_button.click()<br>            time.sleep(3)  # 필요한 경우 대기<br>            break<br>    print(&quot;로그인 및 회원가입으로 이동했습니다.&quot;)<br>    <br><br>def click_join_emali(wait):       <br>    join_emali_buttons = wait.until(<br>        EC.presence_of_all_elements_located((By.CSS_SELECTOR, &quot;button.flex.justify-center&quot;))<br>    )   <br>    #join_emali_buttons = driver.find_elements(By.CSS_SELECTOR, &quot;button.flex.justify-center&quot;)<br>    for join_emali_button in join_emali_buttons:<br>        if &quot;이메일로 시작하기&quot; in join_emali_button.text:<br>            join_emali_button.click()  # 부모 button 클릭            <br>            time.sleep(3)  # 필요한 경우 대기<br>            break<br><br>def input_field_text(wait, input_text):  <br>       <br>    if &#39;이메일을 입력해주세요&#39; in input_text:<br>        input_field = wait.until(<br>        EC.presence_of_all_elements_located((By.CSS_SELECTOR, f&quot;div &gt; input[placeholder=&#39;{input_text}&#39;]&quot;))<br>        )<br>        input_field[0].send_keys(-)<br>        time.sleep(2)  # 필요한 경우 대기<br>        <br>        <br>    if &#39;비밀번호를 입력해주세요&#39; in input_text:<br>        input_field = wait.until(<br>        EC.presence_of_all_elements_located((By.CSS_SELECTOR, f&quot;div &gt; input[placeholder=&#39;{input_text}&#39;]&quot;))<br>        )<br>        input_field[0].send_keys(-)<br>        time.sleep(2)  # 필요한 경우 대기<br>        input_field[0].send_keys(Keys.ENTER) <br>        time.sleep(2)</pre><h3>현재까지의 성과</h3><p>현재 자동화 커버리지는 약 <strong>16%</strong>이며, 이는 <strong>60개</strong>의 테스트 케이스(TC)에 해당합니다. 이 60개 TC의 자동화 테스트 수행 시간은 약 <strong>10초</strong> 정도로, 수동 테스트에 비해 상당한 시간 단축 효과를 가져올 것으로 예상합니다.</p><ol><li>예상 절감 리소스</li></ol><ul><li><strong>테스트 투입 공수(Man/Hour) 37.5% 절감</strong></li><li><strong>수동으로 테스트(매뉴얼) 시, 2명이 8시간씩 소요 = 총 16시간 소요</strong></li><li><strong>UI 테스트 자동화 적용 후 인당 3시간씩 절감 = 총 6시간 절감</strong></li><li>총 테스트 시간 약 37.5% 절감</li><li>결과적으로 6시간(man-hour) 절감</li></ul><p>수행 시간 단축 외에도, 자동화 도입으로 인해 <strong>휴먼 에러가 감소</strong>하고 <strong>업무 병목 현상 또한 미미하게나마 줄어들었다고 판단</strong>하고 있습니다. 자동화 커버리지가 높아질수록 이러한 긍정적인 성과들이 더욱 증폭될 것으로 기대하고 있습니다.</p><h3>어려웠던 문제</h3><p>바비톡에는 고객 편의를 위해 노출 되는 바텀시트나 팝업들이 있는데요, 이 부분들을 자동화 하기가 쉽지 않았습니다. 마찬가지로 웹뷰로 되어 있는 화면을 자동화할 때도 어려움을 겪었습니다.</p><p>처음에는 모든 바텀시트나 웹뷰를 강제로 닫아버릴까도 생각했지만, 기능들이 정상 동작하는지 확인하려는 목적을 가진 테스트에서 적합하지 않다고 판단했습니다.</p><p>따라서 현재 context 를 확인하고 바텀시트나 웹뷰의 context 로 전환하여 기능을 수행을 하는 방법을 적용하었습니다. 혹은 해당 요소의 좌표를 직접 받아와서 선택하는 방식으로 문제를 해결했습니다.</p><ol><li>요소의 위치와 크기 정보로 자동화한 케이스</li></ol><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*BJq0Wd7kSolYymi3r5UOfQ.jpeg" /></figure><ul><li>Safari 의 닫기(완료) 버튼을 위치로 특정하여 선택하도록 하였다.</li></ul><pre>def click_safari_done_button(helper): #사파리 완료 버튼 선택 #고정<br>    try:<br>        # 완료 버튼 찾기<br>        element = WebDriverWait(helper.driver, 5).until(<br>            EC.presence_of_element_located((AppiumBy.IOS_CLASS_CHAIN, &#39;**/XCUIElementTypeButton[`label == &quot;완료&quot;`]&#39;))<br>        )<br>        <br>        # 요소의 위치와 크기 정보 가져오기<br>        location = element.location<br>        size = element.size<br>        <br>        # tap 스크립트로 클릭 실행<br>        helper.driver.execute_script(&#39;mobile: tap&#39;, {<br>            &#39;x&#39;: location[&#39;x&#39;] + size[&#39;width&#39;]/2,<br>            &#39;y&#39;: location[&#39;y&#39;] + size[&#39;height&#39;]/2<br>        })<br>        logger.info(&quot;safari 완료 버튼 선택 성공&quot;)<br>        return True<br>        <br>    except Exception as e:<br>        logger.error(f&quot;safari 완료 버튼 선택 실패: {e}&quot;)<br>        return False</pre><p>elements를 찾고 기능을 수행할 때 context 도 전환해야 할 때와 그렇지 않을 때가 아직 완벽하게 구분되지는 않습니다. 하지만 이 과정에서 어느 정도 핵심을 파악했고, 앞으로도 계속해서 탐구해야 할 부분이라고 생각합니다.</p><h3>앞으로의 목표</h3><p>아직 자동화 커버리지도 낮고 효율을 높이기 위해 해야 할 일이 많지만,<br>바비톡 QA 팀은 다음과 같은 명확한 목표를 가지고 있습니다.</p><h4>1. 통합 테스트 케이스 커버리지 상향 및 유지</h4><p>현재 커버리지를 하반기 내로 30% 이상으로 높이고 기능 추가나 변경이 있을 때도 이 커버리지를 꾸준히 유지하는 것을 목표로 합니다. 이는 단순히 수치를 높이는 것을 넘어, 변화하는 서비스 환경에서도 테스트 자동화의 품질을 일관되게 관리하겠다는 의미입니다.</p><h4>2. 개발 빌드 CI/CD 연동</h4><p>개발 빌드 단계에서 수동 테스트를 시작하기 전에, 기본적인 기능에 대한 자동화 테스트를 수행하여 서비스의 <strong>안정성을 조기에 검증</strong>할 계획입니다. 이를 통해 개발 과정의 효율성을 높이고 잠재적인 문제를 미리 발견할 수 있을 것입니다.</p><h4>3. 라이브 모니터링 도입</h4><p>라이브 환경에서 바비톡 서비스의 핵심 기능들이 얼마나 안정적으로 작동하는지 지속적으로 모니터링하고, 문제가 발생할 경우 <strong>빠르게 탐지하여 선제적으로 대응</strong>하기 위해 라이브 모니터링 시스템을 도입할 예정입니다.</p><p>현재로서는 자동화된 테스트 케이스가 많지 않고, 구현 과정에서 예상치 못한 어려움에 직면할 때도 많습니다. 그럼에도 불구하고 바비톡 QA팀은 서비스 품질의 안정성을 확보하기 위해 꾸준히 노력하고 있습니다.</p><p>앞으로 이 목표들을 달성하여 기술 블로그를 통해 다시 좋은 소식을 전해드릴 수 있도록 최선을 다하겠습니다.</p><p>참고 링크</p><ul><li>Appium 공식 문서 : <a href="https://appium.io/docs/en/latest/">https://appium.io/docs/en/latest/</a></li><li>Appium Discuss Forum <a href="https://discuss.appium.io/">https://discuss.appium.io/</a></li><li>Robot Framework 가이드 문서 : <a href="https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html">https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html</a></li><li>Selenium 공식 문서 : <a href="https://www.selenium.dev/">https://www.selenium.dev/</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d51ec930432b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/babitalk-blog/%EB%B0%94%EB%B9%84%ED%86%A1-qa-%ED%8C%80-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%90%EB%8F%99%ED%99%94-%EA%B5%AC%EC%B6%95%EA%B8%B0-d51ec930432b">바비톡 QA 팀 테스트 자동화 구축기</a> was originally published in <a href="https://medium.com/babitalk-blog">Babitalk</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Search Box MSA 전환 여정: 서비스 분리와 확장 전략]]></title>
            <link>https://medium.com/babitalk-blog/search-box-msa-%EC%97%AC%EC%A0%95-affc76dea87b?source=rss----4433725e2350---4</link>
            <guid isPermaLink="false">https://medium.com/p/affc76dea87b</guid>
            <category><![CDATA[microservice-architecture]]></category>
            <category><![CDATA[amazon-opensearch]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[tech]]></category>
            <category><![CDATA[search-engines]]></category>
            <dc:creator><![CDATA[Babitalk]]></dc:creator>
            <pubDate>Thu, 27 Nov 2025 09:15:03 GMT</pubDate>
            <atom:updated>2026-02-27T05:33:48.707Z</atom:updated>
            <content:encoded><![CDATA[<h3>검색 서비스 분리 필요성 및 문제 정의</h3><p>안녕하세요! 바비톡 제품 플랫폼팀의 정원철 입니다.</p><p>바비톡은 수년간 유지되어 온 레거시 검색 시스템을 운영해오면서 여러 가지 어려움에 직면해 있었습니다.</p><p>주요 이슈는 다음과 같았습니다:</p><p><strong>검색 품질 저하</strong></p><ul><li>사용자 경험을 충분히 만족시키지 못하는 검색 결과</li><li>정확히 일치하는 단어가 포함되어야 검색됨, 2단어 이상 검색이 어색함 등</li></ul><p><strong>불안정한 인프라</strong></p><ul><li>Elasticsearch 2.3.0이라는, <strong>2016년에 릴리스된 버전</strong>을 기반으로 하고 있어 장애 발생 시 대응이 어렵고, Kibana와의 호환성에도 문제가 발생</li></ul><p><strong>보안 및 유지보수 한계</strong></p><ul><li>더 이상 공식적인 패치나 보안 업데이트를 기대하기 어려움</li><li>AWS에서도 지원을 하지 않아, 서버가 내려가면 복구 불가 확률이 높았음</li></ul><p><strong>기술 부채</strong></p><ul><li>초기 구현 이후 수년간 업데이트 없이 방치된 코드베이스, 명확한 문서화 없이 ES에 의존하고 있는 다양한 모듈</li></ul><p>또한, 기존 시스템은 단일 인스턴스 형태로 운영되고 있었으며, 이를 전담하는 사일로 팀조차 존재하지 않아, <strong>전체 서비스 구조에서 검색은 ‘블랙박스’에 가까운 존재</strong>가 되어 있었습니다. 신규 서비스 기획이나 개선을 논의할 때, 매번 해당 기능의 동작 원리를 파악하고 코드를 뒤적여야 하는 상황이 반복되었고, 이는 생산성과 품질 모두에 큰 장애 요소가 되었습니다.</p><p>이 문제를 해결하기 위해, 우리는 <strong>검색 시스템의 모듈화 및 현대화</strong>를 고려하기 시작했습니다.</p><p>초기 검토 과정에서 다음과 같은 기술과 아키텍처 방향을 논의했습니다:</p><p><strong>OpenSearch 채택</strong></p><ul><li>Amazon이 주도하는 오픈소스 기반의 검색 엔진</li><li>AWS와의 높은 친화성 및 운영 편의성</li><li>보안, 모니터링, 스케일링에 대한 기능 강화</li></ul><p><strong>MSA 구조로의 분리</strong></p><ul><li>DB와 검색 기능이 혼재되어 있는 기존 시스템에서, <strong>검색 역할만 분리해 마이크로서비스화</strong></li><li>전체 검색 흐름을 코드 단에서 역추적하며 기존 사용부를 분리 및 문서화</li></ul><p>기존 Elasticsearch 사용부는 문서화가 거의 되어 있지 않아, 실제로는 <strong>전 코드 베이스를 수작업으로 살펴보며 검색 의존 모듈을 정리</strong>해야 했습니다. 이 과정에서 <strong>‘검색 서비스’란 무엇인가</strong>에 대한 내부 정의와 기준도 정립해나갔습니다.</p><h3>최종 선택</h3><p>기술적/운영적 요소를 고려한 끝에, 우리는 다음과 같은 흐름으로 search_box라는 신규 서버를 만들기로 결정내렸습니다:</p><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*zPOFPZP3EvluGTU37FkNag.png" /></figure><p>1. Legacy ElasticSearch 모든 사용 부분 식별</p><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*u0uUQy305k02TIxxr3nFiA.png" /></figure><p>(1) Server 별로 흩어진 검색관련 api를 모두 식별하고, 분석함</p><p>(가)app_api server, client_api server, admin_api server, cron server</p><p>(2) 서버별, 인덱스별, 목적별 등으로 나눠서 총 89개의 대표적 사용부를 분석함</p><p>2. 최신 OpenSearch 기반 MSA search_box 개발</p><p>3. ElasticSearch에 저장된 data Bulk Migration + 한국어 <a href="https://aws.amazon.com/ko/blogs/tech/amazon-opensearch-service-korean-nori-plugin-for-analysis/">Nori 형태소 분석 기능</a> 적용 Nori 형태소 분석: AWS OpenSearch에서 사용되는 <strong>Nori</strong>는 <strong>한국어 텍스트를 형태소 단위로 분석</strong>하여 검색의 정확도를 높여주는 플러그인, 문장을 의미를 가진 가장 작은 단위인 단어로 쪼개어 그 본질을 파악하는 것</p><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/411/1*r_JgscqiVrcS2W5znsjr4A.png" /></figure><ul><li>양이 적은 data는 한번에 migration 진행함</li><li>수백만개 수준의 data는 병렬로 AWS step functions를 활용하여 migration 진행함</li><li>직렬로 처리했을 경우, migration 완료까지 약 70시간이 예상되어 병렬로 진행함</li><li>서버 점검없이 migration을 빠르게 적용하기 위해, 인덱싱된 id별로 잘라서 20개로 병렬 처리함: id는 일부 구간 겹치게 처리하여, 신규 인입되는 데이터들도 대응가능케 함</li></ul><p>4. Create(index), Update, Delete, Bulk app_api server</p><ul><li>ElasticSearch 기반의 기존 서비스가 동작하고 있기에, CUD 먼저 구현 및 적용 후, 병행으로 데이터를 적재하고 테스트함</li></ul><p>5. Search 기능 케이스별 적용</p><ul><li>Create, Update, Delete 기능 안정화에 들었기에 Read(검색) 부분도 전면 교체함</li></ul><p>6. MSA에서 Search 기능 고도화</p><ul><li>산발적으로 흩어진 검색 기능을 하나의 MSA 기반 서버로 옮겼기에, 수정 및 모니터링이 용이해짐</li></ul><p>이 과정은 단순한 검색 엔진 교체가 아닌, <strong>검색의 정의, 설계, 운영 방식 전체를 다시 설계하는 작업</strong>이었습니다. 이미 서비스중인 앱의 기능을 MSA화하며, 신규 엔진으로 변경하는 작업이기에, 마이그레이션, 테스트, 코드 리팩토링을 반복하며 점진적으로 안정화시켰습니다.</p><p>산발적으로 흩어졌던, 검색 관련 기능을 분석하여, search_box라는 micro service로 몰아 뒀기에, 검색 관련 기능의 as-is 파악이 다음과 같이 보다 명확해졌습니다.</p><p>1. swagger를 통해 모든 search_box api <strong>문서화</strong></p><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*B-M5HL0jZwN8IcZdpuUSYQ.png" /></figure><p>2. DataDog을 통해 검색 기능만의 <strong>모니터링</strong>이 용이해짐</p><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*admJp1XgExD4NBy3sxLusA.png" /></figure><p>3. Kibana 사용이 편리해져서, Dev Tool을 활용한, Opensearch 데이터 확인 및 <strong>테스트</strong>가 용이해짐</p><figure><img alt="바비톡 테크 블로그" src="https://cdn-images-1.medium.com/max/1024/1*ab-S8QgsbtbDY-hOFZOeoQ.png" /></figure><p>일차적으로, 서비스의 문서화, 모니터링, 테스트등의 관리가 명확해졌고, 이를 통해, 1) 검색 관련 인사이트를 보다 쉽게 얻을 수 있고, 2) 새로운 기술과 기획적 방향성을 효율적으로 제시할 수 있게 되었습니다.</p><h3>향후 진행</h3><p>독립된 서비스로 분리됐기에,</p><ol><li>검색 관련 인사이트를 보다 쉽게 얻을 수 있고,</li><li>앱, 웹, 글로벌 등 멀티플랫폼에서의 개발 일관성 유지 및 개발비용 절감 가능해졌고,</li><li>새로운 기술과 기획적 방향성을 효율적으로 제시할 수 있게되었기에,</li></ol><p>향후에는 검색 기능 1) 안정화, 2) 고도화에 보다 집중하고자 합니다.</p><p>특히, 고도화 측면에서는 Ai 기능을 활용한 검색 기능을 제공하고자 합니다.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=affc76dea87b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/babitalk-blog/search-box-msa-%EC%97%AC%EC%A0%95-affc76dea87b">Search Box MSA 전환 여정: 서비스 분리와 확장 전략</a> was originally published in <a href="https://medium.com/babitalk-blog">Babitalk</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>