<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 바람의평온</title>
    <description>The latest articles on DEV Community by 바람의평온 (@kys7442).</description>
    <link>https://dev.to/kys7442</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3926107%2F670389ee-4bfd-49d3-84bc-6c5a90bb64e6.jpg</url>
      <title>DEV Community: 바람의평온</title>
      <link>https://dev.to/kys7442</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kys7442"/>
    <language>en</language>
    <item>
      <title>LLM API 과금 폭탄 막는 법: 일일 상한과 선불 크레딧 관리 경험기</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Fri, 28 Aug 2026 07:41:40 +0000</pubDate>
      <link>https://dev.to/kys7442/llm-api-gwageum-pogtan-magneun-beob-ilil-sanghangwa-seonbul-keuredis-gwanri-gyeongheomgi-14ed</link>
      <guid>https://dev.to/kys7442/llm-api-gwageum-pogtan-magneun-beob-ilil-sanghangwa-seonbul-keuredis-gwanri-gyeongheomgi-14ed</guid>
      <description>&lt;h2&gt;
  
  
  LLM API 과금 폭탄 막는 법: 일일 상한과 선불 크레딧 관리 경험기
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fybo3jxqjacv6reta4l4e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fybo3jxqjacv6reta4l4e.png" alt="LLM API 과금 폭탄 막는 법: 일일 상한과 선불 크레딧 관리 경험기" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;안녕하세요, 코딩아빠입니다. 오늘은 제가 LLM API를 사용하면서 겪었던 일과 그 해결 과정을 경험노트 형식으로 정리해봤습니다. 처음 AI 개발에 발을 들이면서 LLM API의 강력함에 매료되었는데, 한편으로는 예상치 못한 비용이 발생할까 봐 늘 마음 한구석이 불안했더군요. 특히 선불 크레딧 방식으로 운영되는 API는 잔액이 갑자기 소진되면 서비스가 멈출 수도 있어서 더 신경이 쓰였습니다. 이런 막연한 불안감을 해소하고 안정적으로 LLM 서비스를 운영하기 위해 어떤 노력을 했는지, 제가 직접 부딪히고 해결한 이야기들을 풀어보려고 합니다. 처음엔 그저 '잘 되겠지' 하고 시작했지만, 곧 현실적인 문제에 직면했고, 이를 시스템적으로 해결해야겠다고 마음먹게 되었네요. 이 글은 한국어 원문을 정리한 것으로, 코드와 설정값은 그대로 재현 가능합니다.&lt;/p&gt;

&lt;p&gt;이런 분께 — LLM API를 사용하며 비용 관리 및 안정적인 서비스 운영에 관심 있는 개발자 · 난이도는 중급 정도&lt;/p&gt;

&lt;h3&gt;
  
  
  여기서 확인할 것
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;LLM API 사용 시 발생할 수 있는 과금 위험과 그 원인&lt;/li&gt;
&lt;li&gt;예상치 못한 비용 지출을 막기 위한 일일 비용 상한 설정 방법&lt;/li&gt;
&lt;li&gt;선불 크레딧 잔액을 주기적으로 모니터링하는 시스템 구축&lt;/li&gt;
&lt;li&gt;비용 상한 초과 또는 크레딧 부족 시 LLM API 호출을 자동 제어하는 로직&lt;/li&gt;
&lt;li&gt;안정적인 LLM 서비스 운영을 위한 통합적인 비용 관리 전략&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  LLM API, 편리함 뒤에 숨겨진 과금의 그림자
&lt;/h3&gt;

&lt;p&gt;LLM API를 처음 접했을 때의 설렘은 정말 대단했습니다. 간단한 호출만으로 복잡한 작업을 수행하는 모습은 마치 마법 같았죠. 하지만 이내 현실적인 고민에 부딪혔습니다. 바로 '과금' 문제였습니다. LLM 호출 비용은 사용되는 토큰의 양에 따라 결정되는데, 이 토큰 사용량이라는 게 개발 초기 단계에서는 예측하기가 정말 어렵더군요. 새로운 기능을 실험하다 보면 한 번의 잘못된 호출이나 무한 루프 때문에 예상보다 훨씬 많은 토큰을 사용하게 될 가능성이 항상 있었습니다. 저의 경우, 처음에는 아무런 제약 없이 API를 사용했습니다. '설마 큰돈이 나오겠어?' 하는 안일한 생각이었죠. 하지만 실제 개발 환경에서는 예상치 못한 호출 패턴이 발생하기 쉽고, 이는 곧바로 비용 증가로 이어질 수 있다는 걸 깨달았습니다. 만약 제가 계속 이런 식으로 아무런 통제 없이 서비스를 운영했다면, 어느 순간 '과금 폭탄'이라는 무시무시한 상황을 맞이했을지도 모릅니다. 특히 Gemini API처럼 선불(Prepay) 크레딧 모델을 사용하는 경우, 잔액이 소진되면 서비스가 예고 없이 중단될 수 있다는 점이 큰 불안 요소였습니다. 잔액 부족으로 인해 중요한 서비스가 멈춰버리는 일은 상상만 해도 아찔하더군요. 이런 위험을 감수하면서 서비스를 운영하는 것은 장기적으로 지속 불가능하다는 판단이 들었고, 보다 적극적인 비용 관리 시스템이 필요하다고 생각하게 되었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  예상치 못한 비용 지출을 막기 위한 첫 시도: 일일 비용 상한 두기
&lt;/h3&gt;

&lt;p&gt;막연한 불안감을 해소하기 위해 제가 처음으로 시도한 방법은 '일일 비용 상한'을 설정하는 것이었습니다. 특정 금액 이상은 하루에 사용하지 못하도록 막는 일종의 안전장치였죠. 이렇게 하면 설령 예상치 못한 문제가 발생하더라도 하루에 나갈 수 있는 최대 비용을 제한할 수 있을 거라고 생각했습니다. 처음에는 단순히 '얼마 이상 쓰면 경고를 띄우자' 정도의 아이디어였는데, 좀 더 확실한 제어가 필요하다고 느껴서 아예 호출 자체를 막아버리는 방향으로 가닥을 잡았습니다.&lt;/p&gt;

&lt;p&gt;이 로직은 LLM API 호출이 발생할 때마다 현재까지의 일일 사용량을 확인하고, 설정된 상한선을 넘어서면 더 이상 호출이 진행되지 않도록 예외를 발생시키는 방식으로 구현했습니다. 대략 이런 형태가 될 것 같습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DAILY_COST_LIMIT = 10.0 # USD
current_day_cost = get_daily_usage_cost()
if current_day_cost &amp;gt;= DAILY_COST_LIMIT:
    raise CostLimitExceededError("일일 비용 상한 초과")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이 코드는 LLM API를 호출하기 전에 현재까지의 일일 비용을 조회하고, 미리 정해둔 &lt;code&gt;DAILY_COST_LIMIT&lt;/code&gt;를 초과하면 &lt;code&gt;CostLimitExceededError&lt;/code&gt; 예외를 발생시켜 API 호출을 중단시킵니다. 물론 &lt;code&gt;get_daily_usage_cost()&lt;/code&gt; 함수는 LLM 제공사의 API를 통해 사용량을 조회하거나, 자체적으로 로그를 쌓아 계산하는 방식 등 환경에 따라 다르게 구현해야 합니다. 이 단계를 통해 최소한 하루 단위의 과금 폭탄은 막을 수 있겠다는 확신이 들었습니다. 이전에는 '얼마나 썼을까?' 하며 조마조마했지만, 이제는 정해진 예산 안에서만 움직인다는 안정감이 생겼습니다. 하지만 이것만으로는 선불 크레딧이 바닥나는 상황까지 완벽하게 막을 수는 없다는 한계가 있었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  선불 크레딧 잔액 모니터링: 서비스 중단 방지를 위한 안전장치
&lt;/h3&gt;

&lt;p&gt;일일 비용 상한을 두는 것만으로는 부족했습니다. 선불 크레딧 모델에서는 하루 지출을 통제하더라도 전체 잔액이 바닥나면 서비스가 중단될 수 있기 때문입니다. 그래서 다음 단계로 크레딧 잔액을 주기적으로 모니터링하고, 특정 임계치에 도달했을 때 자동으로 대응하는 시스템을 추가하기로 했습니다. 이는 서비스 연속성을 확보하는 데 결정적인 역할을 할 것이라고 생각했습니다.&lt;/p&gt;

&lt;p&gt;제가 구현한 방식은 크게 두 가지 임계치를 설정하는 것이었습니다. 첫 번째는 '경고 임계치(WARNING_THRESHOLD)'로, 잔액이 이 수준 이하로 떨어지면 관리자에게 알림을 보내는 역할을 합니다. 두 번째는 '중단 임계치(STOP_THRESHOLD)'로, 잔액이 이 수준 이하가 되면 아예 LLM API 호출을 비활성화하여 추가적인 비용 발생과 서비스 중단을 방지하는 역할을 합니다. 이 로직은 &lt;code&gt;cron&lt;/code&gt; 같은 스케줄러를 이용해 주기적으로 실행되도록 구성했습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def check_prepay_balance(api_key):
    balance = your_llm_api_client.get_credit_balance(api_key)
    if balance &amp;lt; WARNING_THRESHOLD:
        send_alert("LLM 크레딧 부족 임박!", balance)
    if balance &amp;lt; STOP_THRESHOLD:
        disable_llm_calls()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;your_llm_api_client.get_credit_balance()&lt;/code&gt;는 LLM API 제공사의 SDK를 통해 실제 잔액을 조회하는 부분입니다. &lt;code&gt;send_alert()&lt;/code&gt;는 Slack이나 이메일 등으로 관리자에게 알림을 보내는 함수이며, &lt;code&gt;disable_llm_calls()&lt;/code&gt;는 LLM API 호출을 막는 전역 플래그를 설정하거나 서비스를 일시 중지하는 역할을 합니다. 이렇게 하면 크레딧이 소진되기 전에 미리 인지하고 대응할 시간을 벌 수 있으며, 최악의 경우 서비스 중단을 최소화하거나 제어된 방식으로 전환할 수 있게 됩니다. 이전에는 잔액이 얼마나 남았는지 매번 확인해야 했지만, 이제는 시스템이 알아서 관리해주니 훨씬 안심이 되었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  API 호출 전 안전 로직 추가와 예외 처리
&lt;/h3&gt;

&lt;p&gt;일일 비용 상한과 선불 크레딧 모니터링 시스템을 구축한 다음에는, 이 안전장치들이 실제 LLM API 호출 로직과 어떻게 통합되어야 하는지 고민했습니다. 가장 중요한 것은 LLM API를 호출하기 전에 항상 현재 상태를 점검하고, 문제가 발생하면 적절하게 처리하는 것이었습니다. 단순히 예외를 발생시키는 것을 넘어, 사용자 경험에 미치는 영향을 최소화하고 관리자에게 정확한 정보를 전달하는 것도 중요하다고 생각했죠.&lt;/p&gt;

&lt;p&gt;그래서 LLM API를 호출하는 모든 지점에 앞서 언급한 일일 비용 상한 체크 로직을 포함시키고, 만약 상한을 초과하여 예외가 발생하면 이를 적절히 잡아서 처리하도록 &lt;code&gt;try-except&lt;/code&gt; 블록을 구성했습니다. 또한, 크레딧 부족으로 인해 &lt;code&gt;disable_llm_calls()&lt;/code&gt;가 호출된 상태라면, LLM 호출 시 해당 플래그를 확인하여 아예 호출 자체를 시도하지 않도록 했습니다. 이는 불필요한 API 요청을 줄이고, 자원 낭비를 막는 효과도 있습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;try:
    # LLM API 호출 전, 일일 비용 상한 및 크레딧 상태 확인 로직 포함
    # 예: if is_llm_calls_disabled(): raise ServiceDisabledError("LLM 서비스 중단됨")
    #     if get_daily_usage_cost() &amp;gt;= DAILY_COST_LIMIT: raise CostLimitExceededError("일일 비용 상한 초과")
    response = your_llm_api_client.generate_content(prompt)
except CostLimitExceededError as e:
    logger.error(f"LLM 호출 중단: {e}")
    # 폴백 로직 또는 사용자에게 알림 (예: "현재 AI 서비스 이용이 어렵습니다.")
except ServiceDisabledError as e:
    logger.error(f"LLM 서비스 비활성화: {e}")
    # 폴백 로직 또는 사용자에게 알림
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이처럼 예외 처리 로직을 통해 LLM 호출이 중단되었을 때, 단순히 에러를 내뿜는 것이 아니라 사용자에게는 '현재 AI 서비스 이용이 어렵습니다'와 같은 안내 메시지를 보여주고, 개발자에게는 상세한 로그를 남겨 문제 상황을 파악할 수 있도록 했습니다. 이렇게 함으로써 예상치 못한 과금 폭탄을 막는 것뿐만 아니라, 서비스의 안정성과 사용자 경험까지 고려한 시스템을 구축할 수 있었습니다. 이전에는 에러가 발생하면 서비스 전체가 멈추거나 알 수 없는 오류를 냈지만, 이제는 명확한 이유를 가지고 제어된 방식으로 대응하게 된 것이죠.&lt;/p&gt;

&lt;p&gt;.ba-pc{display:none}.ba-mo{display:block}&lt;a class="mentioned-user" href="https://dev.to/media"&gt;@media&lt;/a&gt; (min-width:768px){.ba-pc{display:block}.ba-mo{display:none}}&lt;/p&gt;

&lt;h3&gt;
  
  
  실제 환경에서의 검증과 조심스러운 테스트 과정
&lt;/h3&gt;

&lt;p&gt;이렇게 시스템을 구축하고 나니, 정말 잘 작동하는지 확인하는 과정이 필요했습니다. 특히 비용과 직결되는 부분이어서 더욱 조심스럽게 접근했네요. 저는 몇 가지 시나리오를 만들어 실제 환경과 유사한 조건에서 테스트를 진행했습니다. 물론 실제 돈이 나가지 않도록 모의(mock) API나 테스트용 계정을 활용하는 것이 중요했습니다.&lt;/p&gt;

&lt;p&gt;먼저, 일일 비용 상한이 제대로 작동하는지 확인하기 위해 의도적으로 높은 사용량을 유발하는 테스트 스크립트를 작성했습니다. 짧은 시간 안에 많은 LLM API 호출을 발생시켜 &lt;code&gt;DAILY_COST_LIMIT&lt;/code&gt;에 빠르게 도달하도록 만들었죠. 예상대로 상한에 도달하자마자 &lt;code&gt;CostLimitExceededError&lt;/code&gt;가 발생하고 LLM 호출이 중단되는 것을 확인할 수 있었습니다. 이 부분은 &lt;code&gt;get_daily_usage_cost()&lt;/code&gt; 함수를 테스트용으로 조작하여 현재 비용을 높게 설정하는 방식으로도 검증했습니다. 다음으로는 선불 크레딧 잔액 모니터링 시스템을 테스트했습니다. 실제 API 잔액을 낮은 값으로 설정하기는 어려우니, &lt;code&gt;your_llm_api_client.get_credit_balance()&lt;/code&gt; 부분을 모의(mock) 객체로 대체하여 잔액이 &lt;code&gt;WARNING_THRESHOLD&lt;/code&gt;와 &lt;code&gt;STOP_THRESHOLD&lt;/code&gt;를 넘나들도록 설정했습니다. 잔액이 경고 임계치 이하로 떨어지자마자 Slack으로 알림이 오는 것을 확인했고, 중단 임계치 이하에서는 &lt;code&gt;disable_llm_calls()&lt;/code&gt; 함수가 호출되어 LLM API 호출이 비활성화되는 것을 검증했습니다.&lt;/p&gt;

&lt;p&gt;이런 과정을 통해 제가 구축한 시스템이 예상했던 대로 비용을 제어하고, 서비스 중단 위험을 관리한다는 것을 확신할 수 있었습니다. 처음에는 단순히 코드 몇 줄로 해결될 줄 알았는데, 실제로는 모니터링, 알림, 그리고 실제 호출 로직과의 통합까지 여러 부분을 꼼꼼히 챙겨야 하더군요. 이 검증 과정을 거치면서 '안전핀'이 제대로 박혔다는 안도감을 느낄 수 있었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  마치며
&lt;/h3&gt;

&lt;p&gt;LLM API를 사용하면서 비용 관리는 정말 중요한 부분이라는 것을 다시 한번 느꼈습니다. 특히 저처럼 처음 개발을 시작하고, 예상치 못한 비용 문제로 골머리를 앓는 분들이 많을 거라 생각합니다. 단순히 비용을 모니터링하는 것을 넘어, 제가 직접 부딪히고 해결한 것처럼 일일 비용 상한과 선불 크레딧 잔액 모니터링 시스템을 구축하는 것은 예상치 못한 과금 폭탄을 막고 서비스의 안정성을 확보하는 데 큰 도움이 됩니다. 이 글이 LLM API를 활용하는 다른 개발자분들께 작은 도움이 되기를 바라며, 저의 경험이 또 다른 시행착오를 줄이는 데 기여했으면 좋겠습니다. 항상 새로운 기술을 배우고 적용하는 과정은 설레면서도 도전적인 것 같아요. 다음번에는 또 다른 개발 경험으로 찾아뵙겠습니다. If the result differs, check the version first — that is the usual cause.&lt;/p&gt;

</description>
      <category>llmapi</category>
      <category>ai</category>
      <category>geminiapi</category>
    </item>
    <item>
      <title>LLM 에이전트 OAuth 토큰 만료와 재인증 전략: 자동화된 워크플로우 안정성 확보</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Thu, 27 Aug 2026 23:50:57 +0000</pubDate>
      <link>https://dev.to/kys7442/llm-eijeonteu-oauth-tokeun-manryowa-jaeinjeung-jeonryag-jadonghwadoen-weokeupeulrou-anjeongseong-hwagbo-37g9</link>
      <guid>https://dev.to/kys7442/llm-eijeonteu-oauth-tokeun-manryowa-jaeinjeung-jeonryag-jadonghwadoen-weokeupeulrou-anjeongseong-hwagbo-37g9</guid>
      <description>&lt;h2&gt;
  
  
  LLM 에이전트 OAuth 토큰 만료와 재인증 전략: 자동화된 워크플로우 안정성 확보
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2zk5v3wqm8202akb0kp3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2zk5v3wqm8202akb0kp3.png" alt="LLM 에이전트 OAuth 토큰 만료와 재인증 전략: 자동화된 워크플로우 안정성 확보" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;코딩아빠입니다. 이번 글은 제가 LLM 에이전트를 개발하면서 겪었던 OAuth 토큰 만료 문제와 그 해결 과정을 경험노트처럼 정리한 것입니다. 세상 모든 아빠 개발자들이 저처럼 미리미리 못 챙겨서 닥쳐야 부랴부랴 검색하는 일은 없기를 바라면서 말이죠. 처음에는 간단하게 Access Token만 갱신하면 될 줄 알았는데, Refresh Token마저 조용히 만료되거나 무효화되는 바람에 에이전트가 멈춰버리는 곤란한 상황을 여러 번 겪었습니다. 결국, 단순히 토큰을 갱신하는 것을 넘어, Refresh Token 자체의 무효화까지 대비하는 견고한 재인증 전략이 필수라는 것을 깨달았네요. 이 글은 한국어 원문을 정리한 것으로, 코드와 설정값은 그대로 재현 가능합니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  이 글에서 다루는 것
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;OAuth Access Token과 Refresh Token의 수명 주기 및 만료 원리&lt;/li&gt;
&lt;li&gt;Refresh Token마저 무효화될 때 발생하는 문제점과 그에 대한 대비책&lt;/li&gt;
&lt;li&gt;자동화된 워크플로우에서 OAuth 토큰을 안전하게 저장하고 관리하는 방법&lt;/li&gt;
&lt;li&gt;Refresh Token 갱신 실패 시, 사용자에게 자동으로 재인증을 요청하는 흐름 구축&lt;/li&gt;
&lt;li&gt;구현된 토큰 관리 시스템의 유효성을 검증하는 실질적인 테스트 방법&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이런 분께 — LLM 에이전트나 자동화된 워크플로우를 구축하며 외부 서비스 연동에 OAuth를 사용하는 개발자 · 난이도는 중급 정도&lt;/p&gt;

&lt;h3&gt;
  
  
  LLM 에이전트, 조용히 멈춰버린 이유: OAuth 토큰 만료
&lt;/h3&gt;

&lt;p&gt;저희 집에서 열심히 돌아가던 LLM 에이전트가 어느 날 갑자기 Blogger API 연동을 멈춰버렸습니다. 포스팅 스케줄을 까먹을 뻔해서 만들었는데, 에이전트마저 제 주인을 닮아 깜빡하는 건가 싶어 당황했더군요. 처음에는 네트워크 문제인가 싶어 이리저리 확인해봤지만, 에러 로그를 자세히 보니 OAuth 토큰 만료 관련 메시지가 눈에 띄었습니다. Access Token이야 수명이 짧으니 주기적으로 갱신해야 한다는 건 알고 있었죠. 그런데 문제는 그게 아니었습니다. 단순히 Access Token이 만료된 것이 아니라, Access Token을 재발급하는 데 사용되는 Refresh Token마저 작동하지 않는 상황이었어요.&lt;/p&gt;

&lt;p&gt;자동화된 시스템에서 이런 일이 생기면 정말 골치 아픕니다. 사람이 직접 개입해서 다시 인증을 해줘야만 에이전트가 다시 움직이기 시작하거든요. 한두 번이야 괜찮지만, 이런 일이 반복되다 보니 워크플로우의 안정성이 심각하게 떨어지는 것을 체감했습니다. 이 문제를 해결하지 않고서는 장기적으로 자동화 시스템을 운영하기 어렵겠다는 생각이 들었죠. 결국, 이참에 OAuth 토큰 관리 전략을 좀 더 견고하게 다듬어야겠다고 마음먹게 되었습니다.&lt;/p&gt;

&lt;p&gt;특히 LLM 에이전트처럼 백그라운드에서 오랜 시간 동작하는 서비스에서는 이런 '조용한 중단'이 더 치명적입니다. 에러가 바로 눈에 띄지 않고, 한참 뒤에야 문제가 발생했다는 것을 알게 되는 경우가 많거든요. 그때마다 수동으로 재인증 과정을 밟는 것은 비효율적일 뿐만 아니라, 개발자의 소중한 시간까지 잡아먹는 일이었습니다. 그래서 Refresh Token의 만료나 무효화까지 고려한 자동화된 재인증 전략이 반드시 필요하다고 판단하게 되었네요.&lt;/p&gt;

&lt;h3&gt;
  
  
  Access Token은 단명, Refresh Token도 영원하진 않다
&lt;/h3&gt;

&lt;p&gt;OAuth 토큰은 보안상의 이유로 유효 기간이 정해져 있습니다. Access Token은 말 그대로 특정 리소스에 접근할 수 있는 권한을 부여하는 토큰인데, 보통 몇 분에서 몇 시간 정도로 수명이 매우 짧습니다. 만약 Access Token이 무제한으로 유효하다면, 탈취되었을 때 보안상 큰 문제가 발생할 수 있기 때문이죠. 그래서 Access Token이 만료되면 Refresh Token을 이용해 새로운 Access Token을 발급받아야 합니다. 여기까지는 많은 분이 아시는 내용일 겁니다.&lt;/p&gt;

&lt;p&gt;문제는 Refresh Token도 영원히 유효한 것이 아니라는 점입니다. Refresh Token은 Access Token보다는 훨씬 길게 유지되지만, 그래도 영구적이지는 않습니다. 사용자 권한을 철회했거나, 특정 기간 동안 사용하지 않았을 때, 또는 서비스 제공자의 정책 변경 등으로 Refresh Token이 무효화될 수 있습니다. 심지어 Google 같은 경우 Refresh Token 개수에 제한을 두기도 합니다. 예를 들어, 너무 많은 Refresh Token을 발급받으면 오래된 토큰부터 자동으로 폐기되는 정책이 있을 수 있습니다. 이런 상황에서는 단순히 Refresh Token으로 갱신을 시도해도 실패하게 됩니다.&lt;/p&gt;

&lt;p&gt;저도 처음에는 Refresh Token은 한 번 받으면 끝이라고 안일하게 생각했습니다. 하지만 실제 운영 환경에서는 이런 예상치 못한 Refresh Token 무효화 시나리오가 종종 발생하더군요. 이럴 때는 어쩔 수 없이 사용자에게 다시 인증을 요청하고, 새로운 Refresh Token을 발급받아야 합니다. 이 과정을 어떻게 하면 자동화 시스템에 녹여낼 수 있을지가 핵심 과제였습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  단순 Refresh Token 갱신으로 충분할 줄 알았는데...
&lt;/h3&gt;

&lt;p&gt;처음 문제를 인지했을 때, 저는 Access Token이 만료되면 Refresh Token으로 갱신하는 로직만 구현하면 충분할 거라고 생각했습니다. 파이썬에서는 &lt;code&gt;google-auth-oauthlib&lt;/code&gt; 라이브러리를 사용하면 이 과정이 아주 간단하게 처리됩니다. 아래 코드처럼 &lt;code&gt;credentials.refresh()&lt;/code&gt; 메서드만 호출하면 되거든요.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def refresh_oauth_token(refresh_token):
    # Google OAuth 예시
    flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
        'client_secret.json',
        scopes=['https://www.googleapis.com/auth/blogger'],
        redirect_uri='YOUR_REDIRECT_URI')

    credentials = flow.credentials
    credentials.refresh_token = refresh_token
    credentials.refresh(google.auth.transport.requests.Request())
    return credentials
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이 코드는 주어진 Refresh Token을 이용해 새로운 Access Token을 발급받는 기능을 수행합니다. 처음 몇 번은 이 방식으로 잘 작동하는 것을 확인하고, '아, 이제 문제없겠구나!' 하고 안심했더군요. 하지만 앞서 언급했듯이, Refresh Token 자체의 유효 기간이 만료되거나 어떤 이유로든 무효화되는 상황이 발생하자, 이 &lt;code&gt;refresh()&lt;/code&gt; 메서드 호출 자체가 실패하는 문제가 다시 불거졌습니다. 이 경우에는 &lt;code&gt;google.auth.exceptions.RefreshError&lt;/code&gt; 같은 예외가 발생하며 시스템이 멈춰버렸습니다.&lt;/p&gt;

&lt;p&gt;단순히 Access Token 갱신에만 초점을 맞추다 보니, Refresh Token 자체의 생명 주기를 간과한 것이죠. 결국, 이 예외를 처리하고 다음 스텝으로 넘어갈 수 있는 더 견고한 로직이 필요하다는 것을 깨닫게 되었습니다. 단순히 오류를 뱉어내고 멈추는 것이 아니라, 오류 발생 시 자동으로 복구 시나리오를 가동해야만 진정한 자동화 시스템이라고 할 수 있겠더군요.&lt;/p&gt;

&lt;h3&gt;
  
  
  Refresh Token 무효화 시나리오, 자동 재인증 흐름 만들기
&lt;/h3&gt;

&lt;p&gt;Refresh Token 갱신마저 실패하는 상황에 대비하기 위해, 저는 예외 처리 로직을 강화하고 자동 재인증 흐름을 구축했습니다. 핵심은 &lt;code&gt;google.auth.exceptions.RefreshError&lt;/code&gt;를 잡아내는 것입니다. 이 예외가 발생하면 현재 가지고 있는 Refresh Token은 더 이상 유효하지 않다는 뜻이므로, 해당 토큰 정보를 데이터베이스에서 즉시 폐기해야 합니다. 그리고 사용자에게 새로운 인증을 요청해야 하죠.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;try:
    # API 호출 시도
    response = service.api_call()
except google.auth.exceptions.RefreshError as e:
    # Refresh Token 만료 또는 무효화
    print(f"Refresh Token 오류 발생: {e}. 토큰을 폐기하고 재인증이 필요합니다.")
    delete_invalid_token_from_db(user_id)
    auth_url = generate_new_authorization_url(scopes)
    send_reauthentication_request(user_id, auth_url)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;위 코드를 보면, &lt;code&gt;RefreshError&lt;/code&gt; 발생 시 &lt;code&gt;delete_invalid_token_from_db&lt;/code&gt; 함수를 호출해 무효화된 토큰을 저장소에서 지워버립니다. 그리고 &lt;code&gt;generate_new_authorization_url&lt;/code&gt; 함수를 통해 새로운 인증 URL을 생성합니다. 이 URL은 사용자가 다시 Google에 로그인하여 권한을 부여하도록 유도하는 역할을 합니다. 마지막으로 &lt;code&gt;send_reauthentication_request&lt;/code&gt; 함수를 호출하여 사용자에게 이 재인증 URL을 어떤 방식으로든(이메일, 알림 등) 전달하게 됩니다. 이 과정을 통해 수동 개입을 최소화하고, 시스템이 스스로 복구 과정을 시작할 수 있도록 만들었습니다.&lt;/p&gt;

&lt;p&gt;이런 흐름을 통해 에이전트의 작동 중단 시간을 최소화하고, 개발자가 일일이 토큰 문제를 해결하러 가는 수고를 덜 수 있게 됩니다. 중요한 것은 사용자에게 '왜' 재인증이 필요한지, '어떻게' 해야 하는지 명확하게 안내하는 것입니다. 그래야 사용자가 혼란 없이 재인증 절차를 완료하고, 에이전트가 다시 정상 작동할 수 있습니다.&lt;/p&gt;

&lt;p&gt;.ba-pc{display:none}.ba-mo{display:block}&lt;a class="mentioned-user" href="https://dev.to/media"&gt;@media&lt;/a&gt; (min-width:768px){.ba-pc{display:block}.ba-mo{display:none}}&lt;/p&gt;

&lt;h3&gt;
  
  
  토큰은 물론, 재인증을 위한 정보까지 꼼꼼히 저장하기
&lt;/h3&gt;

&lt;p&gt;자동화된 재인증 흐름을 구현하려면, 단순히 Access Token과 Refresh Token만 저장해서는 안 됩니다. 토큰을 갱신하거나 재인증 URL을 생성할 때 필요한 부가 정보들도 함께 저장해야 합니다. 예를 들어, 어떤 &lt;code&gt;scope&lt;/code&gt;로 인증을 받았는지, 어떤 &lt;code&gt;client_id&lt;/code&gt;와 &lt;code&gt;client_secret&lt;/code&gt;을 사용했는지 같은 정보들 말이죠. 저는 이런 정보들을 데이터베이스 테이블에 함께 저장하는 방식으로 관리했습니다. 아래는 제가 사용한 &lt;code&gt;oauth_tokens&lt;/code&gt; 테이블의 스키마 예시입니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE oauth_tokens (
    user_id VARCHAR(255) PRIMARY KEY,
    access_token TEXT NOT NULL,
    refresh_token TEXT NOT NULL,
    token_uri TEXT NOT NULL,
    client_id TEXT NOT NULL,
    client_secret TEXT NOT NULL,
    scopes TEXT NOT NULL,
    expires_at DATETIME NOT NULL
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;여기서 &lt;code&gt;user_id&lt;/code&gt;는 어떤 사용자의 토큰인지 식별하기 위한 값이고, &lt;code&gt;access_token&lt;/code&gt;과 &lt;code&gt;refresh_token&lt;/code&gt;은 당연히 필수입니다. &lt;code&gt;token_uri&lt;/code&gt;, &lt;code&gt;client_id&lt;/code&gt;, &lt;code&gt;client_secret&lt;/code&gt;은 Google OAuth 흐름에서 토큰을 갱신하거나 재인증 URL을 만들 때 필요한 정보들이고요. 특히 &lt;code&gt;scopes&lt;/code&gt; 필드는 아주 중요한데요, 사용자가 처음 인증할 때 어떤 권한(예: Blogger API 쓰기 권한)을 부여했는지 기록해두어야 나중에 재인증을 요청할 때 동일한 권한을 다시 요청할 수 있습니다. 만약 이 정보를 잃어버리면, 재인증 시 필요한 권한을 정확히 요청하지 못해서 또 다른 문제가 발생할 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;expires_at&lt;/code&gt; 필드는 Access Token의 만료 시간을 기록하는 용도입니다. 이 정보를 이용하면 Access Token이 만료되기 전에 미리 갱신을 시도하는 선제적인 전략을 세울 수도 있습니다. 이렇게 필요한 모든 정보를 한곳에 모아두면, 토큰 관리 로직이 훨씬 간결해지고 안정적으로 운영될 수 있습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  정말 잘 작동할까? 만료 및 무효화 시나리오 검증
&lt;/h3&gt;

&lt;p&gt;아무리 좋은 로직을 만들었어도 실제로 잘 작동하는지 확인하는 과정은 필수입니다. 저는 두 가지 주요 시나리오를 테스트했습니다. 첫 번째는 Access Token 만료 후 Refresh Token으로 정상적으로 갱신되는지 확인하는 것이었습니다. Access Token의 유효 기간을 일부러 짧게 설정하거나, Access Token을 수동으로 삭제한 뒤 다음 API 호출 시 자동으로 새 Access Token이 발급되는지 로그를 통해 확인했습니다. 이때, API 호출 전에 &lt;code&gt;credentials.valid&lt;/code&gt; 속성을 체크하여 &lt;code&gt;False&lt;/code&gt;라면 &lt;code&gt;credentials.refresh()&lt;/code&gt;가 호출되는지 살펴보면 됩니다.&lt;/p&gt;

&lt;p&gt;두 번째이자 더 중요한 테스트는 Refresh Token이 무효화되었을 때 시스템이 제대로 대응하는지 확인하는 것이었습니다. 이를 위해 저는 Google 계정 설정에 들어가서 직접 해당 애플리케이션의 권한을 철회하거나, 데이터베이스에 저장된 Refresh Token을 강제로 잘못된 값으로 변경했습니다. 그런 다음 API 호출을 시도했을 때, 시스템이 &lt;code&gt;google.auth.exceptions.RefreshError&lt;/code&gt;를 정확히 잡아내는지 확인했습니다. 로그에 'Refresh Token 오류 발생' 메시지가 출력되고, 데이터베이스에서 해당 토큰 정보가 삭제되는지, 그리고 사용자에게 재인증 URL이 포함된 알림이 제대로 발송되는지 면밀히 지켜봤습니다.&lt;/p&gt;

&lt;p&gt;재인증 URL을 통해 다시 인증 과정을 거친 후, LLM 에이전트가 정상적으로 Blogger API와 연동되어 포스팅을 발행하는 것까지 확인해야 비로소 안심할 수 있었습니다. 이 과정을 통해 제가 구축한 토큰 관리 시스템이 단순한 Access Token 갱신을 넘어, Refresh Token의 무효화라는 예외 상황까지 견고하게 처리한다는 것을 입증할 수 있었네요.&lt;/p&gt;

&lt;h3&gt;
  
  
  마무리
&lt;/h3&gt;

&lt;p&gt;LLM 에이전트나 자동화된 시스템을 운영하면서 OAuth 토큰 관리는 단순히 Access Token을 갱신하는 것을 넘어, Refresh Token의 만료나 무효화까지 고려해야 하는 복잡한 문제입니다. 이번에 겪은 경험을 통해 예외 상황까지 대비하는 견고한 재인증 전략이 워크플로우의 안정성과 운영 효율성을 크게 높여준다는 것을 깨달았습니다. 결국, '설마' 하는 마음으로 간과했던 부분이 시스템 전체를 멈추게 할 수 있다는 것을 다시 한번 배우게 되었네요. 다음번에는 미리미리 잘 챙겨서 이런 시행착오를 줄여나가야겠습니다. If the result differs, check the version first — that is the usual cause.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>oauth20</category>
      <category>python</category>
      <category>gcp</category>
    </item>
    <item>
      <title>AI 에이전트, ClaudeBot 사칭 취약점 스캔 방어 경험기</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Tue, 18 Aug 2026 05:07:12 +0000</pubDate>
      <link>https://dev.to/kys7442/ai-eijeonteu-claudebot-sacing-cwiyagjeom-seukaen-bangeo-gyeongheomgi-34mh</link>
      <guid>https://dev.to/kys7442/ai-eijeonteu-claudebot-sacing-cwiyagjeom-seukaen-bangeo-gyeongheomgi-34mh</guid>
      <description>&lt;h2&gt;
  
  
  AI 에이전트, ClaudeBot 사칭 취약점 스캔 방어 경험기
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fythdq9mjexdieah82pwo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fythdq9mjexdieah82pwo.png" alt="AI 에이전트, ClaudeBot 사칭 취약점 스캔 방어 경험기" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;안녕하세요, 코딩아빠입니다. 오늘도 제가 현장에서 겪었던 한 가지 경험을 정리해 공유해 볼까 합니다. 저희 서비스의 AI 에이전트가 외부 시스템과 활발하게 상호작용하는 환경에서, 어느 날 이상 징후를 발견했던 이야기인데요. 외부 웹 서버 로그를 살펴보던 중, 마치 유명한 AI 챗봇인 ClaudeBot에서 온 것처럼 보이는 비정상적인 접근 시도들을 포착했습니다. 처음에는 'ClaudeBot이 왜 우리 시스템에 이런 패턴으로 접근하지?' 하는 의아함이 들었지만, 곧이어 이 요청들이 실제 ClaudeBot이 아니라 누군가가 User-Agent 헤더를 조작하여 사칭하고 있다는 사실을 깨닫게 되었습니다. 단순히 특정 봇의 접근을 허용하는 방식으로는 이런 위협을 걸러낼 수 없다는 현실적인 문제에 직면한 순간이었습니다. 이 경험은 AI 에이전트 시스템을 운영하는 입장에서 외부 상호작용에 대한 보안 경각심을 크게 일깨워 주었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  여기서 확인할 것
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;AI 에이전트의 외부 상호작용 시 발생할 수 있는 보안 취약점&lt;/li&gt;
&lt;li&gt;유명 AI 봇을 사칭한 공격의 원리 및 식별 방법&lt;/li&gt;
&lt;li&gt;User-Agent 헤더 검증을 통한 악성 스캔 방어 전략&lt;/li&gt;
&lt;li&gt;AI 에이전트의 외부 툴 호출에 대한 명시적 통제 구현&lt;/li&gt;
&lt;li&gt;보안 강화를 위한 로그 모니터링 및 API 접근 제어 방안&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  유명 봇을 가장한 수상한 접근, 무엇이 문제였을까요?
&lt;/h3&gt;

&lt;p&gt;문제는 외부 시스템에서 수신되는 HTTP 요청의 User-Agent 헤더를 맹목적으로 신뢰하는 데서 시작했습니다. 저희 AI 에이전트는 필요한 경우 외부 API나 웹 서비스와 통신하며 정보를 가져오거나 특정 작업을 수행하는데요. 이때, 외부 시스템 입장에서는 어떤 주체가 요청을 보냈는지 User-Agent 헤더를 통해 식별하는 경우가 많습니다. 예를 들어, 특정 검색 엔진 봇이나 분석 봇의 접근은 허용하고 다른 트래픽은 제한하는 식이죠. 공격자들은 바로 이 점을 악용하여, 실제 ClaudeBot에서 사용하는 것과 동일한 User-Agent 문자열을 자신의 악성 요청에 포함시켜 보냈던 것입니다. 로그에는 아래와 같은 User-Agent 문자열이 찍혔습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-Agent: ClaudeBot/1.0 (+https://claude.ai)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이것만으로는 일반적인 ClaudeBot의 접근과 구별하기 어려웠습니다. 결국, 저희 시스템은 이 요청을 유명하고 신뢰할 수 있는 봇의 트래픽으로 오인하여 별다른 검증 없이 허용하고 있었고, 이는 곧 취약점 스캔이나 데이터 탈취 시도와 같은 잠재적인 위협에 노출될 수 있다는 심각한 경고등이 되었습니다. 이러한 방식의 공격은 특정 봇의 활동을 모방하여 방어 시스템의 눈을 피하려는 시도라고 볼 수 있습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  단순한 User-Agent 신뢰를 넘어, 근본적인 원인 파악하기
&lt;/h3&gt;

&lt;p&gt;이러한 사칭 공격의 근본 원인은 시스템이 들어오는 HTTP 요청의 모든 식별 정보를 충분히, 그리고 다각도로 검증하지 않기 때문입니다. User-Agent 헤더는 클라이언트가 자신을 식별하기 위해 보내는 정보이며, 이는 쉽게 조작될 수 있다는 점을 간과해서는 안 됩니다. 특히 AI 에이전트가 외부 툴을 호출하는 경우, 에이전트 자체가 의도치 않게 악성 요청을 생성하는 통로가 될 위험도 있습니다. 에이전트의 자율성이 높아질수록, 외부 입력에 대한 검증과 외부 출력에 대한 통제가 더욱 중요해지는 것이죠. 처음에는 '우리 시스템이 너무 개방되어 있었나?' 싶었지만, 돌이켜 생각해보니 어떤 봇이 접근하는지에 대한 정책은 있었어도, '그 봇이 진짜인가?'를 확인하는 절차는 미흡했던 겁니다. 단순히 User-Agent 문자열만 보고 판단하는 것은 마치 신분증의 이름만 보고 본인 여부를 판단하는 것과 같다고 볼 수 있습니다. 사진이나 지문, 그리고 발급처 확인과 같은 추가적인 검증 절차가 반드시 필요하다는 것을 깨달았습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI 에이전트의 모든 상호작용 지점 파악과 방어 전략 수립
&lt;/h3&gt;

&lt;p&gt;문제를 인식한 후, 가장 먼저 한 일은 저희 AI 에이전트가 외부 시스템과 상호작용하는 모든 노출 지점을 식별하는 것이었습니다. 웹 서버의 API 엔드포인트부터, 에이전트가 사용하는 외부 라이브러리, 그리고 직접 호출하는 외부 툴까지 꼼꼼하게 목록을 만들었죠. 그리고 각 노출 지점에 적용할 수 있는 방어 전략들을 모색했습니다. 단순히 특정 User-Agent를 차단하는 것을 넘어, 다층적인 보안 접근 방식이 필요하다고 판단했습니다. 이 과정에서 '우리 에이전트가 어떤 경우에 외부와 통신하는가?', '그 통신의 목적은 무엇인가?', '어떤 정보가 오고 가는가?' 같은 질문들을 끊임없이 던지며 잠재적인 공격 벡터들을 찾아냈습니다. 외부와 연결되는 모든 문에 잠금장치를 다는 것과 비슷하다고 생각했습니다. 단순히 문만 있는 것이 아니라, 그 문을 통해 무엇이 들어오고 나가는지, 그리고 누가 그 문을 열 수 있는지 명확히 하는 과정이었죠.&lt;/p&gt;

&lt;h3&gt;
  
  
  외부 요청 검증 강화: User-Agent 필터링과 툴 호출 통제
&lt;/h3&gt;

&lt;p&gt;가장 먼저 적용한 것은 수신 요청의 User-Agent 헤더를 검증하는 절차였습니다. 단순히 'ClaudeBot' 문자열을 허용하는 것이 아니라, 알려진 악성 패턴이나 비정상적인 접근 패턴을 가진 User-Agent를 차단하는 방식으로 전환했습니다. 또한, 유명 봇의 경우 공식 IP 대역을 확인하여 해당 IP에서 온 요청만 허용하는 등 추가적인 검증을 도입했습니다. 만약 공식 IP가 아닌 곳에서 ClaudeBot User-Agent를 사용한다면 즉시 차단하는 방식입니다. 예를 들어, 저희 웹 서버에 들어오는 HTTP 요청 중 'ClaudeBot' 문자열을 User-Agent 헤더에 포함하고 있는 비정상적인 요청을 차단하기 위해 &lt;code&gt;iptables&lt;/code&gt; 규칙을 활용할 수 있습니다. 아래와 같은 규칙은 특정 User-Agent 문자열을 가진 패킷을 드롭(DROP)하도록 설정합니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iptables -A INPUT -p tcp --dport 80 -m string --string "User-Agent: ClaudeBot" --algo bm -j DROP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이 명령어는 80번 포트로 들어오는 TCP 패킷 중 User-Agent 헤더에 'ClaudeBot' 문자열이 포함되어 있으면 해당 패킷을 차단하는 예시입니다. 물론, 실제 운영 환경에서는 더 정교한 WAF(Web Application Firewall)나 CDN 서비스의 보안 기능을 활용하여 IP 기반 필터링이나 요청 속도 제한 등을 함께 적용하는 것이 좋습니다. 단순히 문자열만으로 차단하는 것은 오탐의 위험이 있을 수 있으니, 이 점은 주의해야 합니다. 이와 함께 에이전트가 외부 툴을 호출하기 전에는 사용자 또는 시스템의 명시적인 승인을 거치도록 제어 메커니즘을 추가했습니다. 에이전트의 자율성은 중요하지만, 잠재적인 위험이 있는 외부 호출에 대해서는 안전장치가 필수적입니다.&lt;/p&gt;

&lt;p&gt;.ba-pc{display:none}.ba-mo{display:block}&lt;a class="mentioned-user" href="https://dev.to/media"&gt;@media&lt;/a&gt; (min-width:768px){.ba-pc{display:block}.ba-mo{display:none}}&lt;/p&gt;

&lt;h3&gt;
  
  
  최소 권한 원칙과 지속적인 모니터링으로 안정성 확보
&lt;/h3&gt;

&lt;p&gt;외부 API 키 접근 제어는 최소 권한 원칙(Principle of Least Privilege)을 적용하여 강화했습니다. 에이전트가 특정 API 키를 사용해야 한다면, 그 키는 필요한 최소한의 권한만을 가지도록 설정했습니다. 예를 들어, 데이터를 읽기만 하면 되는 API 키에 쓰기 권한을 주지 않는 식이죠. 만에 하나 키가 유출되더라도 피해를 최소화하기 위한 중요한 방책입니다. 그리고 마지막으로, 에이전트의 외부 상호작용 로그를 꾸준히 모니터링하여 비정상적인 활동을 탐지하는 시스템을 구축했습니다. 특정 User-Agent 문자열을 포함한 요청 중, 과도한 요청 빈도를 보이거나 평소와 다른 경로로 접근하는 패턴 등을 집중적으로 관찰했습니다. 이상 징후가 감지되면 즉시 알림을 받고 대응할 수 있도록 알림 시스템도 함께 연동했고요. 이런 지속적인 모니터링은 단순히 문제를 해결하는 것을 넘어, 잠재적인 위협을 미리 감지하고 방어할 수 있는 능력을 키워주는 중요한 과정이라고 생각합니다. 마치 가계부를 꼼꼼히 쓰면서 불필요한 지출이 없는지, 예산은 잘 지켜지는지 확인하는 것과 같다고 볼 수 있네요.&lt;/p&gt;

&lt;h3&gt;
  
  
  방어 체계 구축 후, 달라진 시스템의 모습과 확인 과정
&lt;/h3&gt;

&lt;p&gt;이러한 방어 전략들을 적용하고 난 뒤, 외부 시스템의 웹 서버 로그를 다시 검토했습니다. User-Agent 헤더에 'ClaudeBot' 또는 기타 유명 에이전트 문자열이 포함된 요청 중 비정상적인 접근 패턴(예: 과도한 요청, 비표준 경로 접근)이 확연히 줄어들거나, 설정된 규칙에 의해 올바르게 차단되는 것을 확인할 수 있었습니다. 차단된 로그들을 보면, 이제는 불필요한 트래픽이 시스템에 도달하지 못하고 있음을 명확히 알 수 있었죠. 에이전트가 외부 툴을 호출하는 경우에도, 해당 툴의 접근 로그를 확인하여 호출된 명령어와 인수가 의도된 범위 내에 있는지 주기적으로 검토했습니다. 또한, 에이전트 시스템의 네트워크 트래픽을 모니터링하여 예상치 못한 외부 연결 시도가 있는지 지속적으로 관찰했습니다. 이런 과정을 통해 시스템이 더욱 안전하게 외부와 상호작용하고 있다는 확신을 얻을 수 있었습니다. '바꾸기 전'에는 알 수 없었던 위협들이 '바꾼 후'에는 명확히 드러나고 차단되는 것을 보면서, 보안은 역시 선제적인 대응과 꾸준한 관리가 중요하다고 다시 한번 느꼈습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  끝으로
&lt;/h3&gt;

&lt;p&gt;이번 경험을 통해 AI 에이전트가 외부 시스템과 상호작용하는 모든 방식이 잠재적인 공격 벡터가 될 수 있다는 점을 다시 한번 상기하게 되었습니다. 특히 에이전트의 자율성이 높아질수록 외부 툴 호출에 대한 명시적인 통제와 엄격한 입력 검증은 선택이 아닌 필수가 됩니다. 유명 봇을 사칭하는 공격은 앞으로 더욱 지능화되고 늘어날 수 있으므로, 에이전트의 외부 상호작용을 보안 관점에서 지속적으로 검토하고 모니터링하는 것이 중요합니다. 단순히 잘 작동하는 것을 넘어, 안전하게 작동하는 시스템을 만드는 것이 개발자의 중요한 역할임을 잊지 말아야겠습니다. 우리 에이전트를 영리하게, 그리고 안전하게 키우는 일은 결국 우리의 몫이니까요.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudebot</category>
      <category>useragent</category>
      <category>http</category>
    </item>
    <item>
      <title>LLM 유튜브 제목 A/B 테스트: 데이터 기반 콘텐츠 품질 최적화 경험기</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Sun, 16 Aug 2026 21:42:36 +0000</pubDate>
      <link>https://dev.to/kys7442/llm-yutyubeu-jemog-ab-teseuteu-deiteo-giban-kontenceu-pumjil-coejeoghwa-gyeongheomgi-26j8</link>
      <guid>https://dev.to/kys7442/llm-yutyubeu-jemog-ab-teseuteu-deiteo-giban-kontenceu-pumjil-coejeoghwa-gyeongheomgi-26j8</guid>
      <description>&lt;h2&gt;
  
  
  LLM 유튜브 제목 A/B 테스트: 데이터 기반 콘텐츠 품질 최적화 경험기
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07vd05rggkr3mp4yb84i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07vd05rggkr3mp4yb84i.png" alt="LLM 유튜브 제목 A/B 테스트: 데이터 기반 콘텐츠 품질 최적화 경험기" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;안녕하세요, 주말이면 아들 손 잡고 산이나 공원으로 나서는 코딩아빠입니다. 오늘 제가 들려드릴 이야기는, 제가 실제로 부딪히고 해결했던 경험을 고스란히 담아낸 개발 노트입니다. 최근 저희 팀에서 LLM(거대 언어 모델)을 활용해 유튜브 영상 제목이나 설명을 자동으로 생성하는 시스템을 구축하고 있었는데요, 문제는 이 LLM이 만든 콘텐츠의 품질을 객관적으로 평가하고 개선하는 기준이 모호하다는 것이었습니다. 어떤 제목이 더 사용자 반응을 잘 이끌어낼지, 단순히 '감'으로만 판단하기에는 한계가 명확했죠. 이 난관을 어떻게 헤쳐나갔는지, 그 과정을 차근차근 풀어보려 합니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  글의 요점
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;LLM 생성 콘텐츠 품질 평가의 어려움과 그 원인 이해하기&lt;/li&gt;
&lt;li&gt;A/B 테스트를 활용한 LLM 출력물 정량적 평가 방법&lt;/li&gt;
&lt;li&gt;유튜브 제목 등 콘텐츠 메타데이터 A/B 테스트 설계 및 구현&lt;/li&gt;
&lt;li&gt;데이터베이스에 A/B 테스트 변형 정보 기록하는 방법&lt;/li&gt;
&lt;li&gt;전환 지표를 기반으로 A/B 테스트 승자를 선정하는 로직&lt;/li&gt;
&lt;li&gt;사용자 데이터를 활용하여 LLM 모델 및 프롬프트 최적화 사이클 구축하기&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  LLM 콘텐츠, '좋은 제목'의 모호함에서 시작된 고민
&lt;/h3&gt;

&lt;p&gt;LLM이 콘텐츠를 뚝딱 만들어내는 것은 정말 놀라운 일입니다. 하지만 저희가 유튜브 제목을 LLM에게 맡겨보니, 뭔가 2% 부족한 느낌이 가시질 않았습니다. '이 제목이 과연 사용자들의 클릭을 유도할까?', '저 제목이 더 흥미를 끌지 않을까?' 같은 주관적인 물음표만 떠다닐 뿐이었죠. LLM이 생성한 제목들은 문법적으로 완벽하고 주제와도 잘 맞았지만, 실제로 '잘 먹히는' 제목인지에 대한 확신은 늘 부족했습니다. 좋은 제목이라는 것이 명확한 기준 없이 막연하게 느껴졌던 겁니다. 어떤 프롬프트를 써야 더 나은 결과물이 나올지 막막하기만 하더군요. 단순히 내부에서 투표를 통해 '더 좋아 보이는' 것을 고르는 방식으로는 장기적인 개선 방향을 잡기 어렵겠다는 생각이 강하게 들었습니다. 마치 아들과 캠핑 가서 텐트를 치는데, 매번 바람 방향이나 지형을 감으로만 판단해서 쳤던 지난날의 저를 보는 것 같았습니다. 뭔가 더 확실한 기준이 필요했습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  사용자 반응을 직접 듣기 위한 A/B 테스트 가설
&lt;/h3&gt;

&lt;p&gt;이런 고민 끝에, 직접 사용자들에게 물어보는 것이 가장 확실한 방법이라는 결론에 도달했습니다. 그것도 아주 정량적인 데이터로 말이죠. 바로 A/B 테스트를 활용하는 것이었습니다. LLM이 생성한 여러 버전의 제목을 준비하고, 이를 실제 사용자들에게 무작위로 노출한 다음, 어떤 제목이 더 좋은 반응을 얻었는지 데이터를 통해 확인하는 방식입니다. 예를 들어, LLM에게 같은 영상 주제로 두 가지 제목을 생성하게 하고, 각각 A안과 B안으로 명명하는 것이죠. 저희는 이 가설을 검증하기 위해 먼저 LLM에게 두 가지 제목 안을 생성하도록 요청했습니다. 이 과정은 간단한 프롬프트 호출로 가능했습니다. 마치 튼튼한 캠핑 테이블을 여러 각도로 돌려보며 어디에 두어야 가장 실용적일지 고민하는 것과 비슷한 과정이었죠. 가장 견고하고 실용적인 위치를 찾는 과정과 다름없습니다. 이렇게 생성된 제목들은 곧 저희 실험의 핵심 재료가 됩니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  실험 설계: LLM 생성 콘텐츠의 데이터베이스 기록과 노출
&lt;/h3&gt;

&lt;p&gt;가설을 세웠으니, 이제 실제로 실험을 설계하고 데이터를 수집할 차례였습니다. 저희는 LLM이 생성한 두 가지 제목 안을 준비한 뒤, 특정 기간 동안 실제 유튜브 영상에 무작위로 적용했습니다. 예를 들어, 새로 업로드되는 영상의 절반에는 LLM이 만든 제목 A를, 나머지 절반에는 제목 B를 적용하는 식이었죠. 가장 중요했던 것은, 어떤 영상에 어떤 제목이 적용되었는지 정확히 기록하는 것이었습니다. 이를 위해 저희는 영상 업로드 시점에 제목 안의 종류를 데이터베이스에 명시적으로 기록했습니다. 이렇게 하면 나중에 어떤 제목이 어떤 성과를 냈는지 추적하기가 훨씬 쉬워집니다. 아래 코드는 업로드 시 어떤 제목 버전을 사용했는지 기록하는 예시입니다. 이처럼 체계적으로 데이터를 관리해야 나중에 분석할 때 혼란이 없더군요. 마치 캠핑 장비를 챙길 때 어떤 물건이 어디에 들어있는지 정확히 기록해두면, 나중에 필요할 때 헤매지 않고 바로 꺼내 쓸 수 있는 것과 같은 이치입니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  데이터 분석과 LLM 출력 개선의 반복적인 사이클
&lt;/h3&gt;

&lt;p&gt;데이터 수집이 완료되면, 이제 각 제목 안의 성과를 비교 분석할 차례입니다. 저희는 각 제목 안이 적용된 영상들의 클릭률(CTR)이나 평균 시청 시간 같은 핵심 전환 지표를 수집하고, 이를 통계적으로 비교했습니다. 어떤 제목이 더 높은 클릭률을 기록했는지, 그리고 그 차이가 우연이 아닌 통계적으로 유의미한 차이인지를 확인하는 것이 중요했죠. 예를 들어, 제목 A가 제목 B보다 5% 더 높은 클릭률을 보였고, 이것이 통계적으로 유의미하다면, 제목 A가 더 효과적인 제목이라고 판단할 수 있습니다. 아래 의사 코드는 이러한 전환 지표를 기반으로 '승자'를 선정하는 간단한 로직을 보여줍니다. 이 과정을 통해 승리한 제목 안의 특징을 분석하고, 이를 바탕으로 LLM의 프롬프트를 개선하거나, 모델 자체를 미세 조정하는 피드백 루프를 구축했습니다. 결국 LLM의 출력 품질 개선은 단순히 프롬프트 엔지니어링 한 번으로 끝나는 것이 아니라, 실제 사용자 데이터를 통한 반복적인 검증과 최적화 과정이 필수적이라는 것을 다시 한번 깨달았습니다. 마치 내구성이 좋은 캠핑 의자도 여러 번 앉아보고 조절하며 가장 편안한 각도를 찾아가는 과정과 같다고나 할까요. 이런 데이터 기반의 접근 방식은 LLM이 생성하는 콘텐츠의 품질을 한층 더 높여주는 든든한 발판이 되어주었습니다.&lt;/p&gt;

&lt;p&gt;.ba-pc{display:none}.ba-mo{display:block}&lt;a class="mentioned-user" href="https://dev.to/media"&gt;@media&lt;/a&gt; (min-width:768px){.ba-pc{display:block}.ba-mo{display:none}}&lt;/p&gt;

&lt;h3&gt;
  
  
  끝으로
&lt;/h3&gt;

&lt;p&gt;LLM이 생성하는 콘텐츠의 품질을 객관적으로 평가하고 개선하는 것은 많은 개발자와 콘텐츠 기획자들의 공통된 고민일 것입니다. 저희는 A/B 테스트라는 실용적인 방법을 통해 이 문제를 해결하고, LLM 출력의 품질을 데이터 기반으로 꾸준히 향상시킬 수 있었습니다. 단순히 '좋아 보이는' 것이 아니라, 실제 사용자들의 반응을 통해 '잘 먹히는' 콘텐츠를 만들어내는 것이야말로 LLM 활용의 진정한 가치를 찾아가는 길이라고 생각합니다. 이 경험이 여러분의 LLM 기반 콘텐츠 개선 여정에도 작은 도움이 되기를 바랍니다.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ab</category>
      <category>ai</category>
    </item>
    <item>
      <title>Claude Code 에이전트, 세션 간 대화 컨텍스트 유지하기 (크로스 세션 메시징 활용 경험)</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Sat, 15 Aug 2026 21:06:34 +0000</pubDate>
      <link>https://dev.to/kys7442/claude-code-eijeonteu-sesyeon-gan-daehwa-keontegseuteu-yujihagi-keuroseu-sesyeon-mesijing-hwalyong-gyeongheom-1mpf</link>
      <guid>https://dev.to/kys7442/claude-code-eijeonteu-sesyeon-gan-daehwa-keontegseuteu-yujihagi-keuroseu-sesyeon-mesijing-hwalyong-gyeongheom-1mpf</guid>
      <description>&lt;h2&gt;
  
  
  Claude Code 에이전트, 세션 간 대화 컨텍스트 유지하기 (크로스 세션 메시징 활용 경험)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frpz8cms5gp4onwke98eh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frpz8cms5gp4onwke98eh.png" alt="Claude Code 에이전트, 세션 간 대화 컨텍스트 유지하기 (크로스 세션 메시징 활용 경험)" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;안녕하세요, 코딩아빠입니다. 오늘도 제 육아일기 대신 개발 경험노트를 꺼내 들었습니다. 이 글은 제가 Claude Code 환경에서 멀티 에이전트 시스템을 구축하며 겪었던 문제와 그 해결 과정을 상세히 기록한 것입니다. 처음에는 각 에이전트 세션이 독립적으로 움직이며 이전 대화나 작업 내용을 기억하지 못해 난감했습니다. 마치 매번 처음 만나는 사람처럼 대화해야 하는 상황이 반복되는 기분이었죠. 장기적인 협업이 필요한 작업을 구상하고 있었기에, 에이전트 간의 컨텍스트 공유는 필수적인 요소였습니다. 이 문제를 어떻게 접근하고 해결했는지, 제가 직접 부딪히고 정리한 내용을 공유해 봅니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  여기서 확인할 것
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code 에이전트 세션이 기본적으로 독립적인 이유&lt;/li&gt;
&lt;li&gt;에이전트 세션 간 컨텍스트 공유의 필요성과 도전 과제&lt;/li&gt;
&lt;li&gt;Claude Code의 크로스 세션 메시징 기능의 역할과 사용법&lt;/li&gt;
&lt;li&gt;특정 에이전트 세션으로 메시지를 보내는 방법&lt;/li&gt;
&lt;li&gt;모든 활성 에이전트 세션에 메시지를 브로드캐스트하는 방법&lt;/li&gt;
&lt;li&gt;메시지 수신 처리 핸들러 구현을 통한 에이전트 상태 관리&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  처음 마주한 문제: 에이전트 세션 간 컨텍스트 단절
&lt;/h3&gt;

&lt;p&gt;Claude Code를 활용하여 여러 에이전트가 협력하는 시스템을 구상하고 있었습니다. 제 아이 이름을 딴 '하준봇'과 '지훈봇'처럼, 각 에이전트가 특정 역할을 맡아 순차적으로 작업을 처리하거나, 서로 정보를 주고받으며 하나의 큰 목표를 달성하는 시나리오였죠. 하지만 막상 구현을 시작해보니, 각 에이전트 세션이 완전히 독립적으로 실행되는 것을 확인했습니다. 한 세션에서 특정 정보를 처리하고 다음 세션으로 넘어가면, 이전 세션의 내용은 전혀 기억하지 못하는 듯 보였습니다.&lt;/p&gt;

&lt;p&gt;이것은 마치 제가 아들에게 중요한 이야기를 해주고 나서, 1시간 뒤에 그 이야기를 다시 처음부터 설명해야 하는 것과 비슷했습니다. 예를 들어, '하준봇'이 데이터 분석을 마치고 특정 결론을 도출했는데, 이 결론을 바탕으로 '지훈봇'이 다음 작업을 수행해야 할 때마다, '하준봇'의 결론을 매번 다시 주입해야 하는 상황이었습니다. 이는 장기적인 관점에서 에이전트 간의 효율적인 협업을 불가능하게 만들었고, 시스템의 확장성에도 큰 제약이 될 것이 분명했습니다. 에이전트들이 서로의 존재를 모르고 각자의 세상에서만 움직이는 듯한 느낌을 지울 수 없었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  독립적인 세션 환경, 그 원인을 파악하다
&lt;/h3&gt;

&lt;p&gt;문제의 본질을 파악하기 위해 Claude Code 환경의 에이전트 세션 작동 방식을 좀 더 깊이 들여다봤습니다. 일반적인 에이전트 세션은 각각 고유한 실행 컨텍스트를 가지며, 이는 격리된 환경에서 안전하고 예측 가능하게 동작하도록 설계된 결과였습니다. 즉, 한 세션에서 정의된 변수나 상태는 다른 세션에 영향을 주지 않으며, 세션이 종료되면 그 안의 모든 정보는 휘발됩니다. 이것은 보안이나 안정성 측면에서는 장점일 수 있지만, 제가 구축하려던 멀티 에이전트 협업 시스템에는 치명적인 단점으로 작용했습니다.&lt;/p&gt;

&lt;p&gt;처음에는 단순히 특정 변수를 전역적으로 선언하거나, 파일을 이용해 상태를 공유하는 등의 방법을 고민했습니다. 하지만 이 방법들은 에이전트가 동적으로 생성되거나 사라지는 환경에서는 관리하기가 매우 복잡하고, 비동기적인 통신에서 데이터 일관성을 보장하기 어렵다는 한계가 있었습니다. 특히, 에이전트 간의 '대화'라는 개념을 구현하기 위해서는 단순한 데이터 공유 이상의, 메시징 기반의 접근 방식이 필요하다는 결론에 도달했습니다. 각 에이전트가 능동적으로 메시지를 주고받으며 컨텍스트를 이어가는 것이 제가 원하던 그림이었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  크로스 세션 메시징, 해결의 실마리를 찾다
&lt;/h3&gt;

&lt;p&gt;여러 문서를 찾아보고 테스트를 거듭하던 중, Claude Code가 제공하는 '크로스 세션 메시징' 기능을 발견했습니다. 이 기능은 이름 그대로 여러 에이전트 세션 간에 비동기적으로 메시지를 주고받을 수 있도록 설계된 것이었습니다. 마치 아빠가 엄마에게, 혹은 아이에게 특정 메시지를 보내는 것처럼, 에이전트가 다른 에이전트에게 명확한 의도를 담은 메시지를 전달할 수 있게 해주는 기능이었죠. 이 기능을 통해 저는 에이전트들이 서로의 존재를 인지하고, 이전 대화 내용을 이어받아 작업 상태를 공유하며, 장기적인 대화 흐름을 유지할 수 있는 기반을 마련할 수 있겠다는 확신이 들었습니다.&lt;/p&gt;

&lt;p&gt;이 메시징 기능은 크게 두 가지 방식으로 메시지를 보낼 수 있었습니다. 하나는 특정 세션 ID를 지정하여 메시지를 보내는 방식이고, 다른 하나는 현재 활성화된 모든 세션에 메시지를 브로드캐스트하는 방식입니다. 이 두 가지 방식은 에이전트 간의 협업 모델을 설계하는 데 있어 매우 유연한 선택지를 제공해 주었습니다. 예를 들어, 특정 에이전트에게 직접 지시를 내리거나, 전체 에이전트들에게 공지사항을 전달하는 시나리오에 각각 적용할 수 있을 것 같았습니다. 단순한 정보 공유를 넘어, 에이전트의 상태를 동적으로 관리하고 제어할 수 있는 길이 열린 셈입니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  특정 에이전트에게 직접 메시지 보내기
&lt;/h3&gt;

&lt;p&gt;먼저 특정 에이전트 세션에게만 메시지를 보내는 방법을 시도했습니다. 이는 제가 '하준봇'에게 특정 작업을 지시하거나, '지훈봇'에게 '하준봇'이 처리한 결과물을 전달해야 하는 상황에 딱 맞는 기능이었습니다. 핵심은 &lt;code&gt;session_id&lt;/code&gt;를 정확히 지정하는 것이었습니다. 각 세션은 고유한 ID를 가지고 있으므로, 이 ID를 통해 타겟 세션을 식별할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;아래 코드는 특정 세션 ID를 가진 에이전트에게 메시지를 보내는 간단한 예시입니다. `` 부분에 메시지를 받을 세션의 실제 ID를 넣으면 됩니다. 이 ID는 Claude Code 환경에서 각 세션의 정보를 확인하여 얻을 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`&lt;br&gt;
await send_message_to_session(session_id="&amp;lt;TARGET_SESSION_ID&amp;gt;", message="Hello from another session!")&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;이 방식을 통해 에이전트 간의 1:1 통신이 가능해졌습니다. 예를 들어, 특정 데이터 처리 에이전트가 분석을 완료한 후, 그 결과를 보고서 작성 에이전트에게 직접 전달하여 다음 단계의 작업을 트리거할 수 있게 되는 것이죠. 단순히 데이터를 던지는 것이 아니라, '이 데이터로 이런 작업을 해달라'는 명확한 의도를 담아 메시지를 보낼 수 있다는 점이 중요했습니다. 저는 이 기능을 활용하여 순차적인 작업 흐름을 가진 에이전트 파이프라인을 구축하는 데 활용했습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  모든 에이전트에게 공지사항 브로드캐스트하기
&lt;/h3&gt;

&lt;p&gt;특정 세션에 메시지를 보내는 것 외에, 모든 활성 에이전트 세션에 일괄적으로 메시지를 보내야 하는 상황도 있었습니다. 예를 들어, 전체 시스템의 상태가 변경되었거나, 모든 에이전트가 알아야 할 전역적인 업데이트가 발생했을 때 유용할 것이라고 생각했습니다. 제가 육아를 하면서 '밥 먹자!'라고 외치면 온 가족이 듣는 것처럼 말이죠. 이럴 때는 &lt;code&gt;send_message_to_all_sessions&lt;/code&gt; 함수를 사용하면 됩니다.&lt;/p&gt;

&lt;p&gt;아래 코드는 모든 활성 세션에 메시지를 브로드캐스트하는 예시입니다. 이 메시지는 현재 Claude Code 환경에서 실행 중인 모든 에이전트 세션이 수신하게 됩니다.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`&lt;br&gt;
await send_message_to_all_sessions(message="Global update: Task completed.")&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;이 기능은 특히 분산된 환경에서 모든 에이전트에게 동시에 특정 지시를 내리거나, 전역 설정 변경 사항을 알리는 데 매우 효과적이었습니다. 예를 들어, 시스템 유지보수 시간 공지, 새로운 설정 파일 로드 명령, 또는 특정 조건이 충족되어 모든 에이전트가 작업을 중단해야 할 때 유용하게 활용할 수 있을 것입니다. 저는 이 기능을 사용하여 특정 에이전트가 중요한 이정표(milestone)에 도달했을 때, 다른 모든 에이전트가 이를 인지하고 각자의 작업을 조정할 수 있도록 시스템을 설계했습니다. 이는 에이전트 간의 느슨한 결합을 유지하면서도, 중요한 정보는 신속하게 공유할 수 있는 좋은 방법이었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  수신된 메시지 처리 로직 구현
&lt;/h3&gt;

&lt;p&gt;메시지를 보내는 것만큼 중요한 것은, 보낸 메시지를 에이전트가 어떻게 수신하고 처리하느냐였습니다. 메시지를 보냈다고 해서 에이전트가 자동으로 알아서 동작하는 것은 아니기 때문이죠. 각 에이전트는 자신에게 도착한 메시지를 처리할 수 있는 핸들러를 가지고 있어야 합니다. Claude Code에서는 &lt;code&gt;on_message_received&lt;/code&gt;와 같은 메커니즘을 통해 메시지 수신 시 특정 함수를 실행하도록 설정할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;`&lt;br&gt;
on_message_received(handler_function)&lt;br&gt;
`&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;위 코드는 메시지가 수신되었을 때 &lt;code&gt;handler_function&lt;/code&gt;이라는 함수를 호출하도록 설정하는 기본적인 형태입니다. 이 &lt;code&gt;handler_function&lt;/code&gt; 내에서 수신된 메시지의 내용을 파싱하고, 그 내용에 따라 에이전트가 특정 동작을 수행하도록 로직을 구현했습니다. 예를 들어, 메시지에 'task_complete'라는 키워드가 있으면 다음 작업을 시작하고, 'status_query'라는 키워드가 있으면 현재 상태를 회신하는 식으로 말이죠. 이 핸들러 함수는 메시지의 발신자, 내용, 타임스탬프 등 다양한 정보를 파라미터로 받을 수 있었고, 이를 활용해 더욱 정교한 상태 관리와 대화 흐름 제어가 가능했습니다. 이로써 에이전트는 단순히 메시지를 받는 것을 넘어, 능동적으로 상황을 인지하고 반응하는 주체로 거듭나게 되었습니다.&lt;/p&gt;

&lt;p&gt;.ba-pc{display:none}.ba-mo{display:block}&lt;a class="mentioned-user" href="https://dev.to/media"&gt;@media&lt;/a&gt; (min-width:768px){.ba-pc{display:block}.ba-mo{display:none}}&lt;/p&gt;

&lt;h3&gt;
  
  
  컨텍스트 유지의 결과와 활용 가능성
&lt;/h3&gt;

&lt;p&gt;크로스 세션 메시징 기능을 활용하여 두 개 이상의 Claude Code 세션을 열고, 한 세션에서 다른 세션으로 메시지를 보내는 테스트를 진행했습니다. 특정 세션 ID를 지정하여 보낸 메시지가 의도한 대로 해당 세션에만 도착하는 것을 로그를 통해 확인했습니다. 또한, 모든 세션에 브로드캐스트한 메시지는 실행 중인 모든 세션에서 성공적으로 수신되는 것을 관찰할 수 있었습니다. 메시지 내용에 따라 에이전트가 미리 정의된 동작을 수행하는 것까지 확인하며, 컨텍스트 공유 및 지속적인 대화 흐름 유지가 성공적으로 이루어지고 있음을 검증했습니다.&lt;/p&gt;

&lt;p&gt;이 경험을 통해 에이전트의 상태 관리와 장기 실행 작업의 일관성을 확보하는 데 매우 중요한 진전을 이루었다고 생각합니다. 단순한 정보 전달을 넘어, 에이전트 간의 복잡한 협업 모델을 설계할 수 있는 견고한 기반을 마련한 셈입니다. 앞으로는 이 기능을 활용하여 에이전트들이 서로의 작업 진행 상황을 실시간으로 공유하고, 필요에 따라 역할을 전환하며, 더욱 복잡하고 지능적인 협업 시스템을 구축해 나갈 계획입니다. 제 아들이 친구들과 협력해서 레고 블록으로 멋진 성을 만드는 것처럼, 저의 에이전트들도 서로 소통하며 더 큰 가치를 만들어낼 수 있을 것이라는 기대감이 큽니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  남는 이야기
&lt;/h3&gt;

&lt;p&gt;Claude Code의 크로스 세션 메시징 기능은 멀티 에이전트 시스템을 구축하는 개발자에게 매우 강력한 도구입니다. 단순히 독립적인 에이전트를 넘어, 서로 소통하고 협력하여 컨텍스트를 유지하는 지능적인 시스템을 만들 수 있는 핵심적인 방법론을 제공합니다. 이 기능을 이해하고 효과적으로 활용한다면, 에이전트 기반 솔루션의 잠재력을 최대한 끌어낼 수 있을 것입니다. 저처럼 에이전트 간 컨텍스트 유지 문제로 고민하셨던 분들께 이 경험이 작은 도움이 되기를 바랍니다.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
    </item>
    <item>
      <title>AI 숏폼 음악: 사용자 제어와 품질 검수 게이트 직접 만들며 얻은 경험</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Fri, 14 Aug 2026 00:43:20 +0000</pubDate>
      <link>https://dev.to/kys7442/ai-syospom-eumag-sayongja-jeeowa-pumjil-geomsu-geiteu-jigjeob-mandeulmyeo-eodeun-gyeongheom-1690</link>
      <guid>https://dev.to/kys7442/ai-syospom-eumag-sayongja-jeeowa-pumjil-geomsu-geiteu-jigjeob-mandeulmyeo-eodeun-gyeongheom-1690</guid>
      <description>&lt;h2&gt;
  
  
  AI 숏폼 음악: 사용자 제어와 품질 검수 게이트 직접 만들며 얻은 경험
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ww4r0tnrxgflkvutkox.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ww4r0tnrxgflkvutkox.png" alt="AI 숏폼 음악: 사용자 제어와 품질 검수 게이트 직접 만들며 얻은 경험" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;안녕하세요, 코딩아빠입니다. 또 마감 직전에 부랴부랴 글을 쓰고 있네요. 이번 글은 제가 AI 기반 숏폼 음악 콘텐츠 제작 시스템을 만들면서 직접 부딪히고 해결했던 경험노트입니다. 처음에는 AI가 알아서 뚝딱 만들어주니 편하겠다 싶었는데, 막상 실제 사용자분들이 써보니 뭔가 2% 부족하다는 피드백이 많더군요. 특히 가사 분량이나 전달력 같은 세밀한 부분을 직접 조절하고 싶어 하셨고, 결과물에 대한 검수 과정도 필요하다는 것을 뼈저리게 느꼈습니다. 결국, AI의 자동화된 편리함에 사용자 제어 권한과 품질 관리 프로세스를 더하는 것이 얼마나 중요한지 다시 한번 깨닫게 된 프로젝트였습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  이 글에서 짚는 것
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;AI 기반 콘텐츠 생성 시스템에서 사용자 제어 옵션의 중요성&lt;/li&gt;
&lt;li&gt;가사 배치 효율성을 높이는 UI/UX 설계 방법&lt;/li&gt;
&lt;li&gt;실시간 비용 예측 및 표시 기능 구현 경험&lt;/li&gt;
&lt;li&gt;AI 생성 콘텐츠의 품질을 관리하는 검수 게이트 구축 노하우&lt;/li&gt;
&lt;li&gt;개발 초기 단계부터 품질 관리 프로세스를 고려해야 하는 이유&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AI 콘텐츠, 사용자에게 선택권을 주지 않아 생기는 문제들
&lt;/h3&gt;

&lt;p&gt;처음 AI 기반 숏폼 음악 콘텐츠 제작 시스템을 만들었을 때는 그저 AI가 알아서 뚝딱 만들어주는 데 집중했습니다. 사용자들은 원하는 키워드만 넣으면 멋진 음악과 가사가 뿅 하고 나오니 신기해하셨죠. 그런데 시간이 지나면서 '이 부분은 좀 더 길었으면 좋겠어요', '가사가 이렇게 전달되는 게 맞나요?' 같은 질문들이 들어오기 시작했습니다. 가장 큰 문제는 가사 배치였는데, AI가 임의로 가사를 배치하다 보니 사용자가 원하는 후렴구나 특정 간주 부분에 가사가 들어가지 않는 경우가 많았습니다. 결과적으로는 괜찮은 콘텐츠도 사용자의 의도와 달라 버려지는 일이 잦아지더군요. 사용자가 원하는 결과물을 얻기 위해 여러 번 재시도해야 하는 비효율성도 큰 숙제였습니다.&lt;/p&gt;

&lt;p&gt;이런 식으로 계속되면 결국 사용자 만족도가 떨어지고 시스템을 찾는 발길도 줄어들 수밖에 없겠다고 생각했습니다. AI가 아무리 뛰어나도 최종 결과물에 대한 사용자의 기대치를 충족시키지 못하면 의미가 없다는 것을 깨닫는 순간이었죠. 단순히 콘텐츠를 생성하는 것을 넘어, 사용자가 자기 의도대로 결과물을 조절할 수 있는 섬세한 제어 권한을 주는 것이 필수적이라는 결론에 도달했습니다.&lt;/p&gt;

&lt;p&gt;초기 시스템은 이런 사용자 경험(UX) 측면의 디테일을 깊이 고려하지 못했습니다. 그저 기능 구현에 급급했던 거죠. 하지만 AI 콘텐츠의 생명력은 결국 사용자가 얼마나 만족하고 활용하느냐에 달려 있다는 걸 뒤늦게나마 알게 되었네요. 이 문제를 해결하기 위해 UI/UX 전반을 다시 들여다보고 사용자가 콘텐츠 생성 과정에 더 깊이 개입할 수 있는 방법을 찾아야 했습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  가사 분량, 전달력 옵션으로 세밀한 제어 권한 부여하기
&lt;/h3&gt;

&lt;p&gt;사용자들이 가장 답답해했던 부분 중 하나가 가사의 분량이나 전달 방식을 AI에 전적으로 맡겨야 했던 점이었습니다. 그래서 저는 가사의 길이를 조절하거나, 가사 전달의 강도(예를 들어, 좀 더 차분하게 부를지, 아니면 힘 있게 부를지)를 선택할 수 있는 옵션을 추가하기로 했습니다. 이와 함께 배경 음악이나 효과 등에 따라 달라지는 '배경 비용'을 실시간으로 표시하는 기능도 함께 넣었죠. 사용자가 자신의 예산 범위 내에서 원하는 품질과 스타일을 선택할 수 있도록 돕는 장치였습니다.&lt;/p&gt;

&lt;p&gt;이런 옵션들은 단순히 기능을 추가하는 것을 넘어, 사용자가 AI와의 '협업'을 체감하게 만드는 중요한 요소였습니다. 마치 작곡가나 프로듀서가 세션에게 디렉션을 주듯, AI에게도 구체적인 지시를 내릴 수 있게 된 셈이죠. 물론 모든 것을 다 조절할 수는 없었지만, 핵심적인 몇 가지 선택권을 줌으로써 사용자는 훨씬 더 '내 것'이라는 느낌을 받게 되었습니다.&lt;/p&gt;

&lt;p&gt;이 기능들을 구현하면서 가장 신경 쓴 부분은 UI의 직관성이었습니다. 복잡한 옵션들을 덕지덕지 붙이는 대신, 사용자가 한눈에 이해하고 쉽게 선택할 수 있도록 디자인하는 데 공을 들였습니다. 예를 들어, 가사 분량은 '짧게', '중간', '길게'처럼 명확한 카테고리로 나누고, 전달력 또한 몇 가지 프리셋을 제공해서 선택의 피로도를 낮추려고 노력했습니다. 이렇게 함으로써 사용자는 자신의 의도를 AI에 효과적으로 전달할 수 있게 되었고, 결과물의 만족도도 덩달아 올라가는 것을 확인할 수 있었네요.&lt;/p&gt;

&lt;p&gt;.ba-pc{display:none}.ba-mo{display:block}&lt;a class="mentioned-user" href="https://dev.to/media"&gt;@media&lt;/a&gt; (min-width:768px){.ba-pc{display:block}.ba-mo{display:none}}&lt;/p&gt;

&lt;h3&gt;
  
  
  후렴/간주 선택 UI로 가사 배치 효율성 개선하기
&lt;/h3&gt;

&lt;p&gt;가장 시급했던 가사 배치 문제를 해결하기 위해, 저는 'STEP 1 가사 배치 옵션'에 후렴과 간주를 직접 선택할 수 있는 UI를 구현했습니다. AI가 음악을 생성할 때, 특정 구간에 가사가 들어가거나 비워지기를 원하는 사용자의 니즈가 분명했거든요. 예를 들어, '후렴 부분은 무조건 이 가사로 시작해야 해!' 라거나, '이 간주 부분에는 가사가 아예 없었으면 좋겠어' 같은 요구사항이 많았습니다.&lt;/p&gt;

&lt;p&gt;이를 위해 라디오 버튼 형태로 간단한 선택지를 제공했습니다. 사용자가 원하는 구간을 선택하면, AI는 해당 구간에 가사를 배치하거나 비우는 방식으로 동작하도록 시스템을 설계했습니다. 코드는 대략 이런 식이었죠.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;input type="radio" name="lyrics_section" value="chorus"&amp;gt; 후렴
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이어서 간주를 선택하는 옵션도 비슷한 방식으로 추가했습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;input type="radio" name="lyrics_section" value="interlude"&amp;gt; 간주
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이 작은 변화가 사용자 경험에 미치는 영향은 예상보다 훨씬 컸습니다. 사용자는 더 이상 AI가 랜덤으로 가사를 배치하는 것에 대해 불안해할 필요가 없게 되었고, 원하는 결과물을 얻기 위해 여러 번 시도할 필요 없이 한두 번의 시도로 만족스러운 결과를 얻을 수 있게 되더군요. 덕분에 콘텐츠 생성 과정의 비효율성이 크게 줄어들었고, 사용자들의 불만도 눈에 띄게 감소했습니다. 특정 구간에 대한 제어 권한은 AI 콘텐츠 제작에 있어 생각보다 훨씬 중요한 요소라는 것을 다시 한번 체감했습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  투명한 비용 예측: 실시간 배경 비용 업데이트
&lt;/h3&gt;

&lt;p&gt;AI 콘텐츠를 생성하는 과정에서 사용자에게 중요한 정보 중 하나는 바로 '비용'입니다. 특히 배경 음악이나 특정 효과를 추가할 때마다 비용이 달라지는 구조였기 때문에, 사용자가 옵션을 변경할 때마다 실시간으로 예상 비용을 보여주는 기능은 필수적이었습니다. 이번에도 깜빡할 뻔했지만, 사용자 입장에서 '나중에 청구서를 받아보니 생각보다 비싸네?' 하는 불만이 나오기 전에 미리 알려주는 것이 중요하다고 판단했죠.&lt;/p&gt;

&lt;p&gt;그래서 사용자가 배경 옵션을 선택하거나 변경할 때마다 즉시 반영되는 비용 표시 기능을 구현했습니다. 이는 단순히 숫자를 보여주는 것을 넘어, 사용자가 자신의 선택이 어떤 금전적 영향을 미치는지 직관적으로 이해할 수 있도록 돕는 역할을 합니다. 내부적으로는 선택된 배경 옵션 ID를 기반으로 미리 정의된 비용 테이블을 조회하고, 이를 화면에 업데이트하는 방식으로 동작했습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;update_cost_display(background_option_id);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이 함수는 사용자가 특정 배경 옵션을 클릭할 때 호출되어, 해당 옵션에 맞는 비용 정보를 가져와 UI에 표시해줍니다. 처음에는 이 기능을 단순한 안내 정도로 생각했지만, 실제 구현해보니 사용자의 신뢰도를 높이고, 예산 계획에 도움을 주는 중요한 UI 요소가 되더군요. 투명한 정보 제공은 사용자 경험뿐만 아니라 서비스에 대한 신뢰도를 구축하는 데도 큰 영향을 미친다는 것을 다시 한번 깨달았습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  최종 품질 관리의 핵심, 곡 검수 게이트 구축
&lt;/h3&gt;

&lt;p&gt;사용자에게 아무리 많은 제어 권한을 주더라도, AI가 생성한 콘텐츠의 최종 품질을 관리하는 것은 또 다른 중요한 과제였습니다. AI가 때로는 예측 불가능한 결과를 내놓거나, 의도치 않게 부적절한 콘텐츠를 생성할 수도 있기 때문이죠. 그래서 저는 최종적으로 '곡 검수 게이트'를 구축하여, 생성된 모든 콘텐츠가 일정 수준 이상의 품질과 서비스 정책을 준수하는지 확인하는 파이프라인을 만들었습니다.&lt;/p&gt;

&lt;p&gt;이 검수 게이트는 단순히 문제가 있는 콘텐츠를 걸러내는 것을 넘어, 시스템 전반의 안정성과 신뢰도를 높이는 역할을 합니다. 예를 들어, 특정 키워드에 대한 AI의 반응이 이상하거나 반복적으로 특정 유형의 오류가 발생한다면, 검수 게이트에서 해당 문제를 빠르게 감지하고 AI 모델 개선을 위한 피드백으로 활용할 수 있습니다. 처음에는 'AI가 다 할 텐데 굳이 사람이 또 봐야 하나?' 하는 생각도 들었지만, 장기적인 관점에서 보면 이 게이트는 필수적이더군요.&lt;/p&gt;

&lt;p&gt;실제로 검수 게이트를 통해 부적절한 가사나 저작권에 문제가 될 수 있는 요소를 포함한 곡들이 걸러지는 것을 테스트하면서, 이 단계의 중요성을 다시 한번 확인했습니다. 이 과정을 통해 콘텐츠의 품질을 유지하고, 사용자들에게 신뢰할 수 있는 결과물만을 제공할 수 있게 되었죠. AI 기반 서비스를 운영한다면, 생성된 콘텐츠에 대한 품질 관리 프로세스를 초기 단계부터 내재화하는 것이 정말 중요하다는 것을 이번 프로젝트를 통해 확실히 배우게 되었습니다. 검수 게이트는 단순한 체크리스트가 아니라, 서비스의 지속 가능성을 담보하는 중요한 안전장치였습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  마치며
&lt;/h3&gt;

&lt;p&gt;AI 기반 콘텐츠 제작 시스템을 만들면서, 초기에는 기술적인 구현에만 매달렸던 저 자신을 반성하게 되는 경험이었습니다. AI의 능력이 아무리 뛰어나도, 결국 그 결과물을 사용하는 '사람'의 관점에서 세심한 제어 권한과 투명한 정보, 그리고 신뢰할 수 있는 품질 관리가 뒷받침되어야 한다는 것을 깨달았네요. 특히 가사 배치 옵션이나 실시간 비용 표시, 그리고 최종 검수 게이트는 사용자 만족도를 높이고 시스템의 안정성을 확보하는 데 결정적인 역할을 했습니다. 이번 경험을 통해 AI 기술만큼이나 사용자 경험과 품질 관리 프로세스 설계가 중요하다는 것을 다시 한번 마음에 새겨봅니다. 이 글이 AI 기반 콘텐츠 파이프라인을 구축하시는 다른 개발자분들께 작은 도움이 되었으면 좋겠습니다.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>uiux</category>
      <category>1</category>
    </item>
    <item>
      <title>Flutter iOS 출시 빌드: 프로비저닝 프로파일 업데이트 실패 해결기</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Sun, 09 Aug 2026 20:48:02 +0000</pubDate>
      <link>https://dev.to/kys7442/flutter-ios-culsi-bildeu-peurobijeoning-peuropail-eobdeiteu-silpae-haegyeolgi-3ofh</link>
      <guid>https://dev.to/kys7442/flutter-ios-culsi-bildeu-peurobijeoning-peuropail-eobdeiteu-silpae-haegyeolgi-3ofh</guid>
      <description>&lt;h2&gt;
  
  
  Flutter iOS 출시 빌드: 프로비저닝 프로파일 업데이트 실패 해결기
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7qbzlgprjtmbntbjcsqv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7qbzlgprjtmbntbjcsqv.png" alt="Flutter iOS 출시 빌드: 프로비저닝 프로파일 업데이트 실패 해결기" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;안녕하세요, 코딩아빠입니다. 오랜만에 주말 짬을 내어 지난주 Flutter iOS 앱 출시 빌드 과정에서 겪었던 한 가지 경험을 메모처럼 정리해 봅니다. 이 글은 제가 실제로 부딪히고 해결한 기록으로, 혹시 비슷한 문제를 겪는 분들에게 작은 실마리가 되기를 바랍니다. Flutter로 iOS 앱을 배포하려던 스크립트가 매번 프로비저닝 프로파일 업데이트 문제로 실패하는 상황이었는데, 결론부터 말씀드리면 &lt;code&gt;xcodebuild&lt;/code&gt; 명령에 특정 옵션 하나를 추가해서 해결할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;이런 분께 — Flutter를 이용해 iOS 앱을 개발하고 배포하는 과정에서 서명 및 프로비저닝 관련 빌드 문제로 어려움을 겪는 개발자 · 난이도는 중급 정도&lt;/p&gt;

&lt;h3&gt;
  
  
  출시 빌드 스크립트가 멈춰선 순간
&lt;/h3&gt;

&lt;p&gt;며칠 전, 힘들게 개발한 Flutter iOS 앱을 앱스토어에 배포하기 위해 출시 빌드 스크립트를 실행했습니다. 그런데 평소 잘 되던 빌드가 갑자기 멈춰 서는 겁니다. 터미널에는 프로비저닝 프로파일 업데이트 관련 오류 메시지가 계속해서 쏟아져 나왔습니다. 이전에 잘 되던 것이 갑자기 안 되니 당황스럽더군요. 처음에는 혹시 개발자 계정 문제인가, 아니면 Xcode 설정이 어딘가 꼬였나 하는 막연한 생각만 들었습니다. 급한 마음에 여러 설정을 뒤적여봐도 딱히 문제점을 찾을 수 없었습니다. 가족들 잠든 늦은 밤에 겨우 시간 내서 작업하는데, 이런 예상치 못한 문제에 부딪히면 정말 힘이 빠집니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  막연했던 첫 삽질과 로그 분석
&lt;/h3&gt;

&lt;p&gt;처음에는 Xcode GUI에서 수동으로 프로비저닝 프로파일을 업데이트해보거나, 프로젝트 설정을 다시 확인하는 등 여러 시도를 해봤습니다. 하지만 스크립트로 빌드를 돌릴 때마다 같은 오류가 반복되었죠. 문제가 발생하면 역시 로그를 꼼꼼히 봐야 한다는 걸 다시 한번 느꼈습니다. 수많은 빌드 로그 중에서 '프로비저닝 프로파일 업데이트 실패'와 관련된 구체적인 메시지들을 집중적으로 살펴보았습니다. 대부분 'Xcode가 프로비저닝 프로파일을 자동으로 업데이트할 권한이 없다'는 뉘앙스의 내용이더군요. 이 부분을 보면서 &lt;code&gt;xcodebuild&lt;/code&gt; 명령 자체에 어떤 옵션이 필요하지 않을까 하는 가설을 세워봤습니다.&lt;/p&gt;

&lt;p&gt;.ba-pc{display:none}.ba-mo{display:block}&lt;a class="mentioned-user" href="https://dev.to/media"&gt;@media&lt;/a&gt; (min-width:768px){.ba-pc{display:block}.ba-mo{display:none}}&lt;/p&gt;

&lt;h3&gt;
  
  
  원인은 Xcode 빌드 시스템의 '묵묵부답'
&lt;/h3&gt;

&lt;p&gt;로그를 자세히 들여다보니, Xcode 빌드 시스템이 자동으로 프로비저닝 프로파일을 업데이트하려고 시도하지만, 그 권한이 명시적으로 주어지지 않아 실패하고 있다는 것을 알게 되었습니다. 보통 Xcode GUI 환경에서는 개발자 계정이 로그인되어 있으면 이런 과정이 자동으로 처리되는 경우가 많습니다. 하지만 CI/CD 환경이나 스크립트를 통해 빌드를 진행할 때는 GUI의 '자동' 기능이 제대로 작동하지 않을 수 있습니다. 특히 빌드 시스템이 특정 프로파일을 업데이트하거나 새로 생성해야 할 때, 이러한 명시적인 허용 없이는 작업을 진행하지 못하도록 되어 있는 듯했습니다. 이 부분이 제가 놓치고 있던 핵심이었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  명시적 허용, -allowProvisioningUpdates
&lt;/h3&gt;

&lt;p&gt;해결책은 의외로 간단했습니다. &lt;code&gt;xcodebuild&lt;/code&gt; 명령에 &lt;code&gt;-allowProvisioningUpdates&lt;/code&gt; 옵션을 추가하는 것이었습니다. 이 옵션은 Xcode 빌드 시스템이 프로비저닝 프로파일을 자동으로 업데이트하거나 새로 생성하는 것을 명시적으로 허용해 줍니다. 특히 CI/CD 파이프라인이나 스크립트 기반 빌드 환경에서 이러한 자동화된 처리가 필요할 때 유용하게 사용될 수 있습니다. 이 옵션을 추가함으로써 Xcode가 필요한 프로파일을 개발자 계정 정보를 바탕으로 직접 업데이트할 수 있게 되는 것이죠. 아래는 제가 사용한 명령어입니다:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;xcodebuild -workspace Runner.xcworkspace -scheme Runner -configuration Release -destination 'generic/platform=iOS' archive -allowProvisioningUpdates -archivePath build/Runner.xcarchive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이 명령어는 Flutter 프로젝트의 &lt;code&gt;Runner.xcworkspace&lt;/code&gt;를 아카이브하면서, 프로비저닝 프로파일 업데이트를 허용하도록 지시하는 역할을 합니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  옵션 추가 후 성공적인 빌드와 배움
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;xcodebuild&lt;/code&gt; 명령에 &lt;code&gt;-allowProvisioningUpdates&lt;/code&gt; 옵션을 추가한 후, 다시 iOS 출시 빌드 스크립트를 실행했습니다. 예상대로 더 이상 프로비저닝 프로파일 업데이트 관련 오류 메시지는 나타나지 않았고, 빌드는 성공적으로 완료되었습니다. 아카이브 생성 및 앱스토어 배포까지 순조롭게 진행되는 것을 보고 안도했습니다. 이번 경험을 통해 iOS 앱 배포 과정에서 발생하는 빌드 문제는 종종 Xcode의 서명 및 프로비저닝 설정과 관련이 깊다는 것을 다시 한번 깨달았습니다. 문제 발생 시 빌드 로그를 면밀히 검토하고, &lt;code&gt;xcodebuild&lt;/code&gt; 명령의 다양한 옵션을 확인하여 해결책을 찾는 습관이 중요함을 느꼈네요.&lt;/p&gt;

&lt;h3&gt;
  
  
  이 글에서 다루는 것
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Flutter iOS 출시 빌드 시 발생할 수 있는 프로비저닝 프로파일 업데이트 오류의 원인&lt;/li&gt;
&lt;li&gt;xcodebuild 명령의 &lt;code&gt;-allowProvisioningUpdates&lt;/code&gt; 옵션의 역할과 사용 방법&lt;/li&gt;
&lt;li&gt;iOS 앱 배포 과정에서 빌드 로그를 분석하는 중요성&lt;/li&gt;
&lt;li&gt;자동 프로비저닝 업데이트가 필요한 상황에 대한 이해&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  끝으로
&lt;/h3&gt;

&lt;p&gt;결국 이번 문제는 &lt;code&gt;xcodebuild&lt;/code&gt; 명령에 작은 옵션 하나를 추가하는 것으로 해결되었습니다. iOS 앱 빌드와 배포 과정은 때때로 복잡하고 예측 불가능한 오류를 던져주지만, 로그를 꼼꼼히 살피고 관련 문서를 찾아보는 기본적인 원칙이 문제를 해결하는 가장 빠른 길임을 다시 한번 확인했습니다. 다음에 같은 문제가 발생하면 당황하지 않고 이 옵션을 먼저 확인하게 될 것 같습니다.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>ios</category>
      <category>xcodebuild</category>
      <category>allowprovisioningupdates</category>
    </item>
    <item>
      <title>Gemini와 Claude, 언제 누구를 써야 할까? LLM 동적 라우팅 전략 경험기</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Thu, 06 Aug 2026 20:36:43 +0000</pubDate>
      <link>https://dev.to/kys7442/geminiwa-claude-eonje-nugureul-sseoya-halgga-llm-dongjeog-rauting-jeonryag-gyeongheomgi-3fhn</link>
      <guid>https://dev.to/kys7442/geminiwa-claude-eonje-nugureul-sseoya-halgga-llm-dongjeog-rauting-jeonryag-gyeongheomgi-3fhn</guid>
      <description>&lt;h2&gt;
  
  
  Gemini와 Claude, 언제 누구를 써야 할까? LLM 동적 라우팅 전략 경험기
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0a1lw1zpunvdswwirlu7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0a1lw1zpunvdswwirlu7.png" alt="Gemini와 Claude, 언제 누구를 써야 할까? LLM 동적 라우팅 전략 경험기" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;안녕하세요, 코딩아빠입니다. 오늘도 아들 재워놓고 제가 실제로 부딪히고 해결했던 개발 경험을 정리한 노트를 공유해 드립니다. 여러 LLM 모델을 서비스에 연동해 사용해 보신 분들이라면 아마 저와 비슷한 고민을 해보셨을 것 같습니다. 처음에는 하나의 모델로 모든 작업을 처리하거나, 필요할 때마다 수동으로 모델을 변경하는 방식으로 운영했었죠. 하지만 Gemini나 Claude 같은 다양한 LLM 모델이 등장하면서, 각자의 강점과 약점, 그리고 무엇보다 비용 구조가 제각각이라는 점이 서비스 운영에 큰 변수로 작용했습니다. 특정 작업에 부적합한 모델을 사용하면 불필요한 비용이 발생하거나, 기대했던 응답 품질을 얻기 어려워지는 문제가 발생하더군요.&lt;/p&gt;

&lt;h3&gt;
  
  
  이 글에서 짚는 것
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;다수의 LLM 모델을 통합할 때 발생하는 문제점과 비효율성을 이해합니다.&lt;/li&gt;
&lt;li&gt;각 LLM 모델의 특성과 비용 구조를 파악하는 중요성을 깨닫습니다.&lt;/li&gt;
&lt;li&gt;작업 유형에 따라 최적의 LLM을 동적으로 선택하는 라우팅 전략을 수립하는 방법을 배웁니다.&lt;/li&gt;
&lt;li&gt;파이썬 예시 코드를 통해 LLM 동적 라우팅 로직 구현의 기본을 익힙니다.&lt;/li&gt;
&lt;li&gt;구현된 라우팅 전략의 효과를 검증하고 지속적으로 개선하는 접근 방식을 알아봅니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  여러 LLM 모델을 한 서비스에서 활용할 때의 딜레마
&lt;/h3&gt;

&lt;p&gt;초기에는 서비스에서 필요한 LLM 기능을 구현할 때, 하나의 모델을 주로 사용하거나 필요에 따라 수동으로 변경하는 방식을 택했습니다. 예를 들어, 처음에는 주로 Gemini Pro 모델을 사용하다가, 특정 작업에서 Claude가 더 좋은 성능을 보인다는 소식을 들으면 해당 기능만 Claude로 변경하는 식이었죠. 문제는 이런 방식이 확장성과 효율성 면에서 한계를 보인다는 점이었습니다. 모든 작업에 최적화된 '만능 LLM'은 사실상 존재하지 않기 때문에, 어떤 모델은 짧은 질문 응답에 빠르고 저렴하지만 긴 문서 요약에는 비효율적일 수 있고, 또 다른 모델은 창의적인 글쓰기에는 탁월하지만 코딩 보조에는 아쉬운 성능을 보일 수 있었습니다. 결국, 매번 수동으로 모델을 선택하는 것은 개발 및 운영 비용을 증가시키는 요인이 되었고, 잘못된 선택은 서비스의 응답 품질 저하로 이어지기도 했습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  각 LLM의 고유한 강점과 비용 구조 파악의 중요성
&lt;/h3&gt;

&lt;p&gt;LLM 동적 라우팅 전략의 핵심은 각 모델의 특성과 비용 구조를 정확히 이해하는 데 있습니다. 단순히 '더 좋은' 모델이나 '더 싼' 모델을 찾는 것이 아니라, 특정 작업의 요구사항과 모델의 강점을 일치시키는 것이 중요하다고 판단했습니다. 예를 들어, Gemini는 복잡한 추론이나 코딩 관련 작업에 강점을 보이며 빠른 응답 속도를 자랑합니다. 반면 Claude는 긴 컨텍스트 처리 능력과 섬세한 대화에 유리한 특성을 가지고 있죠. 이처럼 모델별 강점 외에도, 각 모델의 토큰당 비용도 천차만별입니다. 저렴한 모델은 대량의 단순 작업에 적합하고, 고성능 모델은 비용에 민감하지 않은 핵심 기능에 활용하는 것이 바람직합니다. 저희 팀은 아래와 같이 각 모델의 대략적인 비용과 품질 점수를 내부적으로 정의하여, 어떤 모델이 어떤 작업에 적합할지 판단하는 기준으로 삼았습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MODEL_CONFIG = {
    'claude-3-opus': {'cost_per_token': 0.000015, 'quality_score': 9.5},
    'claude-3-sonnet': {'cost_per_token': 0.000003, 'quality_score': 8.0},
    'gemini-1.5-pro': {'cost_per_token': 0.0000035, 'quality_score': 9.0},
    'gemini-1.5-flash': {'cost_per_token': 0.00000035, 'quality_score': 7.5}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이 설정은 모델 선택의 근거가 되었고, 나중에 라우팅 로직을 만들 때 중요한 참고 자료가 되었습니다.&lt;/p&gt;

&lt;p&gt;.ba-pc{display:none}.ba-mo{display:block}&lt;a class="mentioned-user" href="https://dev.to/media"&gt;@media&lt;/a&gt; (min-width:768px){.ba-pc{display:block}.ba-mo{display:none}}&lt;/p&gt;

&lt;h3&gt;
  
  
  작업 특성에 따른 LLM 동적 라우팅 전략 수립
&lt;/h3&gt;

&lt;p&gt;각 LLM 모델의 특성을 파악한 후, 저희는 작업의 특성이나 서비스의 메뉴 유형에 따라 사용할 LLM 모델을 동적으로 선택하는 라우팅 전략을 수립하기로 결정했습니다. 이는 마치 택배 회사가 물건의 크기나 배송 거리에 따라 적합한 차량을 배정하는 것과 유사합니다. 비용에 민감하지 않고 고품질의 창의적인 응답이 필요한 작업에는 최상위 모델을, 대량 처리와 비용 절감이 중요한 작업에는 가성비 모델을 사용하도록 규칙을 정의한 것이죠. 이 전략의 핵심 목표는 두 가지였습니다. 첫째, 각 LLM의 강점을 최대한 활용하여 서비스의 전반적인 품질을 높이는 것. 둘째, 불필요한 비용 지출을 최소화하여 운영 효율성을 극대화하는 것이었습니다. 우리는 단순히 고정된 모델을 사용하는 것보다, 이런 동적인 접근 방식이 장기적으로 훨씬 유리할 것이라고 판단했네요.&lt;/p&gt;

&lt;h3&gt;
  
  
  파이썬으로 구현한 LLM 라우팅 로직
&lt;/h3&gt;

&lt;p&gt;실제로 이 라우팅 전략을 백엔드 애플리케이션에 적용하기 위해 간단한 파이썬 함수를 구현했습니다. 이 함수는 들어오는 요청의 &lt;code&gt;task_type&lt;/code&gt;과 &lt;code&gt;user_query&lt;/code&gt; 길이를 기반으로 가장 적합한 LLM 모델을 선택하도록 설계되었습니다. 예를 들어, '창의적인 글쓰기'와 같은 고품질이 요구되는 작업에는 Claude-3 Opus를, 길이가 긴 '요약' 작업에는 긴 컨텍스트 처리에 유리한 Claude-3 Sonnet을, 그리고 '코딩 도움'과 같이 특정 도메인 지식이 요구되는 작업에는 Gemini-1.5 Flash를 할당하는 식입니다. 나머지 일반적인 프롬프트는 Gemini-1.5 Pro를 기본으로 사용하도록 했습니다. 아래는 그 구현 예시입니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def route_llm(task_type: str, user_query: str):
    if task_type == 'creative_writing':
        return 'claude-3-opus'
    elif task_type == 'summarization' and len(user_query) &amp;gt; 5000:
        return 'claude-3-sonnet'
    elif task_type == 'coding_help':
        return 'gemini-1.5-flash'
    else:
        return 'gemini-1.5-pro'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이 코드는 실제 서비스 환경에서 사용될 때는 훨씬 더 복잡한 조건과 예외 처리가 추가되겠지만, 핵심 로직은 이와 크게 다르지 않습니다. 작업 유형과 쿼리 특성에 따라 모델을 분기하는 것이 핵심이죠.&lt;/p&gt;

&lt;h3&gt;
  
  
  동적 라우팅 전략의 효과 검증 및 지속적인 개선
&lt;/h3&gt;

&lt;p&gt;라우팅 로직을 구현한 후에는 이 전략이 의도대로 작동하는지 검증하는 과정이 필수적이었습니다. 저희는 LLM API 호출 로그를 면밀히 모니터링하며, 특정 작업 유형에 대해 의도된 모델이 정확히 호출되는지 확인했습니다. 예를 들어, 'creative_writing' 요청이 들어왔을 때 실제로 'claude-3-opus'가 호출되는지, 5000자 이상의 긴 요약 요청에 'claude-3-sonnet'이 사용되는지 등을 지속적으로 확인한 것이죠. 단순히 호출 모델만 확인하는 것을 넘어, 각 모델의 응답 품질과 실제 처리 비용을 측정하여 라우팅 전략이 목표한 비용 효율성 및 품질 기준을 충족하는지 주기적으로 평가했습니다. 이 과정에서 예상치 못한 문제가 발견되면 라우팅 규칙을 수정하거나, 때로는 새로운 LLM 모델을 추가하는 등 전략을 유연하게 개선해 나갔습니다. 이처럼 동적 라우팅은 한 번 구현으로 끝나는 것이 아니라, LLM 시장의 변화와 서비스 요구사항에 맞춰 계속해서 진화해야 하는 부분이더군요.&lt;/p&gt;

&lt;h3&gt;
  
  
  더 나은 LLM 라우팅을 위한 고민들
&lt;/h3&gt;

&lt;p&gt;저희가 현재 구현한 동적 라우팅 전략은 비교적 단순한 규칙 기반입니다. 하지만 실제 운영 환경에서는 더 복잡하고 정교한 전략이 필요할 수 있다는 것을 깨달았습니다. 예를 들어, A/B 테스트를 통해 여러 라우팅 규칙의 성능을 비교하거나, 실시간으로 각 LLM의 응답 시간이나 성공률을 모니터링하여 문제가 발생한 모델은 자동으로 라우팅 대상에서 제외하는 기능도 고려해 볼 수 있습니다. 또한, 사용자 피드백을 수집하여 특정 작업에서 어떤 모델이 더 만족스러운 응답을 주는지 데이터를 기반으로 라우팅 가중치를 조절하는 방법도 있겠네요. 단순히 비용 효율성이나 품질 향상을 넘어, 서비스의 안정성과 사용자 경험 전반을 고려하는 방향으로 라우팅 전략을 고도화하는 것이 앞으로의 과제라고 생각합니다. 이처럼 LLM 동적 라우팅은 단순히 코드를 구현하는 것을 넘어, 서비스의 성장과 함께 끊임없이 고민하고 발전시켜야 할 중요한 영역인 것 같습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  남는 이야기
&lt;/h3&gt;

&lt;p&gt;다양한 LLM 모델을 활용하는 서비스에서 동적 라우팅 전략은 비용 효율성과 응답 품질을 동시에 잡을 수 있는 효과적인 방법이라는 것을 직접 경험했습니다. 각 모델의 특성과 비용을 면밀히 분석하고, 작업의 중요도에 맞춰 최적의 모델을 배정하는 지혜가 필요하다는 것을 다시 한번 깨달았네요. 이 글이 LLM 기반 서비스를 개발하고 운영하시는 다른 분들께 작은 도움이 되었으면 합니다. 다음에 또 다른 경험으로 찾아뵙겠습니다.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>gemini</category>
      <category>claude</category>
      <category>llmapi</category>
    </item>
    <item>
      <title>Petit Nube Silicone Teether: Bear &amp; Crab Wrist Teether Review</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Mon, 03 Aug 2026 20:49:35 +0000</pubDate>
      <link>https://dev.to/kys7442/petit-nube-silicone-teether-bear-crab-wrist-teether-review-2037</link>
      <guid>https://dev.to/kys7442/petit-nube-silicone-teether-bear-crab-wrist-teether-review-2037</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzsejgvcslqpxri43gjnl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzsejgvcslqpxri43gjnl.png" alt="1+1 (한정수량 300개) 국내생산 쁘띠누베 실리콘 곰 꽃게 손목 치발기 아기치발기 동물치발기, 단품, 베이비코랄, 2개" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These days, when I look at young parents' social media, there are so many&lt;/p&gt;

&lt;p&gt;baby products that look great, haha.&lt;/p&gt;

&lt;p&gt;But when I looked into them a bit later, some things appeared different based on my specific needs. Especially as my baby started sucking their hands frequently, I began looking for teethers and came across the 'Petit Nube Silicone Bear Crab Wrist Teether'. I've organized information around a few criteria to determine if it's the right teether for my baby.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrist-type design: When and for which babies is it good?
&lt;/h3&gt;

&lt;p&gt;The Petit Nube silicone teether has a wrist-worn design... If your baby's grip is still weak or they frequently drop objects, a wrist-type teether can be a good choice. The advantage is that the baby can wear it on their wrist and play freely, so parents don't have to constantly hold it for them. If your baby is very active or curious, they might enjoy playing with it on their own. However, if your baby is already accustomed to grasping objects or prefers various types of stimulation, it's good to consider other teether shapes as well. Wrist-type teethers can only stimulate specific areas, so combining them with other teethers for overall oral development is also an option.&lt;/p&gt;

&lt;h3&gt;
  
  
  Domestically produced food-grade silicone: Can we use it with peace of mind?
&lt;/h3&gt;

&lt;p&gt;Since this product goes directly into the baby's mouth, material safety is one of the most important criteria when choosing a teether. The Petit Nube teether is said to use domestically produced food-grade silicone.&lt;/p&gt;

&lt;p&gt;This seems to be a crucial factor that allows parents to give it to their baby with confidence, as food-grade silicone is made to be free from harmful substances. Another big advantage is that it can be sterilized with boiling water or steam, making hygiene management convenient. Since it's a product that babies chew and suck on daily, the ease of sterilization can be a great help for busy parents. If you are particularly concerned about environmental hormones or other harmful substances, it's advisable to carefully check these safety specifications.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzsejgvcslqpxri43gjnl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzsejgvcslqpxri43gjnl.png" alt="1+1 (한정수량 300개) 국내생산 쁘띠누베 실리콘 곰 꽃게 손목 치발기 아기치발기 동물치발기, 단품, 베이비코랄, 2개" width="800" height="800"&gt;&lt;/a&gt;Actual product image&lt;/p&gt;

&lt;h3&gt;
  
  
  Bear and crab shapes with various textures: Are they effective in engaging babies?
&lt;/h3&gt;

&lt;p&gt;This teether is designed in bear and crab shapes, which can visually engage babies. Cute animal shapes stimulate a baby's curiosity and can make playtime with the teether more enjoyable. The soft material and various sized nubs provide gentle stimulation to the baby's gums, helping to relieve gum itchiness and aid oral development. This can help alleviate the discomfort babies feel when their teeth start to emerge. If your baby is sensitive to certain textures or shapes, this product with its soft silicone material and multiple nubs might be a good fit.&lt;/p&gt;

&lt;p&gt;Conversely, if your baby prefers simpler shapes or firmer materials, it's a good idea to look at other types of teethers as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  1+1 configuration and price: Is it a reasonable choice?
&lt;/h3&gt;

&lt;p&gt;The Petit Nube Silicone Bear Crab Wrist Teether is sold in a 1+1 limited quantity configuration for 12,500 won. Teethers often need to be replaced periodically for hygiene reasons, or babies may use several interchangeably.&lt;/p&gt;

&lt;p&gt;Therefore, the 1+1 configuration can be an attractive choice in terms of cost-effectiveness. You get two for the price of one, so you can use one at home and take the other for outings, or have a spare in case one gets lost haha. Parents with a set budget for teethers or those looking to buy multiple at once should consider this price point and configuration. However, it is not a Rocket Delivery product, so it's necessary to order in advance, considering the delivery time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/shorts/5xHigDj86BI" rel="noopener noreferrer"&gt;▶ Watch on Shorts&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About the Author&lt;/strong&gt;&lt;br&gt;
I'm a dad raising two kids, and I personally test and choose the products we need.&lt;br&gt;
My reviews are honest, based on my actual experience, and prioritize 'would I buy this again?' over advertising fees.&lt;/p&gt;

</description>
      <category>babyproducts</category>
      <category>teetherreview</category>
      <category>parentingtips</category>
      <category>productreview</category>
    </item>
    <item>
      <title>LLM API 무료 티어 한도 초과? 자동 폴백 전략으로 서비스 중단 막기</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Mon, 03 Aug 2026 01:31:51 +0000</pubDate>
      <link>https://dev.to/kys7442/llm-api-muryo-tieo-hando-cogwa-jadong-polbaeg-jeonryageuro-seobiseu-jungdan-maggi-4341</link>
      <guid>https://dev.to/kys7442/llm-api-muryo-tieo-hando-cogwa-jadong-polbaeg-jeonryageuro-seobiseu-jungdan-maggi-4341</guid>
      <description>&lt;h2&gt;
  
  
  LLM API 무료 티어 한도 초과? 자동 폴백 전략으로 서비스 중단 막기
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2860rljhfcyza8qnv7fg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2860rljhfcyza8qnv7fg.png" alt="LLM API 무료 티어 한도 초과? 자동 폴백 전략으로 서비스 중단 막기" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;코딩아빠 개발 노트에 오신 걸 환영합니다. 주말에 짬 내어 제가 실제로 부딪히고 해결한 경험을 정리해 봅니다. 얼마 전 저희 LLM 기반 서비스에서 예상치 못한 중단 사태가 발생했습니다. 사용자들은 갑자기 응답이 오지 않는다고 불평했고, 운영팀은 문의 폭탄에 시달렸습니다. 급하게 로그를 살펴보니, 특정 LLM API에서 429 에러가 계속해서 터져 나오고 있더군요. 무료 티어를 사용하고 있었는데, 일일 호출 한도에 걸린 것이었습니다. 처음에는 단순한 API 장애인 줄 알았지만, 문제를 깊게 파고들면서 무료 티어의 편리함 뒤에 숨겨진 함정을 깨달았고, 이에 대한 견고한 방어 전략이 필요하다는 결론에 도달했습니다. 이번 글은 그 과정을 담은 기록입니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  이번에 정리한 내용
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;LLM API 무료 티어 한도 초과 시 발생하는 문제점&lt;/li&gt;
&lt;li&gt;API Rate Limit (429 에러) 및 기타 API 오류의 원인&lt;/li&gt;
&lt;li&gt;여러 LLM API 키와 모델을 활용한 폴백(Fallback) 전략 구현 방법&lt;/li&gt;
&lt;li&gt;Python 예시 코드를 통해 자동 폴백 로직 적용하기&lt;/li&gt;
&lt;li&gt;서비스 안정성 및 비용 효율성을 동시에 확보하는 운영 노하우&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  예상치 못한 서비스 중단, 그 시작
&lt;/h3&gt;

&lt;p&gt;어느 날 아침, 알림이 폭주하기 시작했습니다. 사용자들은 '응답이 안 와요', '서비스가 멈췄어요' 같은 메시지를 쏟아냈습니다. 급하게 상황판을 확인해보니, LLM API 호출 관련 지표가 곤두박질치고 있었더군요. 백엔드 애플리케이션 로그를 열어보니, 수많은 HTTP 429 에러가 연달아 찍히고 있었습니다. 처음에는 LLM 제공자 측의 일시적인 문제인 줄 알았습니다. 재배포나 서버 재시작으로 해결될까 싶어 시도했지만, 문제는 지속되었습니다. 서비스가 멈추자마자 CS팀은 아수라장이 되었고, 저도 마음이 급해지기 시작했습니다. 단순한 버그가 아니라, 서비스의 근간을 흔드는 문제라는 직감이 들었습니다.&lt;/p&gt;

&lt;p&gt;이런 상황은 개발자로서 가장 당황스러운 순간 중 하나입니다. 코드를 배포하고 나서 발생하는 예상치 못한 장애는 늘 긴장감을 주지만, 외부 API 의존성에서 오는 문제는 통제하기 어렵다는 점에서 더욱 까다롭습니다. 특히 LLM API는 단순한 데이터 요청을 넘어 서비스의 핵심 로직에 깊이 연결되어 있었기에, 그 여파는 더욱 컸습니다. 결국 급하게 임시방편으로 호출량을 줄이는 작업을 진행하며 원인 파악에 집중했습니다. 고객 경험이 저하되는 것을 보면서, 이런 상황을 미리 대비하지 못한 것에 대한 아쉬움이 컸습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  무료 티어의 양날의 검, 원인 분석
&lt;/h3&gt;

&lt;p&gt;로그에 찍힌 429 에러 메시지를 자세히 보니, 'Quota exceeded for quota metric 'Queries' and limit 'Queries per minute' of service...'라는 내용이 명확하게 보였습니다. 저희가 사용하던 LLM 서비스의 무료 티어 한도에 도달했던 것입니다. 평소에는 트래픽이 많지 않아 문제가 없었는데, 특정 이벤트로 인해 일시적으로 사용자가 몰리면서 일일 또는 시간당 호출 한도를 초과해 버린 것이죠. 무료 티어는 개발 초기나 소규모 서비스에는 분명 큰 도움이 됩니다. 하지만 이렇게 갑작스러운 트래픽 증가나 예상치 못한 사용 패턴 변화에는 취약하다는 것을 뼈저리게 느꼈습니다.&lt;/p&gt;

&lt;p&gt;또한, 특정 모델의 API가 불안정하거나 간헐적으로 오류를 반환하는 경우도 있었습니다. 이는 Rate Limit과는 별개의 문제로, 네트워크 지연이나 LLM 서버 내부 문제로 추정되었습니다. 이런 상황에서 단순한 재시도 로직만으로는 충분하지 않다는 것을 깨달았죠. 하나의 키나 하나의 모델에만 의존하는 것은 결국 서비스 안정성을 담보할 수 없다는 의미였습니다. 아래는 당시 Gemini API에서 확인했던 Rate Limit 응답의 예시입니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "error": {
    "code": 429,
    "message": "Quota exceeded for quota metric 'Queries' and limit 'Queries per minute' of service 'generativelanguage.googleapis.com' for consumer 'projects/PROJECT_NUMBER'.",
    "status": "RESOURCE_EXHAUSTED"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이런 명확한 메시지는 문제 해결의 실마리가 되었지만, 동시에 저희 서비스가 얼마나 취약했는지 보여주는 증거이기도 했습니다. 원인 파악 후에는 근본적인 해결책을 마련해야 한다는 압박감이 들었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  급한 불 끄기: 다중 키 전략 구상
&lt;/h3&gt;

&lt;p&gt;원인 분석 후, 가장 먼저 떠올린 해결책은 '무료 키가 막히면 유료 키를 쓰자'는 단순한 아이디어였습니다. 하지만 단순히 유료 키를 등록하는 것을 넘어, 어떤 상황에서 어떻게 전환할지 구체적인 전략이 필요했습니다. 첫째, 비용 효율성을 고려해 무료 키를 최우선으로 사용해야 했습니다. 둘째, 무료 키가 한도에 도달하거나 API 오류가 발생하면 자동으로 유료 키로 전환되어야 했습니다. 셋째, 만약 특정 LLM 제공자의 API 자체가 불안정하다면, 다른 LLM 제공자의 모델로도 전환할 수 있는 유연성을 확보하는 것이 좋겠다고 생각했습니다. 이를 위해 여러 API 키를 애플리케이션 레벨에서 관리하는 로직을 구축하기로 했습니다.&lt;/p&gt;

&lt;p&gt;이 전략의 핵심은 '우선순위'와 '상태 관리'입니다. 각 키에 대한 사용 우선순위를 정하고, 현재 키의 상태(예: 한도 초과, 오류 발생)를 추적해야 했습니다. 예를 들어, 무료 키가 429 에러를 반환하면 해당 키는 당분간 사용 불가능한 상태로 표시하고, 다음 우선순위인 유료 키를 사용하도록 하는 방식입니다. 이 과정에서 재시도(Retry) 로직도 함께 고려해야 했습니다. 단순히 한 번 실패했다고 바로 다른 키로 넘어가는 것이 아니라, 짧은 백오프(Backoff) 후 몇 차례 재시도해본 뒤에도 실패하면 다음 키로 전환하는 것이 더 안정적이라고 판단했습니다. 이렇게 하면 일시적인 네트워크 문제나 API 지연에도 더 잘 대응할 수 있습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  견고한 폴백 로직 구현하기
&lt;/h3&gt;

&lt;p&gt;본격적인 폴백 로직 구현에 들어갔습니다. Python 환경에서 OpenAI API를 예시로 들자면, 여러 API 키를 리스트 형태로 관리하고, 순회하면서 API 호출을 시도하는 방식으로 구현할 수 있습니다. 각 호출에서 &lt;code&gt;RateLimitError&lt;/code&gt;나 기타 &lt;code&gt;OpenAIError&lt;/code&gt;가 발생하면, 현재 키를 건너뛰고 다음 키로 넘어가는 구조입니다. 이 과정에서 환경 변수를 활용하여 실제 키를 관리하는 것이 보안상 좋겠다고 생각했습니다. 아래는 제가 구현했던 로직의 핵심 아이디어를 담은 Python 코드입니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import os
import openai

def call_llm_with_fallback(prompt, free_key=os.getenv('FREE_LLM_KEY'), paid_key=os.getenv('PAID_LLM_KEY')):
    keys = [free_key, paid_key]
    for key in keys:
        if not key: continue
        try:
            openai.api_key = key
            response = openai.Completion.create(model="gpt-3.5-turbo-instruct", prompt=prompt)
            return response.choices[0].text.strip()
        except openai.error.RateLimitError:
            print(f"Rate limit hit with key: {key}. Trying next key...")
            continue
        except openai.error.OpenAIError as e:
            print(f"API error with key {key}: {e}. Trying next key...")
            continue
    raise Exception("All LLM keys failed.")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이 코드에서는 &lt;code&gt;free_key&lt;/code&gt;와 &lt;code&gt;paid_key&lt;/code&gt;를 순차적으로 시도합니다. &lt;code&gt;RateLimitError&lt;/code&gt;가 발생하면 다음 키로 넘어가는 것이 핵심이죠. 실제 운영 환경에서는 단순히 다음 키로 넘어가는 것을 넘어, 실패한 키의 상태를 일정 시간 동안 '사용 불가'로 표시하고, 재시도 간격에 지수 백오프(Exponential Backoff)를 적용하는 등의 정교한 로직이 필요합니다. 예를 들어, 특정 키가 429 에러를 반환하면 5분간 해당 키를 사용하지 않도록 캐시하거나, 실패 횟수에 따라 재시도 간격을 점진적으로 늘리는 방식입니다. 다음은 키 관리의 개념적인 의사 코드입니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Conceptual pseudo-code for key management
function get_next_available_llm_key(current_key_status):
    if current_key_status.free_key_limit_exceeded:
        return 'PAID_LLM_KEY'
    if current_key_status.model_a_error_rate &amp;gt; threshold:
        return 'MODEL_B_KEY'
    return 'FREE_LLM_KEY'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이처럼 &lt;code&gt;get_next_available_llm_key&lt;/code&gt; 함수는 현재 키들의 상태를 기반으로 가장 적합한 다음 키를 반환하도록 설계합니다. 이는 단순한 순환을 넘어, 각 키의 건강 상태를 모니터링하고 동적으로 우선순위를 조정하는 복잡한 로직을 포함할 수 있습니다. 이렇게 하면 서비스 중단 시간을 최소화하고, 안정적인 운영을 지속할 수 있습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  실제 환경에서의 검증 과정
&lt;/h3&gt;

&lt;p&gt;폴백 로직을 구현한 후, 실제 상황에서 제대로 동작하는지 검증하는 것이 중요했습니다. 개발 환경에서 테스트하는 것만으로는 부족하다고 생각했죠. 가장 확실한 방법은 의도적으로 문제를 발생시켜 폴백이 트리거되는지 확인하는 것이었습니다. 우선, 무료 티어의 API 키를 일시적으로 무효화해 보았습니다. 예상대로 API 호출이 실패했고, 로그에는 'API error with key [무료 키]: Invalid API key' 같은 메시지와 함께 다음 유료 키로 전환을 시도하는 로그가 찍혔습니다. 유료 키를 사용한 호출은 성공적으로 완료되었고, 서비스는 정상적으로 응답했습니다.&lt;/p&gt;

&lt;p&gt;다음으로는 Rate Limit을 재현하는 테스트를 진행했습니다. 짧은 시간 내에 무료 티어의 한도를 초과하도록 대량의 LLM 호출을 발생시켰습니다. 처음에는 무료 키로 호출이 잘 되다가, 곧이어 429 에러가 발생하기 시작했습니다. 그리고 로그에는 'Rate limit hit with key [무료 키]. Trying next key...'라는 메시지가 명확히 보이며, 유료 키로 전환되어 정상적으로 응답을 받아오는 것을 확인할 수 있었습니다. 이 과정을 통해 저희가 구현한 폴백 전략이 예상대로 동작하며, 서비스 중단 없이 안정적으로 API를 활용할 수 있음을 검증했습니다. 실제 운영 환경과 유사한 조건에서 테스트함으로써, 잠재적인 문제점들을 미리 발견하고 보완할 수 있었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  안정성과 비용 효율, 두 마리 토끼 잡기
&lt;/h3&gt;

&lt;p&gt;이번 경험을 통해 LLM API 폴백 전략이 단순히 장애 대응을 넘어, 서비스 운영의 핵심적인 설계 원칙임을 깨달았습니다. 첫째, 서비스 안정성 측면에서 LLM 제공자의 일시적인 장애나 Rate Limit에 상관없이 사용자에게 지속적인 서비스를 제공할 수 있게 되었습니다. 이는 사용자 경험을 크게 개선하고, 운영팀의 부담을 줄이는 데 결정적인 역할을 했습니다. 둘째, 비용 효율성 측면에서도 큰 이점이 있습니다. 평소에는 무료 티어를 최대한 활용하고, 오직 무료 티어 한도 초과나 장애 발생 시에만 유료 키나 고비용 모델로 전환함으로써 전체 API 호출 비용을 절감할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;물론 이 전략이 만능은 아닙니다. 여러 LLM 제공자를 사용하는 경우, 각 모델의 응답 품질이나 지연 시간, 비용 등을 지속적으로 모니터링하고 최적화하는 노력이 필요합니다. 또한, 키 관리 시스템 자체의 안정성과 보안에도 신경 써야 합니다. 하지만 이 정도의 노력으로 얻을 수 있는 서비스 안정성과 비용 절감 효과는 충분히 가치 있다고 생각합니다. 앞으로 LLM 기반 서비스를 개발할 때는 이 폴백 전략을 기본 설계에 포함하여, 더욱 견고하고 효율적인 서비스를 만들어나갈 계획입니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  마치며
&lt;/h3&gt;

&lt;p&gt;LLM API 호출에서 발생할 수 있는 무료 티어 한도 초과나 일시적 오류는 서비스 안정성을 위협하는 큰 요인이었습니다. 하지만 이번에 구축한 자동 폴백 전략 덕분에 이런 위험을 효과적으로 관리할 수 있게 되었습니다. 여러 API 키를 우선순위에 따라 사용하고, 에러 발생 시 자동으로 전환하는 로직은 서비스 중단을 막는 든든한 방패가 되어주었습니다. 앞으로도 LLM 기반 서비스를 안정적으로 운영하기 위한 다양한 전략들을 지속적으로 고민해볼 생각입니다.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>api</category>
      <category>fallback</category>
      <category>ratelimit</category>
    </item>
    <item>
      <title>FCM HTTP v1 푸시 알림, 서버와 Flutter 앱에서 처음부터 구현하기</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Tue, 28 Jul 2026 08:52:52 +0000</pubDate>
      <link>https://dev.to/kys7442/fcm-http-v1-pusi-alrim-seobeowa-flutter-aebeseo-ceoeumbuteo-guhyeonhagi-32ff</link>
      <guid>https://dev.to/kys7442/fcm-http-v1-pusi-alrim-seobeowa-flutter-aebeseo-ceoeumbuteo-guhyeonhagi-32ff</guid>
      <description>&lt;h2&gt;
  
  
  FCM HTTP v1 푸시 알림, 서버와 Flutter 앱에서 처음부터 구현하기
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft9vrop4v4y2njj2rb13k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft9vrop4v4y2njj2rb13k.png" alt="FCM HTTP v1 푸시 알림, 서버와 Flutter 앱에서 처음부터 구현하기" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;안녕하세요, 코딩아빠입니다. 오늘 제가 들려드릴 이야기는, 최근에 직접 부딪히고 해결했던 경험을 고스란히 담아낸 기술 노트입니다. 기존 푸시 알림 시스템이 오래된 FCM Legacy API를 사용하고 있었는데, 이 방식이 앞으로는 권장되지 않거나 언제든 지원이 중단될 수 있다는 소식에 마음이 편치 않았습니다. 그래서 고심 끝에, 최신 FCM HTTP v1 API를 이용해 서버부터 Flutter 앱까지 푸시 알림 시스템 전체를 새롭게 구축하기로 결정했습니다. 아무것도 없는 백지상태에서 시작해야 했기에, 그 과정에서 꽤 많은 시행착오를 겪었지만, 덕분에 많은 것을 배울 수 있었네요. 이 글이 저와 비슷한 고민을 하고 계신 분들께 작은 길잡이가 되었으면 합니다.&lt;/p&gt;

&lt;p&gt;이런 분께 — PHP 서버와 Flutter 앱으로 FCM HTTP v1 푸시 알림 시스템을 처음부터 구축하려는 개발자. · 난이도는 중급 정도&lt;/p&gt;

&lt;h3&gt;
  
  
  글의 요점
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;FCM HTTP v1 API를 사용하는 이유와 이점&lt;/li&gt;
&lt;li&gt;PHP 서버에서 Firebase 서비스 계정을 이용한 인증 및 메시지 발송 방법&lt;/li&gt;
&lt;li&gt;Flutter 앱에서 FCM 토큰 관리 및 포그라운드/백그라운드 메시지 수신 처리&lt;/li&gt;
&lt;li&gt;푸시 알림 시스템 구축 시 필요한 서버, 앱, DB, 관리자 UI 연동 과정&lt;/li&gt;
&lt;li&gt;실제 시스템 구축 중 발생할 수 있는 주요 문제점과 해결 과정&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  오래된 방식 대신 새로운 길을 택하며 겪은 고민
&lt;/h3&gt;

&lt;p&gt;새로운 푸시 알림 시스템을 구축해야 한다는 과제를 받았을 때, 가장 먼저 마주한 것은 어떤 API를 사용할 것인가 하는 문제였습니다. 기존에 사용하던 FCM Legacy API는 분명 익숙했지만, Firebase 공식 문서에서는 이미 HTTP v1 API로의 전환을 강력히 권장하고 있었죠. Legacy API가 언제든 지원이 중단될 수 있다는 경고 문구가 계속 마음에 걸렸습니다. 당장은 큰 문제가 없어도, 몇 년 후에는 시스템을 다시 뜯어고쳐야 할 수도 있다는 생각이 들더군요. 그래서 눈앞의 편의보다는 장기적인 안정성과 확장성을 택해, 다소 생소했지만 최신 HTTP v1 API를 처음부터 적용하기로 마음먹었습니다.&lt;/p&gt;

&lt;p&gt;이 결정은 새로운 학습 곡선을 의미했습니다. Legacy API는 간단한 키 기반 인증으로 메시지를 보낼 수 있었지만, v1 API는 OAuth 2.0 기반의 인증 절차를 요구했으니까요. 단순히 메시지 페이로드만 바꾸는 문제가 아니라, 서버 측에서 인증 토큰을 관리하고 갱신하는 로직까지 새로 만들어야 한다는 부담이 있었습니다. 또한, Flutter 앱에서도 기존 FCM 연동 방식을 최신 &lt;code&gt;firebase_messaging&lt;/code&gt; 패키지에 맞춰 재정비해야 했습니다. 말 그대로 바닥부터 시작하는 셈이었지만, 한 번 제대로 구축해두면 오랫동안 안정적으로 사용할 수 있을 것이라는 기대로 차근차근 준비를 시작했습니다.&lt;/p&gt;

&lt;p&gt;가장 먼저 고려한 것은 서버 측에서 Firebase 서비스 계정을 어떻게 활용할 것인가였습니다. 서비스 계정 JSON 파일을 PHP 서버에서 안전하게 관리하고, 이를 이용해 Google OAuth 2.0 액세스 토큰을 발급받는 과정이 핵심이었습니다. 처음에는 PHP에서 직접 JWT를 구성하여 액세스 토큰을 요청하는 방법을 생각했는데, 이는 생각보다 복잡하고 오류 가능성이 높아 보였습니다. 다행히 Google API 클라이언트 라이브러리가 PHP용으로 잘 준비되어 있어, 이를 활용하기로 방향을 잡았습니다. 이 라이브러리를 사용하면 복잡한 인증 절차를 비교적 쉽게 처리할 수 있겠더군요.&lt;/p&gt;

&lt;h3&gt;
  
  
  서버에서 FCM HTTP v1 API와 첫 대면하기: 인증의 벽
&lt;/h3&gt;

&lt;p&gt;PHP 서버에서 FCM HTTP v1 API를 사용하기 위한 첫 관문은 바로 인증이었습니다. FCM 메시지 발송은 Firebase 프로젝트에 대한 권한이 필요하며, 이를 위해 OAuth 2.0 액세스 토큰을 받아야 했습니다. Firebase 서비스 계정 JSON 파일을 서버에 안전하게 업로드하고, 이 파일을 통해 토큰을 발급받는 것이 핵심 과정이었죠. 처음에는 &lt;code&gt;Google_Client&lt;/code&gt; 클래스를 어떻게 초기화하고 사용할지 감이 잘 오지 않았습니다. 문서들을 찾아보면서 &lt;code&gt;setAuthConfig&lt;/code&gt; 메소드를 통해 서비스 계정 파일을 지정하고, &lt;code&gt;setScopes&lt;/code&gt;로 필요한 권한 범위를 설정해야 한다는 것을 알게 되었습니다.&lt;/p&gt;

&lt;p&gt;특히 &lt;code&gt;scopes&lt;/code&gt; 설정이 중요했는데, FCM 메시징을 위한 정확한 스코프인 &lt;code&gt;'https://www.googleapis.com/auth/firebase.messaging'&lt;/code&gt;를 지정해야 했습니다. 만약 이 스코프를 잘못 지정하거나 누락하면, 토큰 발급은 성공하더라도 메시지 발송 API 호출 시 권한 오류가 발생하더군요. 몇 번의 시도 끝에 올바른 스코프를 찾아 적용하니, 비로소 &lt;code&gt;fetchAccessTokenWithAssertion()&lt;/code&gt; 메소드를 통해 유효한 액세스 토큰을 받아낼 수 있었습니다. 이 토큰은 유효 기간이 정해져 있으므로, 실제 시스템에서는 토큰 만료 시 자동으로 갱신하는 로직을 추가해야 했습니다. 저는 토큰을 캐싱해두고 만료 직전에 갱신하는 방식을 채택했습니다.&lt;/p&gt;

&lt;p&gt;아래는 PHP에서 OAuth 토큰을 발급받는 기본적인 예시입니다. 서비스 계정 JSON 파일 경로를 정확히 지정하는 것이 중요합니다. 이 과정이 성공적으로 이루어져야만 FCM HTTP v1 API를 호출할 수 있는 자격을 얻게 됩니다. 처음에는 이 인증 과정에서 시간을 많이 할애했는데, 결국은 라이브러리의 도움을 받아 해결할 수 있었습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$client = new Google_Client();
$client-&amp;gt;setAuthConfig('path/to/your-service-account.json');
$client-&amp;gt;setScopes(['https://www.googleapis.com/auth/firebase.messaging']);
$accessToken = $client-&amp;gt;fetchAccessTokenWithAssertion()['access_token'];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이렇게 발급받은 액세스 토큰은 FCM 발송 요청의 &lt;code&gt;Authorization&lt;/code&gt; 헤더에 &lt;code&gt;Bearer&lt;/code&gt; 토큰으로 포함되어야 합니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  메시지 발송 로직 구현: 헛다리와 실제 동작
&lt;/h3&gt;

&lt;p&gt;액세스 토큰을 발급받는 데 성공했으니, 이제 실제 FCM 메시지를 발송할 차례였습니다. FCM HTTP v1 API는 &lt;code&gt;https://fcm.googleapis.com/v1/projects/YOUR_PROJECT_ID/messages:send&lt;/code&gt; 엔드포인트를 사용하며, &lt;code&gt;POST&lt;/code&gt; 방식으로 JSON 형태의 메시지 페이로드를 전송해야 합니다. 처음에는 Legacy API와 유사하게 단순한 &lt;code&gt;notification&lt;/code&gt; 필드만으로 메시지를 구성했는데, 생각보다 복잡한 구조를 요구하더군요. &lt;code&gt;message&lt;/code&gt; 객체 안에 &lt;code&gt;token&lt;/code&gt;, &lt;code&gt;notification&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt; 등의 필드를 계층적으로 구성해야 했습니다. 공식 문서를 꼼꼼히 살펴보며 JSON 구조를 맞춰나가는 데 시간이 좀 걸렸습니다.&lt;/p&gt;

&lt;p&gt;특히, &lt;code&gt;token&lt;/code&gt; 필드에 기기별 FCM 토큰을 정확히 넣어주는 것이 중요했습니다. 이 토큰이 없으면 어떤 기기로도 메시지가 전달되지 않으니까요. &lt;code&gt;notification&lt;/code&gt; 필드는 알림창에 표시될 제목과 본문을 정의하고, &lt;code&gt;data&lt;/code&gt; 필드는 앱에서 처리할 추가 데이터를 key-value 형태로 담는 데 사용됩니다. &lt;code&gt;data&lt;/code&gt; 메시지는 앱이 백그라운드나 종료 상태일 때 &lt;code&gt;notification&lt;/code&gt; 메시지와 함께 전달되거나, 포그라운드 상태일 때 앱 내에서만 처리되는 용도로 유용하게 쓰일 수 있습니다.&lt;/p&gt;

&lt;p&gt;아래는 PHP에서 &lt;code&gt;cURL&lt;/code&gt;을 이용해 FCM HTTP v1 메시지를 발송하는 예시입니다. &lt;code&gt;Authorization&lt;/code&gt; 헤더에 앞서 발급받은 액세스 토큰을 포함하고, &lt;code&gt;Content-Type&lt;/code&gt;을 &lt;code&gt;application/json&lt;/code&gt;으로 설정해야 합니다. 또한, &lt;code&gt;YOUR_PROJECT_ID&lt;/code&gt; 부분은 실제 Firebase 프로젝트 ID로 교체해야 한다는 점을 잊지 말아야 합니다. 이 부분을 처음에는 프로젝트 이름으로 잘못 넣었다가 오류를 겪기도 했습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$headers = ['Authorization: Bearer ' . $accessToken, 'Content-Type: application/json'];
$data = ['message' =&amp;gt; ['token' =&amp;gt; 'FCM_DEVICE_TOKEN', 'notification' =&amp;gt; ['title' =&amp;gt; '제목', 'body' =&amp;gt; '내용']]];
$ch = curl_init('https://fcm.googleapis.com/v1/projects/YOUR_PROJECT_ID/messages:send');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_exec($ch);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이렇게 코드를 작성하고 테스트 발송을 해보니, 드디어 첫 FCM 알림이 기기에 도착했습니다. 성공적인 발송을 확인한 후에는, 발송 결과를 데이터베이스에 로그로 남기는 로직을 추가하여 추후 문제 발생 시 추적할 수 있도록 대비했습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flutter 앱 연동 과정: 토큰 등록과 메시지 수신 확인
&lt;/h3&gt;

&lt;p&gt;서버 측에서 메시지를 발송할 준비가 되었으니, 이제 Flutter 앱에서 이 메시지를 수신하고 처리할 차례였습니다. Flutter 앱에 FCM을 연동하기 위해서는 &lt;code&gt;firebase_messaging&lt;/code&gt; 패키지를 사용해야 합니다. 먼저 &lt;code&gt;main&lt;/code&gt; 함수에서 &lt;code&gt;Firebase.initializeApp()&lt;/code&gt;를 호출하여 Firebase를 초기화하는 것이 필수적입니다. 이 과정이 누락되면 FCM 기능이 제대로 동작하지 않더군요. 또한, 앱이 실행될 때 기기의 고유한 FCM 토큰을 받아 서버에 등록하는 로직을 구현해야 했습니다.&lt;/p&gt;

&lt;p&gt;FCM 토큰은 기기가 변경되거나 앱이 재설치될 때 등 여러 상황에서 갱신될 수 있기 때문에, &lt;code&gt;FirebaseMessaging.instance.onTokenRefresh.listen()&lt;/code&gt; 메소드를 사용하여 토큰 갱신 이벤트를 감지하고, 갱신된 토큰을 즉시 서버에 업데이트하는 것이 중요했습니다. 만약 이 과정을 놓치면, 서버가 구형 토큰으로 메시지를 보내게 되어 알림이 도달하지 않는 문제가 발생할 수 있습니다. 저는 앱 실행 시 현재 토큰을 한 번 서버에 전송하고, 토큰 갱신 이벤트가 발생할 때마다 다시 전송하도록 구현했습니다.&lt;/p&gt;

&lt;p&gt;메시지 수신 처리는 앱의 상태에 따라 다르게 접근해야 합니다. 앱이 포그라운드에 있을 때는 &lt;code&gt;FirebaseMessaging.onMessage.listen()&lt;/code&gt;을 통해 메시지를 실시간으로 수신하고, 사용자에게 직접 알림을 보여주거나 앱 내에서 특정 동작을 수행할 수 있습니다. 앱이 백그라운드나 종료 상태일 때는 &lt;code&gt;FirebaseMessaging.onBackgroundMessage()&lt;/code&gt; 핸들러를 등록하여 메시지를 처리하도록 했습니다. 이 백그라운드 핸들러는 앱이 완전히 종료된 상태에서도 메시지를 받아 처리할 수 있게 해주므로, 매우 중요한 부분입니다. 이 핸들러는 반드시 최상위 함수로 선언되어야 한다는 제약도 있었습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Future&amp;lt;void&amp;gt; _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // 백그라운드 메시지 처리: 예를 들어, 로컬 알림을 띄우거나 데이터 업데이트 등
  await Firebase.initializeApp(); // 백그라운드에서도 Firebase 초기화 필요
  print('Handling a background message: ${message.messageId}');
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  // FCM 토큰 갱신 리스너
  FirebaseMessaging.instance.onTokenRefresh.listen((fcmToken) {
    print('FCM Token refreshed: $fcmToken');
    // 서버에 토큰 등록 API 호출: 이 부분을 구현해야 합니다.
    // 예를 들어, ApiService.registerFcmToken(fcmToken); 와 같이 호출합니다.
  });

  // 앱 시작 시 현재 FCM 토큰 가져와서 서버에 등록 (선택 사항이지만 권장)
  String? currentToken = await FirebaseMessaging.instance.getToken();
  if (currentToken != null) {
    print('Current FCM Token: $currentToken');
    // 서버에 토큰 등록 API 호출
  }

  // 포그라운드 메시지 처리
  FirebaseMessaging.onMessage.listen((RemoteMessage message) {
    print('Got a message whilst in the foreground!');
    print('Message data: ${message.data}');
    if (message.notification != null) {
      print('Message also contained a notification: ${message.notification!.title} / ${message.notification!.body}');
      // 로컬 알림을 띄우는 등의 처리
    }
  });

  runApp(const MyApp());
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이렇게 앱 코드까지 완성하고 나니, 이제 서버와 앱 간의 연동 준비가 거의 끝난 것 같았습니다. 마지막으로 Firebase 콘솔에서 APNs 인증 키를 등록하는 등, iOS 환경에서 푸시 알림이 제대로 동작하도록 몇 가지 수동 설정을 해주는 것도 잊지 않았습니다. 이 설정이 없으면 iOS 기기에서는 알림이 오지 않더군요.&lt;/p&gt;

&lt;h3&gt;
  
  
  견고한 시스템을 위한 뒷단 작업: DB와 관리 도구
&lt;/h3&gt;

&lt;p&gt;서버와 앱 간의 기본적인 푸시 알림 연동은 마쳤지만, 실질적인 시스템으로 운영하기 위해서는 몇 가지 뒷단 작업이 더 필요했습니다. 가장 중요한 것은 바로 사용자 기기별 FCM 토큰을 저장하고 관리하는 데이터베이스 스키마와 API를 구축하는 것이었습니다. 토큰은 사용자별로 고유하며, 앱 설치나 재설치, 기기 변경 등 다양한 상황에서 갱신될 수 있으므로, 항상 최신 상태를 유지해야 했습니다. 저는 MySQL 데이터베이스에 사용자 ID와 FCM 토큰을 매핑하는 테이블을 만들고, 토큰이 갱신될 때마다 &lt;code&gt;UPDATE&lt;/code&gt; 하거나, 새로운 기기에서 로그인 시 &lt;code&gt;INSERT&lt;/code&gt; 하는 로직을 구현했습니다.&lt;/p&gt;

&lt;p&gt;또한, 푸시 알림 발송 기록을 남기는 것도 중요했습니다. 어떤 알림을 언제, 누구에게 보냈는지, 그리고 그 결과는 어떠했는지 추적할 수 있어야 했기 때문입니다. 이를 위해 발송 로그 테이블을 별도로 구축하고, 메시지 제목, 내용, 대상 사용자, 발송 시간, 그리고 Firebase로부터 받은 발송 응답까지 기록하도록 했습니다. 이 로그는 나중에 알림 전달 문제를 디버깅하거나, 알림 효과를 분석하는 데 귀중한 자료가 됩니다.&lt;/p&gt;

&lt;p&gt;마지막으로, 관리자가 직접 푸시 알림을 작성하고 발송할 수 있는 UI를 구축했습니다. 이 UI는 알림 제목과 내용을 입력하고, 특정 사용자 그룹이나 전체 사용자에게 알림을 보낼 수 있는 기능을 포함했습니다. 이 관리자 UI를 통해 테스트 알림을 쉽게 발송하고, 실제 운영 환경에서도 효율적으로 알림을 관리할 수 있도록 했습니다. UI에서 입력된 데이터는 앞서 만든 서버 API를 통해 FCM 발송 로직으로 전달되고, 발송 후에는 DB에 로그가 기록되는 전체 워크플로우를 완성한 셈입니다. 이처럼 서버, 앱, DB, 관리자 UI까지 전체적인 흐름을 한 번에 고려해야만 견고하고 사용하기 편리한 푸시 알림 시스템을 만들 수 있다는 것을 다시 한번 깨달았네요.&lt;/p&gt;

&lt;h3&gt;
  
  
  마무리 검증: 예상치 못한 상황과 최종 확인
&lt;/h3&gt;

&lt;p&gt;모든 시스템 구축이 끝나고 나면, 이제 철저한 검증 단계가 남아 있었습니다. 단순히 알림이 한두 번 오는 것을 확인하는 것을 넘어, 다양한 시나리오에서 시스템이 안정적으로 동작하는지 확인해야 했죠. 저는 관리자 UI를 통해 여러 종류의 테스트 푸시 알림을 발송하며 개발 및 실제 디바이스에서 수신 여부를 확인했습니다. 특히 중요한 것은 앱의 상태에 따른 알림 수신이었습니다. 포그라운드, 백그라운드, 그리고 앱이 완전히 종료된 상태에서도 알림이 정상적으로 도착하는지 여러 번 확인했습니다.&lt;/p&gt;

&lt;p&gt;초기에는 백그라운드 메시지 처리가 제대로 되지 않는 문제가 있었습니다. Flutter의 &lt;code&gt;onBackgroundMessage&lt;/code&gt; 핸들러가 최상위 함수로 선언되지 않아 발생한 문제였는데, 이 부분을 수정하니 잘 동작했습니다. 또한, iOS 환경에서는 APNs 인증 키 설정이 제대로 되지 않아 알림이 오지 않는 경우가 있었는데, Firebase 콘솔에서 &lt;code&gt;Apple 개발자 계정&lt;/code&gt;에서 발급받은 &lt;code&gt;APNs 인증 키&lt;/code&gt;를 올바르게 등록하고, Firebase 프로젝트 설정에서 Bundle ID와 팀 ID를 정확히 입력하니 해결되었습니다. 이처럼 플랫폼별 특성을 고려한 설정이 중요하더군요.&lt;/p&gt;

&lt;p&gt;데이터베이스에 발송 로그가 올바르게 기록되는지도 꼼꼼히 검증했습니다. 메시지 발송 후 DB를 확인하여, 발송 시간, 내용, 대상 등 모든 정보가 정확히 저장되어 있는지 확인했죠. 만약 발송은 되었는데 DB에 기록이 없다면 나중에 문제 추적이 어려워질 수 있습니다. 마지막으로, FCM 토큰 갱신 시 서버에 제대로 반영되는지도 확인했습니다. 앱을 삭제 후 재설치하거나, 다른 기기에서 로그인하는 등의 상황을 시뮬레이션하여 토큰이 변경되었을 때 서버의 DB에 최신 토큰이 정확히 업데이트되는지 확인하는 것이 중요했습니다. 이 모든 검증 과정을 거치고 나서야 비로소 FCM HTTP v1 푸시 알림 시스템이 안정적으로 작동할 준비가 되었다고 판단했습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  남는 이야기
&lt;/h3&gt;

&lt;p&gt;FCM HTTP v1 API를 이용한 푸시 알림 시스템을 처음부터 구축하는 과정은 결코 쉽지 않았습니다. 특히 구형 API와는 다른 인증 방식과 메시지 페이로드 구조 때문에 초반에 헤매기도 했습니다. 하지만 서버의 인증 및 발송 로직, Flutter 앱에서의 토큰 관리와 메시지 수신 처리, 그리고 이를 뒷받침하는 데이터베이스 스키마와 관리자 UI까지 전체적인 그림을 완성하면서 많은 것을 배울 수 있었습니다. 이 과정에서 겪었던 시행착오와 해결 과정이, 혹시라도 저와 같은 길을 걷고 계실 다른 개발자분들에게 조금이나마 도움이 되었으면 하는 바람입니다. 최신 기술 스택을 적용하는 것은 때론 번거롭지만, 그만큼 더 안정적이고 확장성 있는 시스템을 만들 수 있다는 보람이 큰 작업이었습니다.&lt;/p&gt;

</description>
      <category>fcmhttpv1</category>
      <category>firebasecloudmessaging</category>
      <category>php</category>
      <category>flutter</category>
    </item>
    <item>
      <title>그누보드와 Flutter 앱, JWT로 회원 연동하기: 레거시 시스템 통합 경험기</title>
      <dc:creator>바람의평온</dc:creator>
      <pubDate>Mon, 27 Jul 2026 08:29:37 +0000</pubDate>
      <link>https://dev.to/kys7442/geunubodeuwa-flutter-aeb-jwtro-hoeweon-yeondonghagi-regeosi-siseutem-tonghab-gyeongheomgi-4d16</link>
      <guid>https://dev.to/kys7442/geunubodeuwa-flutter-aeb-jwtro-hoeweon-yeondonghagi-regeosi-siseutem-tonghab-gyeongheomgi-4d16</guid>
      <description>&lt;h2&gt;
  
  
  그누보드와 Flutter 앱, JWT로 회원 연동하기: 레거시 시스템 통합 경험기
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd083prk0k42o27amhsr8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd083prk0k42o27amhsr8.png" alt="그누보드와 Flutter 앱, JWT로 회원 연동하기: 레거시 시스템 통합 경험기" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;안녕하세요, 코딩아빠입니다. 이번 글은 제가 최근 직접 부딪히고 해결했던 개발 경험을 정리한 기록입니다. 레거시 PHP CMS인 그누보드 웹사이트의 회원 체계를 그대로 활용하면서 Flutter 앱에서 자체 로그인, 회원가입, 토큰 갱신 기능을 구현해야 했던 상황이었죠. 기존 시스템의 작동 방식을 깊이 이해하는 것이 얼마나 중요한지 다시 한번 깨달았고, 특히 include 순서나 전역 변수 사용 방식 같은 사소한 부분이 예상치 못한 큰 오류를 발생시킬 수 있다는 교훈을 얻었습니다. 이 경험을 통해 얻은 노하우를 공유해 드립니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  이 글에서 짚는 것
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;레거시 그누보드 환경에서 PHP 기반 API를 구축하는 방법&lt;/li&gt;
&lt;li&gt;JWT(JSON Web Token)를 활용한 앱 인증 시스템 구현 원리&lt;/li&gt;
&lt;li&gt;그누보드 &lt;code&gt;common.php&lt;/code&gt; 및 &lt;code&gt;lib.php&lt;/code&gt; 로딩의 중요성과 올바른 적용법&lt;/li&gt;
&lt;li&gt;기존 웹사이트 회원 데이터베이스를 앱과 연동하는 실제 과정&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  오래된 그누보드, 새로운 Flutter 앱과의 만남 준비
&lt;/h3&gt;

&lt;p&gt;새로운 Flutter 앱을 개발하면서 기존에 운영하던 그누보드 웹사이트의 회원 체계를 그대로 사용해야 하는 상황에 놓였습니다. 별도의 소셜 로그인 연동 없이, 웹과 앱 사용자가 동일한 아이디와 비밀번호로 로그인하고 회원 정보를 공유하는 통합 시스템이 필요했죠. 이는 기존 회원의 불편을 최소화하고 데이터 일관성을 유지하기 위한 중요한 결정이었습니다. 가장 먼저 고민했던 부분은 바로 어떻게 기존 그누보드 회원 데이터베이스를 안전하고 효율적으로 앱과 연동할 것인가였습니다.&lt;/p&gt;

&lt;p&gt;그누보드가 PHP 기반의 레거시 CMS라는 점은 분명 도전 과제였습니다. 자체 라이브러리 로딩 방식과 전역 변수 의존성이 높다는 특성 때문에, 신규 API 개발 시에는 기존 그누보드 환경을 해치지 않으면서도 필요한 기능을 추가해야 했으니까요. 특히 기존 웹사이트의 로그인 로직을 직접 복제하거나 수정하는 대신, 앱만을 위한 별도의 인증 시스템을 구축하여 보안성과 확장성을 확보하는 방향으로 가닥을 잡았습니다. 여기에는 JWT(JSON Web Token)를 활용하는 것이 가장 적합하다고 판단했지요.&lt;/p&gt;

&lt;p&gt;JWT는 클라이언트와 서버 간의 정보 교환 시 사용되는 안전한 방법으로, 서버가 상태를 유지할 필요가 없어 확장성이 좋다는 장점이 있습니다. 앱에서 로그인 요청을 보내면, 서버는 회원 정보를 확인하고 유효한 경우 JWT를 발급하여 앱에 넘겨주는 방식입니다. 이후 앱은 이 토큰을 매 요청마다 헤더에 포함하여 보내고, 서버는 토큰의 유효성을 검증하여 인증된 사용자임을 확인하는 것이죠. 이러한 접근 방식은 레거시 시스템의 복잡성을 최소화하면서도 현대적인 앱 인증 시스템을 구축할 수 있는 좋은 대안이었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  그누보드 환경 위에서 API 엔드포인트 설계하기
&lt;/h3&gt;

&lt;p&gt;JWT 기반 인증 시스템을 구현하기 위해 서버에 몇 가지 새로운 API 엔드포인트를 구축해야 했습니다. 구체적으로는 로그인(&lt;code&gt;api/app/login.php&lt;/code&gt;), 회원가입(&lt;code&gt;api/app/register.php&lt;/code&gt;), 그리고 토큰 갱신(&lt;code&gt;api/app/refresh.php&lt;/code&gt;) 기능을 담당하는 PHP 파일을 새로 만들었죠. 이 파일들은 그누보드 설치 경로 내에 별도의 디렉터리를 만들어 관리함으로써 기존 그누보드 코드와의 충돌을 피하려고 했습니다.&lt;/p&gt;

&lt;p&gt;각 엔드포인트는 그누보드의 &lt;code&gt;g5_member&lt;/code&gt; 테이블을 직접 조회하거나 업데이트하는 방식으로 작동합니다. 예를 들어, 로그인 요청이 오면 전달받은 아이디와 비밀번호를 &lt;code&gt;g5_member&lt;/code&gt; 테이블의 데이터와 비교하여 일치 여부를 확인하는 식입니다. 여기서 가장 중요했던 부분은 바로 '그누보드 환경을 올바르게 초기화하는 것'이었습니다. 그누보드 핵심 라이브러리들을 제대로 로드하지 않으면 데이터베이스 연결이나 기타 전역 변수들이 초기화되지 않아 예상치 못한 오류를 뱉어내더군요.&lt;/p&gt;

&lt;p&gt;이를 해결하기 위해 각 API 파일의 최상단에 &lt;code&gt;define('_GNUBOARD_', true);&lt;/code&gt;를 선언하고, 이어서 &lt;code&gt;include_once('../../common.php');&lt;/code&gt;를 호출하는 순서를 엄격하게 지켰습니다. 이 &lt;code&gt;common.php&lt;/code&gt; 파일은 그누보드의 거의 모든 환경 설정과 핵심 함수들을 로드하는 역할을 하죠. 이 한 줄이 빠지거나 순서가 틀리면, 그누보드 환경이 제대로 잡히지 않아 데이터베이스 연결조차 되지 않는 문제가 발생했습니다. 제가 처음 겪었던 시행착오 중 하나였습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php
define('_GNUBOARD_', true);
include_once('../../common.php'); // 그누보드 환경 초기화

// JWT 라이브러리 로드 및 API 로직 구현
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;이렇게 &lt;code&gt;common.php&lt;/code&gt;를 로드한 후에는 비로소 그누보드의 데이터베이스 연결 객체(&lt;code&gt;$g5['db']&lt;/code&gt;)나 다른 유틸리티 함수들을 사용할 수 있게 됩니다. 이 부분이 레거시 시스템 위에서 새로운 기능을 개발할 때 가장 먼저 해결해야 할 퍼즐 조각이었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  JWT 발급과 갱신, 그리고 그누보드 회원 연동의 핵심
&lt;/h3&gt;

&lt;p&gt;그누보드 환경이 제대로 초기화되었다면, 이제 실제 로그인 로직을 구현하고 JWT를 발급하는 단계로 넘어갑니다. 앱에서 아이디와 비밀번호를 POST 요청으로 보내면, 서버에서는 이를 받아 &lt;code&gt;g5_member&lt;/code&gt; 테이블에서 해당 회원 정보를 조회합니다. 비밀번호는 그누보드의 해싱 방식에 맞춰 검증해야 하므로, 기존 그누보드 로그인 로직에서 사용되던 함수를 활용하는 것이 안전하고 정확합니다.&lt;/p&gt;

&lt;p&gt;인증이 성공하면, 해당 회원의 고유 식별자(예: &lt;code&gt;mb_id&lt;/code&gt;)를 포함하는 JWT 페이로드를 생성합니다. 이 페이로드에는 토큰의 만료 시간(&lt;code&gt;exp&lt;/code&gt;)과 같은 정보도 함께 담습니다. 만료 시간은 앱의 보안 정책과 사용자 편의성을 고려하여 적절히 설정하는 것이 중요합니다. 너무 짧으면 사용자가 자주 로그인해야 해서 불편하고, 너무 길면 보안에 취약해질 수 있으니까요. 발급된 JWT는 암호화되어 서명되며, 이 토큰을 HTTP 응답으로 앱에 전달합니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// JWT 발급 예시 (실제 라이브러리 코드는 복잡함)
$payload = ['user_id' =&amp;gt; $member['mb_id'], 'exp' =&amp;gt; time() + 3600];
$jwt = 'YOUR_GENERATED_JWT_TOKEN';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;앱은 이 JWT를 안전하게 저장하고, 이후 보호된 API를 호출할 때마다 HTTP &lt;code&gt;Authorization&lt;/code&gt; 헤더에 &lt;code&gt;Bearer {JWT}&lt;/code&gt; 형태로 포함하여 보냅니다. 또한, 장기적인 사용자 경험을 위해 리프레시 토큰(Refresh Token) 메커니즘도 함께 구현했습니다. 액세스 토큰이 만료되면, 앱은 저장된 리프레시 토큰을 이용해 새로운 액세스 토큰을 요청하는 방식으로, 사용자가 다시 로그인할 필요 없이 세션을 연장할 수 있도록 했습니다. 이 과정 역시 &lt;code&gt;api/app/refresh.php&lt;/code&gt; 엔드포인트를 통해 처리됩니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  보호된 API 접근과 토큰 유효성 검증 과정
&lt;/h3&gt;

&lt;p&gt;JWT를 발급하고 나면, 앱은 이 토큰을 사용하여 회원 전용 기능과 같은 보호된 API에 접근하게 됩니다. 서버는 이러한 보호된 API 요청이 들어올 때마다 클라이언트로부터 전달받은 JWT의 유효성을 검증해야 합니다. 이 검증 과정은 보안에 있어 매우 중요한 단계이며, 토큰이 위조되지 않았는지, 만료되지는 않았는지 등을 확인합니다.&lt;/p&gt;

&lt;p&gt;일반적으로 JWT는 HTTP &lt;code&gt;Authorization&lt;/code&gt; 헤더에 &lt;code&gt;Bearer&lt;/code&gt; 스키마와 함께 전달됩니다. 서버 측 API에서는 먼저 이 헤더에서 토큰 문자열을 추출하는 작업이 필요합니다. &lt;code&gt;$_SERVER['HTTP_AUTHORIZATION']&lt;/code&gt; 변수에서 값을 읽어와 &lt;code&gt;Bearer&lt;/code&gt; 접두사를 제거하면 순수한 JWT를 얻을 수 있습니다. 만약 토큰이 없거나 형식이 올바르지 않으면 즉시 인증 실패를 처리해야 합니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// JWT 검증 예시 (실제 라이브러리 코드는 복잡함)
$token = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (strpos($token, 'Bearer ') === 0) {
    $token = substr($token, 7);
}
// if (isValidJwt($token)) { ... } else { unauthorized }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;추출된 토큰은 JWT 라이브러리를 사용하여 서명 검증, 만료 시간 확인, 페이로드 유효성 검사 등의 과정을 거칩니다. 토큰의 서명이 유효하지 않거나 이미 만료된 토큰이라면, 해당 요청은 인증되지 않은 것으로 간주하여 401 Unauthorized 응답을 반환합니다. 이 과정을 통해 오직 유효한 토큰을 가진 사용자만이 보호된 리소스에 접근할 수 있도록 보장할 수 있었지요. 이처럼 서버에서 철저하게 토큰을 검증하는 것이 시스템 전체의 보안을 유지하는 핵심이었습니다.&lt;/p&gt;

&lt;h3&gt;
  
  
  실제로 작동하는 시스템을 위한 꼼꼼한 확인
&lt;/h3&gt;

&lt;p&gt;모든 API 엔드포인트 개발과 JWT 로직 구현을 마친 후, 실제로 시스템이 제대로 작동하는지 검증하는 과정이 필요했습니다. 단순히 코드가 에러 없이 실행되는 것을 넘어, Flutter 앱에서 실제 사용자 흐름에 맞춰 로그인, 회원가입, 토큰 갱신 기능을 사용했을 때 예상대로 동작하는지 확인하는 것이 중요했습니다. 저는 단계별로 꼼꼼하게 테스트 시나리오를 만들고 하나씩 점검해 나갔습니다.&lt;/p&gt;

&lt;p&gt;먼저, 새로운 계정으로 회원가입을 시도하여 그누보드 &lt;code&gt;g5_member&lt;/code&gt; 테이블에 새로운 레코드가 성공적으로 추가되는지 확인했습니다. 다음으로, 방금 가입한 계정으로 로그인 기능을 테스트하여 유효한 JWT가 발급되고 앱으로 전달되는지 응답 값을 면밀히 살펴보았죠. 발급된 JWT는 앱의 로컬 스토리지에 잘 저장되는지, 그리고 만료 시점에 맞춰 토큰 갱신 API가 호출되어 새로운 토큰을 받아오는지도 확인했습니다.&lt;/p&gt;

&lt;p&gt;가장 중요한 검증 단계는 바로 발급된 JWT를 사용하여 다른 보호된 API(예: 회원 정보 조회 API)에 접근했을 때였습니다. 앱이 토큰을 HTTP &lt;code&gt;Authorization&lt;/code&gt; 헤더에 포함하여 요청을 보냈을 때, 서버에서 이 토큰의 유효성을 성공적으로 검증하고 올바른 데이터를 반환하는지 확인했습니다. 만료된 토큰으로 접근을 시도했을 때는 401 Unauthorized 응답이 정확히 오는지도 확인하여, 인증 시스템이 설계대로 보안적으로도 작동하는지 검증할 수 있었습니다. 이 모든 과정을 거치면서 레거시 시스템과의 연동이 성공적으로 마무리되었음을 확인할 수 있었네요.&lt;/p&gt;

&lt;h3&gt;
  
  
  정리하며
&lt;/h3&gt;

&lt;p&gt;오래된 그누보드와 현대적인 Flutter 앱을 JWT 기반으로 연동하는 작업은 분명 쉽지 않은 과정이었습니다. 특히 레거시 시스템의 특성을 이해하고 그 위에서 새로운 기능을 안정적으로 구현하는 것이 관건이었죠. 이번 경험을 통해 저는 다시 한번 '기존 시스템의 작동 원리와 의존성을 깊이 파악하는 것이 중요하다'는 교훈을 얻었습니다. 모든 개발은 새로운 것을 만드는 즐거움도 있지만, 기존의 것을 존중하고 이해하는 데서 더 큰 가치를 찾을 때도 있다는 점을 잊지 말아야겠습니다. 같은 문제를 겪는 분들께 도움이 되었기를 바랍니다.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>jwt</category>
      <category>php</category>
      <category>api</category>
    </item>
  </channel>
</rss>
