<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Pause &amp;amp; Play</title>
    <link>https://journal7218.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 25 Jul 2026 13:53:36 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>eun_ll</managingEditor>
    <image>
      <title>Pause &amp;amp; Play</title>
      <url>https://tistory1.daumcdn.net/tistory/7317195/attach/e115af09fa58443d88f9dace244c4697</url>
      <link>https://journal7218.tistory.com</link>
    </image>
    <item>
      <title>[MS AI School] 6개월 수료 &amp;mdash; 전공자의 솔직한 후기</title>
      <link>https://journal7218.tistory.com/8</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;합격 후기를 쓴 게 엊그제 같은데, 벌써 수료 후기를 쓰고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때 &quot;앞으로의 기록을 천천히 해나갈 생각이다&quot;라고 썼었는데, 열심히 달리다보니 벌써 수료를 했다는게 내심 신기하고 뿌듯하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6개월이 어땠는지, 미화 없이 솔직하게 정리해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 부트캠프 후기를 찾아보면 대부분 비전공자분들의 후기라서, 전공자 입장에서 쓴 후기는 정말 찾기 힘들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나처럼 전공자인데 부트캠프를 고민하는 사람이 있다면 이 글이 도움이 되면 좋겠다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[가장 힘들었던 것]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술적인 부분일 거라 생각했다면 아니다. 가장 힘들었던 건 6개월 동안 &lt;b&gt;매일 9시에 기상해서 6시까지 책상 앞에 앉아 있는 것&lt;/b&gt; 그 자체였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의외라 생각할 수도, 당연하다 생각할 수도 있겠지만 나에겐 너무나도 의외였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #dddddd;&quot;&gt;(학생땐 항상 그래왔기 때문에 별로 힘들지 않을거라 생각했다...)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하루 이틀은 괜찮다. 근데 이게 주 5일 &amp;times; 6개월이 되면 이야기가 달라진다. 학교처럼 공강이 있는 것도 아니고, 생각보다 체력 소모가 엄청나다. 프로젝트 기간에는 6시에 끝나도 끝난 게 아니었다ㅎㅎ..&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 지원을 고민하고 있다면, 커리큘럼 난이도보다 &lt;b&gt;이 생활 리듬을 6개월 버틸 수 있는지&lt;/b&gt;를 먼저 생각해보길 바란다. 진심이다..&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[전공자 입장에서 솔직하게 &amp;mdash; 아쉬웠던 점]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 아쉬운 것부터 말하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 도움이 안 되는 과정은 절대 아니다. 하지만 &lt;b&gt;버리는 시간이 조금 있는 것도 사실이다.&lt;/b&gt; 비전공자를 위한 완전 기초 수업 구간(파이썬 기초 같은?)이 있는데, 이미 개발을 해본 사람에게는 아는 내용을 다시 듣는 시간이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 다양한 배경의 사람들이 같이 듣는 과정이니 어쩔 수 없는 구조라는 건 이해한다. 합격 후기에서 나도 &quot;비전공자도 듣기 쉽게 완전 기초부터 설명해준다&quot;를 장점으로 꼽았었는데, 전공자에게는 그 장점이 단점이 되어버렸다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;팁을 주자면&lt;/b&gt;, 이 시간을 그냥 흘려보내지 말고 개인 공부나 프로젝트 아이디어 구상에 병행 투자하는 걸 추천한다. 버리는 시간이 '있다'는 것보다, 그 시간을 방치하면 진짜 버려진다는 게 더 중요하다. 나 같은 경우는 사실 같이 공부를 하면서 다지는 시간을 가졌지만, 다시 돌아간다면 자격증 공부를 했을거같다..(SQLD 같은 시간을 오래 투자하지 않아도 되는 것들은 이때 써도 좋을거같다.) 하지만, 미니 프로젝트 등을 진행하면서 기획을 생각해보는 것도 나중에 큰 도움이 되었다. 이 기능을 어드 기획에 써야겠다 하는 툴이 잡혀졌다. 그러니, 너무 이 시간을 버리지 말고 도움이 된다는 생각으로 임했으면 좋겠다.(강사님들에게 예의를 다하자)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[그럼에도 남은 것 1 &amp;mdash; Azure]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합격할 때 기대했던 부분이었는데, 수료하고 보니 &lt;b&gt;가장 큰 자산이 맞았다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Custom Vision, ML Designer, OpenAI, Speech, Blob Storage 같은 걸 문서로 읽는 것과 직접 리소스를 만들어서 굴려보는 건 완전히 다른 경험이다. 나는 프로젝트 하다가 DB 사양 설정을 잘못해서 하루 만에 10만 원이 청구된 적도 있는데(자세한 건 프로젝트 회고에..), 이런 것도 직접 겪어봐야 아는 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 컴퓨터 안에서만 돌아가는 코드를 짜다가 클라우드 위에서 리소스를 다루기 시작하면 &lt;b&gt;보는 눈이 확실히 넓어진다.&lt;/b&gt; 모델 정확도만 보던 눈이 배포, 확장성, 비용까지 보게 된다. 개인이 사이드 프로젝트로 클라우드를 이 정도로 써보는 건 비용 때문에 쉽지 않은데, 그 장벽을 과정이 대신 치워준다는 게 정말 크다. 나는 이번 3번의 프로젝트동안 정말 내가 하고싶었던 기획, 혹은 기술들을 원없이 사용했다. 물론 중간에 고난이 없었다면 거짓말이겠지만 이번 기회로 클라우드, 보안 등 평소에 관심갖지 않았던 부분도 한번 더 들여다 보고 관심을 갖게 되는 좋은 기회가 되었다. 특히 Azure를 원없이 사용할 수 있다는 건 정말 다른사람과는 큰 차별점이라 생각하였다. (큰 비용을 지불하지 않는 이상 사용하기 쉽지 않은건 사실이기 때문에..)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[그럼에도 남은 것 2 &amp;mdash; 사람]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째는 사람이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀에 개발 전공만 있는 게 아니다 보니, 같은 문제를 완전히 다른 각도에서 보는 순간들이 온다. 개발자끼리였으면 구현에만 초점을 맞춰 진행되었을텐데, 이걸 왜 만들어야 하지, 사용자는 어떻게 느낄까, 사용자의 흐름에서 지루하게 느끼지는 않을까 에서 시작된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 과정에서 &lt;b&gt;기획적인 시야&lt;/b&gt;가 자연스럽게 생겼다. 내가 개발자로서 생각하지 못한 부분을 생각해주는 팀원들 덕분에 기획에 대해 고려해야할 상황들이 점점 눈에 선명하게 보였다. 앞으로 기획을 할 때 어떤 식으로 해야할 지에 대한 눈을 키운거 같다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[포기하고 싶었던 순간, 그리고 1등]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간에 포기하고 싶은 순간이 없었다면 거짓말이다. 반복되는 일과에 지치고, 아는 내용을 듣는 시간에 현타가 오는 구간이 분명 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 프로젝트가 시작되면서 분위기가 완전히 바뀌었다. 배운 걸 쏟아부을 대상이 생기니까, 앉아있던 시간들이 전부 재료가 됐다. 1차 프로젝트로 화재 감지 시스템(E-gle Eye)을 만들면서 모델을 '시스템'으로 완성하는 법을 배웠고, 최종 프로젝트로 AI 영어 회화 서비스(SimSpeak)를 만들었는데 &amp;mdash;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;최종 프로젝트 1등을 할 수 있었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 말하면, 전공자 입장에서 상을 너무너무 타고 싶었다. 그런데 막상 와보니 전공자 비전공자 할 것 없이 잘하는 사람이 정말 많았다. 오히려 그 안에서 느낀 건, &lt;b&gt;다양한 분야의 전문성이 만나면 혼자서는 못 낼 더 큰 결과가 나온다&lt;/b&gt;는 거였다. 그래서인지 1차 프로젝트 때는 '상을 타고 싶다'는 마음에 가까웠다면, 마지막으로 갈수록 '이왕 하는 거 다양한 기술을 다 써보자'는 쪽으로 마음이 바뀌었다. 최종 프로젝트는 다들 이를 갈고 나오는 무대라 1등에는 운도 분명 있었겠지만 &amp;mdash; 사실 상이 아니었어도, 이 6개월의 대장정 자체가 너무 의미 있는 시간이었다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;63:1-63:204;2449-2652&quot; data-ke-size=&quot;size16&quot;&gt;수료하고 나니 &quot;안 하길 잘했다&quot;가 아니라 포기 안 하길 정말 잘했다라는 생각이 든다. 수상도 물론 좋지만, 그보다 여러 기술을 직접 써보고 보는 눈을 넓히는 것 &amp;mdash; 나는 그게 가장 중요하다고 본다. 목표가 그저 버텨서 받는 수료증이면 버리는 시간이 보이고, &lt;b&gt;자신이 정한 목표를 이뤄내겠다는 마음이면 남는 게 정말 많다.&lt;/b&gt; 같은 커리큘럼인데 그렇다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;63:1-63:204;2449-2652&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트에서 뭘 만들고 뭘 겪었는지는 따로 회고 시리즈로 정리해뒀다. 기술적인 부분이 궁금하다면 참고!&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://journal7218.tistory.com/3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[E-gle Eye 개발기 1편 &amp;mdash; 모델 신뢰도 구축기]&amp;nbsp;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://journal7218.tistory.com/4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[E-gle Eye 개발기 2편 &amp;mdash; 실시간 시스템 구축기]&amp;nbsp;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://journal7218.tistory.com/6&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[SimSpeak 개발기 1편 &amp;mdash; 아키텍처 설계기]&amp;nbsp;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://journal7218.tistory.com/7&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[SimSpeak 개발기 2편 &amp;mdash; 성능 최적화 전면전]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[그래서, 추천하냐고 묻는다면]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;추천한다.&lt;/b&gt; 다만 이런 사람에게 추천한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Azure 같은 클라우드를 실제로 다뤄보고 싶은 사람 (전공자라면 특히)&lt;/li&gt;
&lt;li&gt;혼자 코딩만 해봤고, 다양한 배경의 팀원들과 프로젝트를 처음부터 끝까지 만들어보고 싶은 사람&lt;/li&gt;
&lt;li&gt;포트폴리오에 완성된 프로젝트 + 수상 실적을 만들고 싶은 사람&lt;/li&gt;
&lt;li&gt;9 to 6 루틴을 6개월 버틸 체력이 준비된 사람&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로, 커리큘럼의 모든 시간이 새로운 지식이길 기대하는 전공자라면 기초 구간에서 답답할 수 있으니 그 부분은 감안하고 지원하길 바란다. 그리고 수료 자체가 목표라면.. 이 과정은 목표를 걸고 달리는 사람과 아닌 사람의 결과 차이가 꽤 크다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[마치며]&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버리는 시간이 있었냐고 물으면, 있었다. 그래도 다시 돌아가도 지원할 거냐고 물으면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;mdash; 당연히 그렇다. 클라우드를 보는 눈, 같이 일하는 감각, 완성된 프로젝트 세 개와 트로피 하나. 책상 앞에서 버틴 6개월치고는 꽤 괜찮은 정산이지 않나ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합격 후기 때처럼 이번에도 같은 말로 마무리하려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시라도 부트캠프를 고민하고 있는 사람이 있다면, 이 글이 도움이 되었으면 좋겠다. 6개월간의 기록은 여기까지!  &lt;/p&gt;</description>
      <category>MS ai School</category>
      <category>AI</category>
      <category>AI부트캠프</category>
      <category>It</category>
      <category>msaischool</category>
      <category>개발</category>
      <category>부트캠프</category>
      <author>eun_ll</author>
      <guid isPermaLink="true">https://journal7218.tistory.com/8</guid>
      <comments>https://journal7218.tistory.com/8#entry8comment</comments>
      <pubDate>Thu, 9 Jul 2026 05:02:21 +0900</pubDate>
    </item>
    <item>
      <title>[MS AI School 최종 프로젝트] AI 응답 14초 &amp;rarr; 1.5초, 성능 최적화 전면전 &amp;mdash; SimSpeak 개발기 (2/2)</title>
      <link>https://journal7218.tistory.com/7</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 투트랙 아키텍처로 LLM 파이프라인 병목을 제거하고, N+1 쿼리를 걷어내고, 실패 경로까지 방어한 기록. 1편에서 다룬 아키텍처와 인프라 위에서, 이번 편은 숫자로 증명하는 성능 개선을 다룬다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈 [1편 &amp;mdash; 설계 편]: &lt;a href=&quot;https://journal7218.tistory.com/6&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;배경 &amp;middot; 팀 빌딩 &amp;middot; 아키텍처 &amp;middot; 인프라 비용 최적화&lt;/a&gt;&lt;br /&gt;2편 &amp;mdash; 성능 편 (현재 글): 레이턴시 &amp;middot; N+1 &amp;middot; 음성 엔진 이원화 &amp;middot; 데이터 계약 &amp;middot; 신뢰성 설계&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 글의 핵심 성과&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;E2E 응답 지연 최대 14초 &amp;rarr; 1~2초 (약 85% 단축)&lt;/b&gt; &amp;mdash; 실시간 대화 / 정밀 평가 투트랙 분리 + 비동기 콜백(/api/chat/callback)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;N+1 제거로 p95 4.12초 &amp;rarr; 1.99초 (-52%), 쿼리 수 11개 &amp;rarr; 4개&lt;/b&gt; &amp;mdash; IN 절 일괄 조회. 대조군 계측으로 개선 원인 검증&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오디오 처리 지연 추가 1초+ 단축&lt;/b&gt; &amp;mdash; 디스크 I/O 제거, io.BytesIO RAM-to-RAM 파이프라인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;음성 파이프라인 이원화&lt;/b&gt; &amp;mdash; STT는 Whisper, 발음 채점은 Azure Speech로 전문화&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;[Trouble Shooting]&lt;/span&gt; 레이턴시 병목 &amp;mdash; 14초를 1.5초로 만든 비동기 투트랙 아키텍처&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Problem&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통합 테스트 단계에서 &lt;b&gt;대화 답변 생성 + 문법 평가를 한 번에 동기 처리하면서 E2E 응답 지연이 최대 14초&lt;/b&gt;까지 늘어나는 병목이 드러났다. '실시간 통화'가 본질인 서비스에서 14초의 딜레이는 곧 사용자들의 이탈로 일어나게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인 분해&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지연을 구성 요소별로 분해하자, &lt;b&gt;유저가 지금 당장 필요로 하는 것과 나중에 봐도 되는 것&lt;/b&gt;이 하나의 파이프라인에 묶여 있다는 구조적 문제가 확인됐다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;즉시 필요&lt;/b&gt;: 캐릭터의 대답(텍스트+음성), 행동 지문, 호감도 변동 &amp;rarr; 대화 몰입의 생명선&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지연 허용&lt;/b&gt;: CEFR 레벨 채점, 정밀 문법 교정, IPA 기준 발음 상세 진단 &amp;rarr; 리포트/오답노트에서 소비&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Solution: 투트랙 분리 + 비동기 콜백 아키텍처&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답의 성격에 따라 파이프라인을 두 트랙으로 물리적으로 분리하고, 무거운 평가 트랙은 &lt;b&gt;콜백(Callback) 기반 비동기 수신 구조&lt;/b&gt;로 백엔드를 전면 개편했다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;[User 발화] ──&amp;gt; POST /api/chat/message (Spring Boot)
                     │
     ┌───────────────┴────────────────────────────────┐
     │                                                │
 Track 1: 즉시 응답 (동기, 1~2초)             Track 2: 백그라운드 평가 (비동기)
     │                                                │
     ├─ Whisper STT (고정확도 텍스트 추출)                 ├─ Base GPT-4o: 정밀 문법 교정 + CEFR 채점
     ├─ GPT-4o-mini: 대사 + 행동 지문                    ├─ Azure Speech: 정확도&amp;middot;유창성&amp;middot;완전성&amp;middot;
     │                + affinity_delta 생성            │   억양 진단 (IPA 기준 100점)
     ├─ ElevenLabs TTS: 캐릭터 음성 합성                  │
     │                                                └─&amp;gt; POST /api/chat/callback
     └──&amp;gt; 즉시 응답 반환                                       │
          (text_content, audio_url,                         ├─ turn_id 매핑으로 세션 로그와 매치
           affinity_delta)                                  └─ Neon PostgreSQL(JSONB) 영속화&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;354&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lPsC6/dJMcaiDXIEF/jzjpHyLt5rAlq9S8PalILK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lPsC6/dJMcaiDXIEF/jzjpHyLt5rAlq9S8PalILK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lPsC6/dJMcaiDXIEF/jzjpHyLt5rAlq9S8PalILK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlPsC6%2FdJMcaiDXIEF%2FjzjpHyLt5rAlq9S8PalILK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;354&quot; height=&quot;254&quot; data-origin-width=&quot;354&quot; data-origin-height=&quot;254&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설계 포인트&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;turn_id 기반 지연 결합(Late Binding)&lt;/b&gt;: 평가 결과가 늦게 도착해도 turn_id로 정확히 해당 대화 턴에 꽂힌다. 유저가 기다리는 것은 Track 1뿐이고, Track 2는 보이지 않는 곳에서 처리된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모델 이원화의 비용 효과&lt;/b&gt;: 매 턴 호출되는 대화 트랙은 저비용&amp;middot;고속의 GPT-4o-mini, 정확도가 중요한 평가 트랙만 Base GPT-4o &amp;mdash; 레이턴시와 토큰 비용을 동시에 최적화하였다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프론트엔드 분할 수신&lt;/b&gt;: 1차 응답 수신 즉시 음성 재생 + 호감도 애니메이션을 기동하고, 2차 피드백 API를 chain 호출. 오답노트 북마크에는 Optimistic Update(pendingUnstar)를 적용해 체감 지연을 제로화했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가 최적화: 디스크 I/O 전면 제거 (io.BytesIO)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저 음성과 TTS 오디오를 파일 시스템에 임시 저장 후 재독취하던 로직이 &lt;b&gt;1초 이상의 물리적 지연&lt;/b&gt;을 유발하고 있었다. 임시 파일 경유를 차단하고 io.BytesIO 메모리 버퍼로 오디오 패킷을 &lt;b&gt;RAM-to-RAM으로 직결&lt;/b&gt;하여 해당 지연을 제거했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[Before]  STT 결과 ──&amp;gt; 디스크 임시 파일 write ──&amp;gt; read ──&amp;gt; TTS 전달   (+1초&amp;uarr;)
[After]   STT 결과 ──&amp;gt; io.BytesIO (RAM) ──────────────&amp;gt; TTS 전달   (물리 I/O 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;E2E 응답 지연: 최대 14초 &amp;rarr; 1~2초 (약 85% 단축) 정밀 평가 데이터는 품질 저하 없이 백그라운드에서 100% 수집&amp;middot;영속화&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;[Trouble Shooting]&lt;/span&gt; N+1 쿼리 제거 &amp;mdash; p95 4.12초 &amp;rarr; 1.99초 (-52%)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Problem&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐릭터&amp;middot;스테이지 현황 조회(/characters/status) 요청 1건에 대해 Hibernate 로그를 확인하니, &lt;b&gt;스테이지마다 where stage_id = ? 쿼리가 반복 실행되어 요청당 총 11개의 쿼리&lt;/b&gt;가 나가고 있었다. 전형적인 N+1 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 DB였다면 티가 덜 났겠지만, 우리 DB는 1편에서 선택한 &lt;b&gt;원격 서버리스(Neon)&lt;/b&gt; &amp;mdash; 쿼리 1개당 네트워크 왕복(RTT) 비용이 그대로 누적되어 p95 응답시간이 4.12초까지 치솟았다. 서버리스 전환으로 비용을 잡은 대가가 여기서 청구된 셈이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;before.png&quot; data-origin-width=&quot;1777&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IdAHh/dJMcaiDXIEX/mmJD2Jk4yU4hdPNlkqnNi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IdAHh/dJMcaiDXIEX/mmJD2Jk4yU4hdPNlkqnNi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IdAHh/dJMcaiDXIEX/mmJD2Jk4yU4hdPNlkqnNi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIdAHh%2FdJMcaiDXIEX%2FmmJD2Jk4yU4hdPNlkqnNi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1777&quot; height=&quot;278&quot; data-filename=&quot;before.png&quot; data-origin-width=&quot;1777&quot; data-origin-height=&quot;278&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Solution: IN 절 일괄 조회&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스테이지마다 따로 묻던 진행도를, 필요한 식별자를 모아 &lt;b&gt;where stage_id in ( &amp;hellip; ) 한 번으로 일괄 조회&lt;/b&gt;하도록 리팩터링했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[Before]  요청 1건 ──&amp;gt; SQL &amp;times; 11  (스테이지마다 개별 조회, RTT 누적)
[After]   요청 1건 ──&amp;gt; SQL &amp;times; 4   (IN 절 일괄 조회)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;After.png&quot; data-origin-width=&quot;1595&quot; data-origin-height=&quot;102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NO2Ue/dJMcafAtmbU/RlT9jzg3Bvfp8eTD6guRIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NO2Ue/dJMcafAtmbU/RlT9jzg3Bvfp8eTD6guRIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NO2Ue/dJMcafAtmbU/RlT9jzg3Bvfp8eTD6guRIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNO2Ue%2FdJMcafAtmbU%2FRlT9jzg3Bvfp8eTD6guRIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1595&quot; height=&quot;102&quot; data-filename=&quot;After.png&quot; data-origin-width=&quot;1595&quot; data-origin-height=&quot;102&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&amp;nbsp;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;요청당 쿼리 수&lt;/td&gt;
&lt;td&gt;11개&lt;/td&gt;
&lt;td&gt;4개&lt;/td&gt;
&lt;td&gt;&lt;b&gt;-64%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95 응답시간&lt;/td&gt;
&lt;td&gt;4.12초&lt;/td&gt;
&lt;td&gt;1.99초&lt;/td&gt;
&lt;td&gt;&lt;b&gt;-52%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대조군 (미변경 엔드포인트)&lt;/td&gt;
&lt;td&gt;0.85초&lt;/td&gt;
&lt;td&gt;0.82초&lt;/td&gt;
&lt;td&gt;변화 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 것은 마지막 행이다. 같은 기간 &lt;b&gt;변경하지 않은 대조군 엔드포인트의 응답시간은 0.85초 &amp;rarr; 0.82초로 사실상 동일&lt;/b&gt;했다. 즉 개선 효과는 네트워크나 DB 웜업 같은 환경 요인이 아니라 &lt;b&gt;쿼리 수 감소 그 자체&lt;/b&gt;에서 왔음을 계측으로 검증했다. 최적화 주장에는 대조군이 있어야 제대로 비교가 된다는 사실을 깨달았다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;[Trouble Shooting]&lt;/span&gt; 음성 파이프라인 이원화 &amp;mdash; STT는 Whisper, 발음 채점은 Azure Speech&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Problem&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 Azure Speech 하나로 STT와 발음 채점을 모두 처리했다. 그러나 &lt;b&gt;한국어가 혼용되거나 발음이 부정확한 발화에서 텍스트 추출 정확도가 크게 떨어지는&lt;/b&gt; 문제가 확인됐다. 콩글리시 화자를 타겟으로 하는 서비스에서 이는 치명적이다 &amp;mdash; 텍스트를 잘못 추출하면 이후의 대화&amp;middot;교정 파이프라인 전체가 오염된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Solution: 엔진별 전문화 (Dual Voice Engine)&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Azure OpenAI Whisper&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;STT (텍스트 추출) 전담&lt;/td&gt;
&lt;td&gt;한글 혼용&amp;middot;부정확 발음에서도 높은 텍스트 추출 정확도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Azure Speech SDK&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;발음 채점 전담&lt;/td&gt;
&lt;td&gt;정확도&amp;middot;유창성&amp;middot;완전성&amp;middot;억양 4개 축, IPA 기준 100점 정밀 진단&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 엔진은 FastAPI 오케스트레이터에서 &lt;b&gt;1단계 병렬 분석&lt;/b&gt;으로 동시 기동되며, 2단계에서 GPT-4o가 응답과 함께 &lt;b&gt;75점 미만 단어에 대한 IPA 발음 가이드&lt;/b&gt;를 생성해 반환한다. 적재된 발음 점수(accuracy, fluency, completeness, prosody)와 단어별 상세(word_details_json)는 리포트 화면의 레이더 차트로 재사용된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 데이터 모델 &amp;amp; 저장 계층 &amp;mdash; 바이트 단위까지 통제하는 데이터 계약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1인 백엔드 체제에서 3개 파트의 병렬 개발을 성립시킨 핵심은 &lt;b&gt;초기 데이터 계약의 엄격한 표준화&lt;/b&gt;였다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vJQyG/dJMcacwYG1I/Cca8zeGTkwQNQkYi8BApDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vJQyG/dJMcacwYG1I/Cca8zeGTkwQNQkYi8BApDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vJQyG/dJMcacwYG1I/Cca8zeGTkwQNQkYi8BApDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvJQyG%2FdJMcacwYG1I%2FCca8zeGTkwQNQkYi8BApDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;610&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;610&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 대화 로그 중심 설계 (chat_log)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 대화 턴은 chat_log 테이블을 허브로 적재되며, 무거운 분석 데이터는 위성 테이블로 분리했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;chat_log (
  id             BIGINT PK,
  session_id     VARCHAR,
  turn_count     INT,
  role           ChatRoleType,        -- ENUM (user / assistant)
  input_type     ChatInputType,       -- ENUM (text / voice)
  text_content   TEXT,                -- 대사
  audio_url      VARCHAR,             -- Blob Storage 경로만 매핑
  grammar_feedback TEXT,
  is_penalty     BOOLEAN,
  affinity_delta INT,                 -- 호감도 변동량
  penalty_reason PenaltyReasonType    -- ENUM
)

-- 위성 테이블 (chat_log_id FK로 지연 결합)
pronunciation_evaluations ( accuracy, fluency, completeness, prosody, word_details_json )
corrections               ( original_sentence, corrected_sentence, corrections_json, translation, is_reviewed )
raw_ai_responses          ( response_data JSONB )   -- AI 원본 응답 전문 보존
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 Track 2의 콜백 데이터가 늦게 도착해도 chat_log의 해당 턴에 FK로 정확히 결합되고, raw_ai_responses에 원본 응답 전문을 JSONB로 보존해 디버깅&amp;middot;재처리 경로를 확보했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. 저장 계층 최적화 3원칙&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;정수형 식별자&lt;/b&gt;: user_id, stage_id 등 마스터 데이터를 무작위 문자열 대신 정수(BIGINT) 매핑 &amp;rarr; 인덱스 크기 축소, 원격 DB 조회 성능 개선&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태값 ENUM화&lt;/b&gt;: GenderType, CefrLevelType, StageType, ChatRoleType, PenaltyReasonType 등 반복 적재되는 상태값을 ENUM으로 코드화 &amp;rarr; 수 바이트 수준 압축 + 조건절 검색 속도 확보&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대용량 오디오의 인프라 이원화&lt;/b&gt;: mp3/wav를 DB에 바이너리로 넣지 않고 &lt;b&gt;Azure Blob Storage 적재 + DB에는 audio_url 경로만 매핑&lt;/b&gt; &amp;rarr; DB 저장 비용과 IOPS 리스크 격리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. &quot;AI 호출 없는&quot; 리포트 서빙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리포트&amp;middot;복습 화면은 조회 경로를 둘로 분리했다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;경로 A (세션 종료 시 1회):  로그&amp;middot;점수 취합 ──&amp;gt; GPT-4o CEFR 채점 ──&amp;gt; 5각 레이더 + A1~C2 등급
경로 B (평상시 오답노트):   Neon JSONB 캐시 단순 조회 ──&amp;gt; AI 호출 0회, 딜레이 없이 즉시 반환
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대화 중 Track 2가 미리 적재해 둔 교정&amp;middot;점수 데이터를 경로 B에서 그대로 재사용하므로, 복습 화면은 &lt;b&gt;토큰 비용 0원 + 즉시 응답&lt;/b&gt;으로 서빙된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-4. LLM 계층의 토큰&amp;middot;포맷 통제&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;슬라이딩 윈도우 + 요약 엔진&lt;/b&gt;: 멀티턴 기억을 위한 과거 대화 주입 시, 무제한 누적으로 인한 토큰 폭증을 방지하고 필수 컨텍스트만 콤팩트하게 적재&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프롬프트 Skeleton화&lt;/b&gt;: 출력 지침을 장황한 자연어 대신 프롬프트 말미 [STRICT OUTPUT FORMAT] 구역에 &lt;b&gt;한 줄짜리 순수 JSON 샘플&lt;/b&gt;로 제공 &amp;rarr; 포맷 파손율 감소 + 지시 토큰 최소화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;세이프티 가드레일&lt;/b&gt;: 욕설&amp;middot;탈옥(Jailbreak) 시도를 차단하는 안전망 레이어를 오케스트레이터 파이프라인에 단일 모듈로 통합&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 신뢰성 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데모가 아닌 서비스라면, 행복 경로(happy path)보다 실패 경로가 품질을 결정한다. 계층별로 방어선을 세웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;백엔드 / AI 파이프라인&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;지수 백오프 재시도&lt;/b&gt;: AI API 실패 구간 진입 시 지수적 지연 기반 자동 재시도로 일시 장애를 흡수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;폴백 메시지&lt;/b&gt;: 서버 장애 시 기본 인터랙션 메시지로 대화 흐름 유지 (침묵 대신 안내)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FALLBACK_QUESTIONS&lt;/b&gt;: 문항 로딩 오류 시 사전 정의된 6문항으로 전환하여 레벨테스트 플로우 보존&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 원자성&lt;/b&gt;: DB 쓰기 에러로 인한 데이터 유실이 없도록 원자적 저장 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;프론트엔드&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;isMounted 가드&lt;/b&gt;: 화면 이탈 후 늦게 도착한 응답이 언마운트된 컴포넌트를 갱신하며 발생하는 간헐적 크래시를 useEffect cleanup으로 차단&lt;/li&gt;
&lt;li&gt;&lt;b&gt;403 에러 삼원인 분해&lt;/b&gt;: 하나의 403 뒤에 숨어 있던 세 가지 원인 &amp;mdash; ① 빈 body 응답의 JSON 파싱 실패 &amp;rarr; HTTP status별 에러 메시지 매핑, ② fallback 문항 ID의 서버 제출 &amp;rarr; 제출 자체 차단, ③ answerType 대소문자 불일치 &amp;rarr; multiple_choice 소문자 통일 &amp;mdash; 을 각각 분리 해결&lt;/li&gt;
&lt;li&gt;&lt;b&gt;빈 데이터 방어&lt;/b&gt;: 빈 배열 접근 시 CHARACTER_DATA[0] 폴백 + 비치명 오류는 console.warn으로 강등&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네이티브 의존성 정합화&lt;/b&gt;: Gradle 9.0 비호환으로 빌드가 깨지던 음성 라이브러리를 expo-av로 교체해 Expo 공식 빌드 플로우 안에서 해결&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;관측&amp;middot;보안&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Azure App Insights&lt;/b&gt;: 앱 진입&amp;middot;종료 등 이벤트를 실시간 추적하여 이탈률 분석 기반 마련. 전체 대화 로그(발화&amp;middot;응답&amp;middot;점수)는 세션 단위로 DB에 보존해 문제 발생 시 추적 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BCrypt 단방향 해시&lt;/b&gt;: 비밀번호 평문 보관 금지, 보안 설정은 코드와 분리 관리. 약관&amp;middot;민감정보&amp;middot;마케팅 수신은 선택적 동의로 분리 설계&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 마치며 (Retrospective)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5주라는 일정과 백엔드 1인이라는 제약 속에서, 이번 프로젝트가 남긴 인사이트는 네 가지로 정리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;b&gt;첫째, 레이턴시는 최적화가 아니라 '분류'의 문제다.&lt;/b&gt; &lt;/span&gt;14초 병목의 해법은 개별 연산의 가속이 아니라, 응답을 '즉시 필요한 것'과 '지연 허용 가능한 것'으로 분류하고 아키텍처 레벨에서 분리하는 것이었다. 동기/비동기의 경계를 어디에 긋느냐가 유저 체감 성능을 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;b&gt;둘째, 성능 주장에는 대조군이 필요하다.&lt;/b&gt;&lt;/span&gt; N+1 개선에서 p95가 4.12초 &amp;rarr; 1.99초로 떨어진 것보다 중요한 발견은, 미변경 엔드포인트가 0.85초 &amp;rarr; 0.82초로 그대로였다는 사실이다. 대조군 없는 벤치마크는 환경 노이즈와 개선 효과를 구분하지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;b&gt;셋째, 인프라 선택은 트레이드오프의 연쇄다.&lt;/b&gt; &lt;/span&gt;서버리스 전환으로 비용을 잡았지만(1편), 그 대가로 커진 쿼리당 RTT가 N+1 문제를 증폭시켰다(2편). 완벽한 선택은 없고, 선택의 부작용을 예측하고 후속 대응하는 것이 아키텍처 운영이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;b&gt;넷째, 소수 인원 체제의 성패는 초기 계약 설계에서 갈린다.&lt;/b&gt;&lt;/span&gt; ERD&amp;middot;API 명세&amp;middot;ENUM 체계를 1주차에 고정한 덕분에 FE 3 / AI 3 / BE 1이라는 비대칭 팀이 병렬 개발 체제로 작동했고, 늦게 도착하는 비동기 데이터조차 turn_id 하나로 제자리를 찾아갔다. 백엔드 엔지니어의 역할은 코드 작성 이전에 &lt;b&gt;팀 전체가 의존하는 데이터 계약의 아키텍트&lt;/b&gt;라는 점을 체감했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술을 많이 쓰는 것보다 &lt;b&gt;유저가 기술의 존재를 잊게 만드는 것&lt;/b&gt;이 좋은 아키텍처라는 확신을 얻었다. 유저는 1.5초 만에 돌아오는 캐릭터의 목소리에 몰입할 뿐, 그 뒤의 투트랙 파이프라인과 콜백 영속화, IN 절로 묶인 쿼리를 인지하지 못한다 &amp;mdash; 그리고 그것이 우리가 추구한 서비스에 맞았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://journal7218.tistory.com/6&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[1편 &amp;mdash; 설계 편 보러가기]&lt;/a&gt;&lt;/blockquote&gt;</description>
      <category>MS ai School</category>
      <author>eun_ll</author>
      <guid isPermaLink="true">https://journal7218.tistory.com/7</guid>
      <comments>https://journal7218.tistory.com/7#entry7comment</comments>
      <pubDate>Tue, 7 Jul 2026 02:30:53 +0900</pubDate>
    </item>
    <item>
      <title>[MS AI School 최종 프로젝트] 하루 만에 DB 비용 10만 원이 청구됐다 &amp;mdash; AI 회화 서비스 SimSpeak 개발기 (1/2)</title>
      <link>https://journal7218.tistory.com/6</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Native + Spring Boot + FastAPI 3-Tier로 설계한 AI 연애 시뮬레이션 영어 회화 서비스. 1편에서는 문제 정의와 전략적 팀 빌딩, 시스템 아키텍처, 그리고 관리형 MySQL에서 서버리스 PostgreSQL로 전환하게 된 비용 사고를 다룬다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈 1편 &amp;mdash; 설계 편 (현재 글): 배경 &amp;middot; 팀 빌딩 &amp;middot; 아키텍처 &amp;middot; 인프라 비용 최적화 &lt;br /&gt;[2편 &amp;mdash; 성능 편]: &lt;a href=&quot;https://journal7218.tistory.com/7&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AI 응답 14초 &amp;rarr; 1.5초, N+1 제거 p95 4.12초 &amp;rarr; 1.99초, 신뢰성 설계&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 개요&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 7.90698%;&quot;&gt;&lt;b&gt;서비스&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 91.9767%;&quot;&gt;SimSpeak &amp;mdash; AI 연애 시뮬레이션 포맷의 영어 회화 학습 앱 (음성 대화 + 발음 진단 + 문법 교정 + 오답노트)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 7.90698%;&quot;&gt;&lt;b&gt;기간&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 91.9767%;&quot;&gt;2026.05.21 ~ 2026.06.25 (약 5주, 9개 마일스톤)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 7.90698%;&quot;&gt;&lt;b&gt;팀 구성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 91.9767%;&quot;&gt;7명 (AI 3 / Backend 1 / Frontend&amp;middot;디자인 3) &amp;mdash; 본인: Backend 리드 + 앱 디자인 + AI 보조&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 7.90698%;&quot;&gt;&lt;b&gt;스택&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 91.9767%;&quot;&gt;React Native(Expo, Zustand) &amp;middot; Spring Boot(Security, WebClient) &amp;middot; FastAPI &amp;middot; Azure OpenAI(GPT-4o/4o-mini, Whisper) &amp;middot; Azure Speech &amp;middot; ElevenLabs &amp;middot; Neon PostgreSQL &amp;middot; Azure Blob Storage &amp;middot; Docker &amp;middot; Railway &amp;middot; k6 &amp;middot; App Insights&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 글(1편)의 핵심&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유지율 1.76% 시장에서 '채점' 대신 '호감도'를 선택한 문제 정의&lt;/li&gt;
&lt;li&gt;FE 3 / AI 3 / BE 1 &amp;mdash; 비대칭 팀 빌딩의 전략적 근거&lt;/li&gt;
&lt;li&gt;게이트웨이(Spring)와 오케스트레이터(FastAPI)를 분리한 4계층 아키텍처&lt;/li&gt;
&lt;li&gt;DB 고정 비용 일 ~10만 원 &amp;rarr; 사실상 0원 (서버리스 전환)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 프로젝트 배경: 교육 앱에서 7일 안에 98%가 떠나는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외국어 학습 앱 시장은 공급 과잉이지만, &lt;b&gt;출시 후 7일 내 초기 유지율은 1.76%&lt;/b&gt; 수준에 불과하다(모바일 앱 리텐션 리서치 기준). 원인을 기존 서비스의 인터랙션 모델에서 찾았다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;기존 교육 앱의 이탈 루프
유저를 '학생' 취급 ──&amp;gt; 틀리면 빨간 줄&amp;middot;정색 ──&amp;gt; 대화 흐름 단절 ──&amp;gt; 앱 삭제
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SimSpeak은 이 구조를 뒤집었다. &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;채점하는 선생님 대신 매력적인 AI 페르소나를 전면에 배치&lt;/b&gt;&lt;/span&gt;하고, 유저가 고급 표현을 쓸수록 캐릭터의 호감도가 오르고 더 달달한 답변&amp;middot;음성이 돌아오는 &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;도파민 기반 보상 루프&lt;/b&gt;를 설계&lt;/span&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게이미피케이션의 효과는 데이터로도 뒷받침된다 &amp;mdash; 게임화 결합 학습은 일반 학습 대비 &lt;b&gt;코스 완료율 18% &amp;rarr; 72%, 30일 지식 유지율 23% &amp;rarr; 68%&lt;/b&gt;, 주 평균 체류 시간도 15.6시간 &amp;rarr; 25.3시간으로 60% 이상 증가하는 것으로 조사됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교정과 채점은 사라진 것이 아니라 &lt;b&gt;백그라운드 파이프라인으로 숨겼다&lt;/b&gt;(2편에서 투트랙 아키텍처를 자세히 설명할 예정이다). 핵심 차별점은 LLM의 문맥 이해력을 교육 피드백에 직결시킨 것이다. 단순 키워드 매칭으로는 &quot;notebook&quot;이 '공책'인지 콩글리시 '노트북(laptop)'인지 판별할 수 없다. LLM이 문맥을 판별해 오용 시에만 유쾌하게 받아치며 호감도 페널티(-1)와 오답노트 적재를 수행한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 전략적 팀 빌딩:&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7명을 통상적인 균등 배분 대신 &lt;b&gt;FE&amp;middot;디자인 3 / AI 3 / BE 1&lt;/b&gt;로 배치했다. 근거는 서비스의 가치 사슬 분석이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.0232%;&quot;&gt;&lt;b&gt;Frontend + 캐릭터 디자인&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 1.16279%;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 81.7442%;&quot;&gt;React Native 하이브리드 앱은 인터랙션 퀄리티가 낮으면 '양산형 웹뷰 앱'으로 전락. 첫 화면부터 캐릭터 몰입을 만들어야 하는 서비스 특성상 비주얼&amp;middot;애니메이션 전담 인력 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.0232%;&quot;&gt;&lt;b&gt;AI&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 1.16279%;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 81.7442%;&quot;&gt;페르소나 일관성, 멀티턴 대화 기억, 흐름을 끊지 않는 정밀 피드백(문법&amp;middot;발음&amp;middot;콩글리시) &amp;mdash; 세 가지 고난도 과제의 병렬 해결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.0232%;&quot;&gt;&lt;b&gt;Backend&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 1.16279%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 81.7442%;&quot;&gt;인증&amp;middot;세션&amp;middot;영속화는 표준 패턴으로 압축 가능. 단, &lt;b&gt;초기에 데이터 계약(Contract)과 아키텍처를 완벽히 고정&lt;/b&gt;해야 한다는 전제가 붙음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;백엔드를 1명으로 한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 1인 체제의 리스크를 상쇄하기 위해 1주차에 &lt;b&gt;ERD와 API 명세를 선행 확정&lt;/b&gt;하여, 이후 3개 파트가 계약 기반으로 독립 개발할 수 있는 구조를 만들었다. 데이터 계약의 구체적인 설계(ENUM 체계, turn_id 지연 결합, JSONB 캐시)는 2편에서 상세히 다룬다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;983&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnEjlX/dJMcafmYDN2/dtDgKhdeIKk1T3LV11Nknk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnEjlX/dJMcafmYDN2/dtDgKhdeIKk1T3LV11Nknk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnEjlX/dJMcafmYDN2/dtDgKhdeIKk1T3LV11Nknk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnEjlX%2FdJMcafmYDN2%2FdtDgKhdeIKk1T3LV11Nknk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;541&quot; height=&quot;415&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;983&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/03lTN/dJMcaglNPP3/LQNEwUgSkmuEOHULhrzfU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/03lTN/dJMcaglNPP3/LQNEwUgSkmuEOHULhrzfU0/img.png&quot; data-alt=&quot;화면 플로우와 와이어프레임을 바탕으로 초기에 erd를 작성했다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/03lTN/dJMcaglNPP3/LQNEwUgSkmuEOHULhrzfU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F03lTN%2FdJMcaglNPP3%2FLQNEwUgSkmuEOHULhrzfU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;646&quot; height=&quot;308&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;610&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;화면 플로우와 와이어프레임을 바탕으로 초기에 erd를 작성했다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 시스템 아키텍처 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 시스템은 &lt;b&gt;역할이 명확히 분리된 4계층&lt;/b&gt;으로 구성했다. Spring Boot는 인증&amp;middot;라우팅만 담당하는 얇은 게이트웨이로, FastAPI는 AI 호출&amp;middot;조립을 전담하는 오케스트레이터로 역할을 고정하여 장애 격리와 독립 배포를 확보했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-07 오전 2.18.05.png&quot; data-origin-width=&quot;879&quot; data-origin-height=&quot;457&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HEe6W/dJMcahyhVBg/5WXaPL1T1xvIbRF5qdAAKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HEe6W/dJMcahyhVBg/5WXaPL1T1xvIbRF5qdAAKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HEe6W/dJMcahyhVBg/5WXaPL1T1xvIbRF5qdAAKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHEe6W%2FdJMcahyhVBg%2F5WXaPL1T1xvIbRF5qdAAKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;879&quot; height=&quot;457&quot; data-filename=&quot;스크린샷 2026-07-07 오전 2.18.05.png&quot; data-origin-width=&quot;879&quot; data-origin-height=&quot;457&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qfofu/dJMcacDNtJK/sGppfaX5o2uztn2MeIriIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qfofu/dJMcacDNtJK/sGppfaX5o2uztn2MeIriIk/img.png&quot; data-alt=&quot;주요 api만 정리하여 올린다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qfofu/dJMcacDNtJK/sGppfaX5o2uztn2MeIriIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqfofu%2FdJMcacDNtJK%2FsGppfaX5o2uztn2MeIriIk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;350&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;주요 api만 정리하여 올린다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드가 노출하는 대화 관련 API 계약은 1주차에 다음과 같이 고정했다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;/api/chat/sessions&lt;/td&gt;
&lt;td&gt;세션 생성 &amp;middot; 초기 대화 수신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;/api/chat/message&lt;/td&gt;
&lt;td&gt;유저 메시지 전송 및 즉시 응답&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;/api/chat/callback&lt;/td&gt;
&lt;td&gt;AI 비동기 평가 결과 콜백 수신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;/api/chat/sessions/{session_id}/logs&lt;/td&gt;
&lt;td&gt;메시지 히스토리 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;/api/chat/sessions/active&lt;/td&gt;
&lt;td&gt;이어하기 세션 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/message와 /callback이 분리된 이유 &amp;mdash; 즉, 왜 하나의 요청을 두 개의 응답 경로로 쪼갰는지 &amp;mdash; 가 이 프로젝트 성능 개선의 핵심인데, 이는 2편의 투트랙 아키텍처에서 다룬다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;[Trouble Shooting]&lt;/span&gt; 인프라 및 비용 최적화 &amp;mdash; 관리형 MySQL에서 서버리스 PostgreSQL로&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Problem&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 백엔드 DB로 Azure Database for MySQL(Flexible Server)을 프로비저닝했다. 데이터 세팅 완료 후 24시간 만에 &lt;b&gt;약 10만 원의 비용이 청구&lt;/b&gt;되었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인 분석&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Flexible Server 생성 시 &lt;b&gt;컴퓨팅 계층(Compute Tier)을 기본값으로 두고 생성&lt;/b&gt;한 것이 직접 원인. 프로토타입 워크로드에 상시 가동형 고사양 인스턴스가 할당되어 있었다.&lt;/li&gt;
&lt;li&gt;근본 문제는 과금 모델과 트래픽 패턴의 불일치. 개발 단계 트래픽은 간헐적(bursty)인데, 상시 가동형 관리형 DB는 &lt;b&gt;유휴 시간에도 고정 비용이 발생&lt;/b&gt;한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;교훈&lt;/b&gt;: 클라우드 관리형 DB 프로비저닝 시 기본값을 신뢰하지 말 것. 컴퓨팅 계층과 과금 모델을 워크로드 패턴에 맞춰 명시적으로 선택해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Solution: &lt;b&gt;요구사항 기반 DB 재선정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용 사고를 계기로 DB 요구사항을 처음부터 재정의했다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;대화 로그&amp;middot;AI 교정 데이터 등 &lt;b&gt;스키마 가변성이 높은 데이터&lt;/b&gt; 적재&lt;/td&gt;
&lt;td&gt;PostgreSQL &lt;b&gt;JSONB&lt;/b&gt; &amp;mdash; 인덱싱 가능한 반정형 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;개발/데모 단계의 &lt;b&gt;간헐적 트래픽&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;쿼리 발생 시에만 컴퓨팅을 사용하는 &lt;b&gt;서버리스&lt;/b&gt; 과금 모델&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1인 백엔드 체제에서 &lt;b&gt;배포 오버헤드 최소화&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;GitHub 연동 즉시 빌드&amp;middot;배포되는 PaaS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;최종 선택: Neon DB (Serverless PostgreSQL) + Docker + Railway&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[Before]  Azure MySQL Flexible Server (상시 가동)
          └─&amp;gt; 유휴 시간에도 과금 ──&amp;gt; 일 ~10만 원

[After]   Neon Serverless PostgreSQL + GitHub ─&amp;gt; Docker ─&amp;gt; Railway 자동 배포
          └─&amp;gt; 쿼리 시에만 컴퓨팅 과금 ──&amp;gt; 고정 비용 사실상 0원
              + JSONB로 반정형 데이터 네이티브 지원
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 선택에는 복선이 하나 깔려 있었다. &lt;b&gt;원격 서버리스 DB는 쿼리 1개당 네트워크 왕복(RTT) 비용이 로컬 DB보다 훨씬 크다.&lt;/b&gt; 이 특성이 이후 N+1 쿼리 문제와 만나 p95 응답시간을 4.12초까지 밀어올리게 되는 문제가 생겼다. &amp;mdash; 이 해결방안은 2편에서 이어진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2편(성능 편)에서 다루는 내용:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;AI 응답 14초 &amp;rarr; 1.5초&lt;/b&gt;: 비동기 콜백 기반 투트랙 아키텍처와 io.BytesIO RAM-to-RAM 파이프라인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;N+1 제거로 p95 4.12초 &amp;rarr; 1.99초 (-52%)&lt;/b&gt;: 그리고 &quot;빨라진 게 진짜 내 덕분인지&quot; 대조군으로 검증한 이야기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;STT는 Whisper, 발음 채점은 Azure Speech&lt;/b&gt;: 음성 엔진 이원화 결정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 계약 설계&lt;/b&gt;: chat_log 허브 스키마, ENUM 체계, AI 호출 0회 리포트 서빙&lt;/li&gt;
&lt;li&gt;&lt;b&gt;신뢰성 설계&lt;/b&gt;: 지수 백오프, 폴백, 403 삼원인 분해, isMounted 가드&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://journal7218.tistory.com/7&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[2편 &amp;mdash; 성능 편 바로가기]&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>MS ai School</category>
      <author>eun_ll</author>
      <guid isPermaLink="true">https://journal7218.tistory.com/6</guid>
      <comments>https://journal7218.tistory.com/6#entry6comment</comments>
      <pubDate>Tue, 7 Jul 2026 02:30:43 +0900</pubDate>
    </item>
    <item>
      <title>[FastAPI] 카카오 소셜 로그인 백엔드 구현하기 (OAuth 2.0 &amp;amp; Azure 배포)</title>
      <link>https://journal7218.tistory.com/5</link>
      <description>&lt;div id=&quot;model-response-message-contentr_0cbc7c5b4ecc4e5e&quot;&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;최근 프로젝트를 진행하며 FastAPI 환경에서 카카오 소셜 로그인을 구현할 기회를 가지게 되었다. 소셜 로그인 구현이 처음 해보았을 때, 좀 헷갈렸어서... 이렇게 정리를 하는 시간을 가지게 되었다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;구현 성공을 기념으로 어떻게 진행하면 좋을지 정리해보도록 하겠다.&lt;/p&gt;
&lt;h2 data-path-to-node=&quot;4&quot; data-ke-size=&quot;size26&quot;&gt;1. 전체적인 로그인 흐름 이해하기&lt;/h2&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;본격적으로 코드를 작성하기 전에 카카오 로그인 API의 인증 흐름을 이해하는 게 중요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,0&quot;&gt;Frontend&lt;/b&gt;: 사용자가 '카카오 로그인' 버튼 클릭 &amp;rarr; 카카오 인가 코드 요청&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,0&quot;&gt;Kakao API&lt;/b&gt;: 사용자 인증 후 인가 코드(Authorization Code)를 Backend로 전달&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,2,0&quot;&gt;Backend&lt;/b&gt;: 전달받은 인가 코드로 카카오에 &lt;b data-index-in-node=&quot;26&quot; data-path-to-node=&quot;6,2,0&quot;&gt;Access Token&lt;/b&gt; 요청&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,3,0&quot;&gt;Backend&lt;/b&gt;: 토큰을 이용해 사용자 정보(이메일, 닉네임 등) 획득&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,4,0&quot;&gt;Backend&lt;/b&gt;: 우리 서비스의 자체 &lt;b data-index-in-node=&quot;20&quot; data-path-to-node=&quot;6,4,0&quot;&gt;JWT 발급&lt;/b&gt; 또는 로그인 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-path-to-node=&quot;7&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p data-path-to-node=&quot;7,0&quot; data-ke-size=&quot;size16&quot;&gt;Note: 나 같은 경우는 프론트에서 인가코드를 받아 백엔드로 전달해주는 흐름이 아닌 프론트에서 엑세스 토큰을 발급받을 줄 알아서 처음 프론트와의 연결에서 살짝 문제가 생겼었다.. 그래서 개발 시작 전 &lt;b data-index-in-node=&quot;19&quot; data-path-to-node=&quot;9,0&quot;&gt;Swagger&lt;/b&gt;나 &lt;b data-index-in-node=&quot;28&quot; data-path-to-node=&quot;9,0&quot;&gt;Postman&lt;/b&gt;을 활용해 요청(Request)과 응답(Response)의 데이터 타입, 필수값 여부, 에러 코드 규격을 먼저 확정하는 'API-First' 접근 방식을 지향하게 되었다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size23&quot;&gt;JWT(JSON Web Token)란?&lt;/h3&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;로그인에 성공한 사용자에게 &quot;당신은 인증된 사용자입니다&quot;라고 증명서를 끊어준다고 생각하면 이해하기 쉽다. 토큰을 받아오면 그 토큰의 유효성만 검증하면 되는 형식이기 때문에 서버 부하를 줄일 수 있어 가장 많이 쓰인다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;10&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,0,0&quot;&gt;Header&lt;/b&gt;: 토큰의 타입(JWT)과 사용 중인 해시 알고리즘 정보.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,0&quot;&gt;Payload&lt;/b&gt;: 사용자 ID, 토큰 만료 시간 등 실제 전달하려는 정보(Claim).&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,2,0&quot;&gt;Signature&lt;/b&gt;: 서버만 알고 있는 'Secret Key'로 암호화된 서명. (변조 확인용)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-path-to-node=&quot;11&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;12&quot; data-ke-size=&quot;size26&quot;&gt;2. 사전 준비&lt;/h2&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;카카오 디벨로퍼스에서 다음 설정을 완료한다. (메인화면 &amp;gt; 앱 &amp;gt; 앱생성 클릭)&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;884&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/px353/dJMcaiQv951/8Z3XUx5ezgvScmrBabQkY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/px353/dJMcaiQv951/8Z3XUx5ezgvScmrBabQkY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/px353/dJMcaiQv951/8Z3XUx5ezgvScmrBabQkY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpx353%2FdJMcaiQv951%2F8Z3XUx5ezgvScmrBabQkY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;512&quot; height=&quot;311&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;884&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-18 오후 6.32.19.png&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;1158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kGaCR/dJMcacW5GSJ/spKLwmk9JdXV72ehfy78D1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kGaCR/dJMcacW5GSJ/spKLwmk9JdXV72ehfy78D1/img.png&quot; data-alt=&quot;플랫폼키로 들어간다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kGaCR/dJMcacW5GSJ/spKLwmk9JdXV72ehfy78D1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkGaCR%2FdJMcacW5GSJ%2FspKLwmk9JdXV72ehfy78D1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;512&quot; height=&quot;407&quot; data-filename=&quot;스크린샷 2026-04-18 오후 6.32.19.png&quot; data-origin-width=&quot;1458&quot; data-origin-height=&quot;1158&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;플랫폼키로 들어간다.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;14&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,0,0&quot;&gt;내 애플리케이션 등록&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,1,0&quot;&gt;Platform&lt;/b&gt;: 웹 서비스 URL 등록 (예: http://localhost:8000)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,2,0&quot;&gt;Redirect URI&lt;/b&gt;: 인가 코드를 받을 주소 등록 (예: /auth/kakao/callback)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,3,0&quot;&gt;Client Secret&lt;/b&gt;: 보안을 위해 활성화 권장&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-path-to-node=&quot;15&quot; data-ke-size=&quot;size23&quot;&gt;REST API 방식을 선택한 이유?&lt;/h3&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;백엔드에서 server to server로 통신을 하기 때문에 좀 더 토큰 관리의 안정성이 보장되기도 하고, 추후 플랫폼 확장성과 비즈니스 로직을 생각했을 때도 REST API가 적합하다는 판단을 내렸다.&lt;/p&gt;
&lt;hr data-path-to-node=&quot;17&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;18&quot; data-ke-size=&quot;size26&quot;&gt;3. FastAPI 코드 구현&lt;/h2&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;기본적으로 팀 프로젝트를 진행하여 깃허브에 올릴 경우 보안상의 문제로 .env 파일을 활용한다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;api키와 리디엑트 uri 만 있어도 되지만, 나는 인가 코드를 가로채는 공격을 방지하기 위해 서버 사이드에서 client_secret을 사용해 검증력을 높였다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;20&quot;&gt;[.env]&lt;/b&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwivppTO__aTAxUAAAAAHQAAAAAQgAI&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;KAKAO_REST_API_KEY=&amp;lt;사용한 RESTAPI 키&amp;gt;
KAKAO_USER_INFO_URL=https://kapi.kakao.com/v2/user/me
KAKAO_REDIRECT_URI=&amp;lt;리디렉트 URI 주소&amp;gt;
KAKAO_CLIENT_SECRET=&amp;lt;클라이언트 시크릿 키&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22&quot;&gt;[Backend Logic]&lt;/b&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwivppTO__aTAxUAAAAAHQAAAAAQgQI&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session
import httpx
from src.app.core.config import settings
from src.app.core.security import create_access_token
from src.app.core.database import get_db
from src.app.models.user import User
from src.app.schemas.auth import LoginRequest

router = APIRouter()

@router.post(&quot;/login&quot;)
async def kakao_login(request: LoginRequest, db: Session = Depends(get_db)):
    &quot;&quot;&quot;
    카카오톡을 이용한 로그인 프로세스를 진행합니다.
    &quot;&quot;&quot;
    # 1. 인가 코드로 카카오 토큰 발급 요청 (POST 방식)
    token_url = &quot;https://kauth.kakao.com/oauth/token&quot;
    token_data = {
        &quot;grant_type&quot;: &quot;authorization_code&quot;,
        &quot;client_id&quot;: settings.KAKAO_REST_API_KEY,
        &quot;redirect_uri&quot;: settings.KAKAO_REDIRECT_URI,
        &quot;code&quot;: request.accessToken, # 프론트에서 받은 인가코드
        &quot;client_secret&quot;: settings.KAKAO_CLIENT_SECRET,
    }

    async with httpx.AsyncClient() as client:
        token_resp = await client.post(token_url, data=token_data)

        if token_resp.status_code != 200:
            raise HTTPException(status_code=401, detail=&quot;카카오 토큰 발급 실패&quot;)

        kakao_tokens = token_resp.json()
        real_access_token = kakao_tokens.get(&quot;access_token&quot;)

        # 2. 받은 진짜 access_token으로 사용자 정보 가져오기
        user_info_url = settings.KAKAO_USER_INFO_URL
        headers = {
            &quot;Authorization&quot;: f&quot;Bearer {real_access_token}&quot;,
            &quot;Content-type&quot;: &quot;application/x-www-form-urlencoded;charset=utf-8&quot;,
        }
        user_resp = await client.post(user_info_url, headers=headers)

    if user_resp.status_code != 200:
        raise HTTPException(status_code=401, detail=&quot;사용자 정보 조회 실패&quot;)

    kakao_user = user_resp.json()
    kakao_id = str(kakao_user.get(&quot;id&quot;))

    # 3. DB 조회 및 회원가입 로직
    user = db.query(User).filter(User.social_id == kakao_id).first()
    if not user:
        temp_nickname = f&quot;BINGO_{kakao_id[:5]}&quot;
        user = User(social_id=kakao_id, social_provider=&quot;KAKAO&quot;, nickname=temp_nickname)
        db.add(user)
        db.commit()
        db.refresh(user)

    # 4. 우리 서비스 전용 JWT 발급
    my_token = create_access_token(data={&quot;sub&quot;: str(user.id)})
    is_required_nickname = user.nickname.startswith(&quot;BINGO_&quot;)

    return {
        &quot;status&quot;: &quot;success&quot;,
        &quot;message&quot;: &quot;로그인 성공&quot;,
        &quot;data&quot;: {
            &quot;accessToken&quot;: my_token,
            &quot;is_required_nickname&quot;: is_required_nickname,
            &quot;user&quot;: {&quot;id&quot;: user.id, &quot;nickname&quot;: user.nickname},
        },
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-path-to-node=&quot;24&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;25&quot; data-ke-size=&quot;size26&quot;&gt;4. Azure 배포 및 CORS 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 다 짰다면 이제 실제 서버에 올려서 프론트엔드와 연결할 차례다. 우리는 &lt;b data-index-in-node=&quot;48&quot; data-path-to-node=&quot;5&quot;&gt;Azure&lt;/b&gt; 인프라를 활용해 환경을 구축하였다.&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;6&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6&quot;&gt;1. 인프라 구성&lt;/b&gt;&lt;/h4&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1gdKH/dJMcaaZhlcn/nGdTKaku58nItbBR0xofr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1gdKH/dJMcaaZhlcn/nGdTKaku58nItbBR0xofr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1gdKH/dJMcaaZhlcn/nGdTKaku58nItbBR0xofr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1gdKH%2FdJMcaaZhlcn%2FnGdTKaku58nItbBR0xofr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1334&quot; height=&quot;138&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1334&quot; data-origin-height=&quot;138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-path-to-node=&quot;7&quot;&gt;
&lt;li&gt;&lt;b data-path-to-node=&quot;7,0,0&quot; data-index-in-node=&quot;0&quot;&gt;Frontend:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Azure Static Web Apps (SWA)를 이용해 배포했다.&amp;nbsp; SWA는 GitHub 액션과 연동되어 push만 하면 자동으로 배포되는 형식으로 정말 편하다.&lt;/li&gt;
&lt;li&gt;&lt;b data-path-to-node=&quot;7,1,0&quot; data-index-in-node=&quot;0&quot;&gt;Backend:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-path-to-node=&quot;7,1,0&quot; data-index-in-node=&quot;9&quot;&gt;Azure App Service&lt;/b&gt;를 사용했다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;&lt;b&gt;[배포 tip] Azure Static Web Apps 생성 및 GitHub 연동&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;1066&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z3BLL/dJMcadVYsqS/oFUm5V1cCO2IWtGqehqVQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z3BLL/dJMcadVYsqS/oFUm5V1cCO2IWtGqehqVQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z3BLL/dJMcadVYsqS/oFUm5V1cCO2IWtGqehqVQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz3BLL%2FdJMcadVYsqS%2FoFUm5V1cCO2IWtGqehqVQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;488&quot; height=&quot;1066&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;1066&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h4 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8&quot;&gt;1. 빌드 세부 설정 (Build Details)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;연동 후 하단에 나타나는 빌드 상세 설정도 꼼꼼히 봐야 한다. 이 부분이 틀리면 &quot;배포는 성공했는데 화면이 안 떠요&quot; 하는 상황이 발생한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;10&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,0,0&quot;&gt;Build Presets:&lt;/b&gt; 프론트엔드 프레임워크(React, Vue, Next.js 등)에 맞춰 선택한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,0&quot;&gt;App location:&lt;/b&gt; 소스 코드가 있는 루트 경로 (보통 /)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,2,0&quot;&gt;Output location:&lt;/b&gt; 빌드 결과물이 생기는 폴더명. 프레임워크마다 dist인지 build인지 다르므로 본인의 프로젝트 구조를 꼭 확인해야 한다. (예: Vite는 보통 dist, React 기본 설정은 build)&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;531&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4SLR9/dJMcagypLsz/e68AoPjwHmIfOsm5oumrAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4SLR9/dJMcagypLsz/e68AoPjwHmIfOsm5oumrAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4SLR9/dJMcagypLsz/e68AoPjwHmIfOsm5oumrAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4SLR9%2FdJMcagypLsz%2Fe68AoPjwHmIfOsm5oumrAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;501&quot; height=&quot;531&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;531&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h4 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5&quot;&gt;2. 배포 권한 설정 (Deployment configuration)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;리소스를 처음 생성할 때 가장 중요한 부분이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;7&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,0,0&quot;&gt;GitHub 선택 필수:&lt;/b&gt; 캡처 화면처럼 Deployment authorization policy에서 &lt;b data-index-in-node=&quot;56&quot; data-path-to-node=&quot;7,0,0&quot;&gt;GitHub&lt;/b&gt;를 선택해야 한다. 그래야 GitHub Repository의 코드가 수정될 때마다 Azure가 알아서 빌드하고 배포하는 자동화(CI/CD)가 완성된다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,0&quot;&gt;권한 승인:&lt;/b&gt; GitHub를 선택하면 내 계정을 연동하라는 창이 뜨는데, 거기서 대상 리포지토리를 정확히 골라주면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8&quot;&gt;2. 프론트엔드와 연결 시 주의할 점 (CORS 설정)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;서로 다른 도메인(SWA 주소와 App Service 주소) 간에 통신을 하려면 FastAPI에서 &lt;b data-index-in-node=&quot;55&quot; data-path-to-node=&quot;9&quot;&gt;CORS(Cross-Origin Resource Sharing)&lt;/b&gt; 설정을 반드시 해주어야 한다.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;27&quot; data-ke-size=&quot;size23&quot;&gt;CORS(Cross-Origin Resource Sharing)란?&lt;/h3&gt;
&lt;p data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;브라우저에서 보안을 위해 출처가 다른 서버로의 요청을 막는 정책이다. FastAPI에서는 CORSMiddleware를 통해 허용할 프론트엔드 주소를 등록해주어야 한다.&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwivppTO__aTAxUAAAAAHQAAAAAQggI&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=[&quot;https://your-static-web-app-url.azurestaticapps.net&quot;],
    allow_credentials=True,
    allow_methods=[&quot;*&quot;],
    allow_headers=[&quot;*&quot;],
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;30&quot; data-ke-size=&quot;size16&quot;&gt;또한 Azure App Service의 설정(Configuration) 메뉴에서 환경 변수를 등록해주는 것도 잊지 말아야 한다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bt2F2O/dJMcaiiH5rg/jhAE8T072RUhmP6JEKT1k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bt2F2O/dJMcaiiH5rg/jhAE8T072RUhmP6JEKT1k0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bt2F2O/dJMcaiiH5rg/jhAE8T072RUhmP6JEKT1k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbt2F2O%2FdJMcaiiH5rg%2FjhAE8T072RUhmP6JEKT1k0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1478&quot; height=&quot;414&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이렇게 연결 후 테스트를 하면 로그인이 성공하는 걸 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 프론트엔드에서 넘겨주는 값이 인가 코드인지 엑세스 토큰인지 헷갈려 커뮤니케이션 미스가 있기도 했지만, 덕분에 OAuth 2.0의 흐름을 정확히 머릿속에 새길 수 있었다. 역시 개발은 직접 삽질해보고 구현해봐야 가장 빠르게 배우는 것 같다...&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;Azure 배포 환경에서의 설정이나 CORS 이슈 등 제가 겪은 과정이 소셜 로그인을 처음 구현하시는 분들께 조금이나마 이정표가 되길 바란다!!&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발</category>
      <author>eun_ll</author>
      <guid isPermaLink="true">https://journal7218.tistory.com/5</guid>
      <comments>https://journal7218.tistory.com/5#entry5comment</comments>
      <pubDate>Sat, 18 Apr 2026 18:56:05 +0900</pubDate>
    </item>
    <item>
      <title>[MS AI School 1차 프로젝트] 오탐 판정 13.3%p 감소: '양치기 소년'을 잡는 시계열 엔진 &amp;mdash; E-gle Eye 실시간 시스템 구축기 (2/2)</title>
      <link>https://journal7218.tistory.com/4</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오프라인 지표 95%의 모델이 실전 웹캠에서 오답을 쏟아냈다.&amp;nbsp;2편에서는 그 간극의 원인(전처리 불일치, 편향, 기하 왜곡)을 하나씩 제거하고, 시계열 신뢰도 엔진과 Grad-CAM, WebSocket 실시간 서빙으로 '시스템'을 완성한 과정을 다룬다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈&amp;nbsp;[1편 &amp;mdash; 모델 편]: &lt;a href=&quot;https://journal7218.tistory.com/3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;문제 정의 &amp;middot; 데이터 파이프라인 &amp;middot; 과적합 트러블슈팅 &amp;middot; ResNet50 전이학습&lt;/a&gt;&lt;br /&gt;2편 &amp;mdash; 실시간 시스템 편&amp;nbsp;(현재 글): 실전 오탐 제거 &amp;middot; 시계열 엔진 &amp;middot; Grad-CAM &amp;middot; WebSocket &amp;middot; 책임 있는 AI&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;이 글의 핵심 성과&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;동일 프레임 세트 기준 위험 판정 27/33건 &amp;rarr; 23/33건 (오탐 13.3%p 감소, 실제 화재 탐지율 유지)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 시계열 큐 + EWMA + 투표 로직&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Train-Serving Skew 제거&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 전처리 중첩&amp;middot;불일치 교정, Squash &amp;rarr; Padding 전환으로 검증 정확도 97.53% &amp;rarr; 98.15%, Loss 0.0898 &amp;rarr; 0.0586&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Grad-CAM 판단 근거 시각화&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 관리자가 &quot;AI가 어디를 보고 판단했는지&quot; 즉시 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTP &amp;rarr; WebSocket 전환&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 유튜브 라이브/VOD를 0.1초 단위로 분석&amp;middot;전송하는 실시간 파이프라인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Azure ML 트랙 최종 정확도 0.993 / 정밀도 0.993 / 재현율 0.996&lt;/b&gt;&lt;br /&gt;&amp;mdash; 이진 분류 전환 + Hard Negative + Targeted Fine-tuning&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;1. Problem: 지표는 좋은데 실전에서 틀린다&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1편에서 검증 정확도 95%+를 달성했지만, 웹캠&amp;middot;실제 영상 테스트에서 오답이 속출했다. 실전 화재 장면 9장 배치 테스트에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;5장이 오분류&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 특히 화면 가득 불이 타는 장면을 Negative로 판정하는, 화재 감지 시스템으로서는 치명적인 미탐(False Negative)까지 나왔다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1052&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvapFy/dJMcacwYHUb/3mJdXVOKeJzk3AdAif7ZS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvapFy/dJMcacwYHUb/3mJdXVOKeJzk3AdAif7ZS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvapFy/dJMcacwYHUb/3mJdXVOKeJzk3AdAif7ZS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvapFy%2FdJMcacwYHUb%2F3mJdXVOKeJzk3AdAif7ZS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;1052&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1052&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cflYcb/dJMcahZjkrV/olpwC7IUgXIvSUVtGcFczk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cflYcb/dJMcahZjkrV/olpwC7IUgXIvSUVtGcFczk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cflYcb/dJMcahZjkrV/olpwC7IUgXIvSUVtGcFczk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcflYcb%2FdJMcahZjkrV%2FolpwC7IUgXIvSUVtGcFczk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;1318&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1318&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마지막 학습 지표는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;정확도 95.63% / Precision&amp;middot;Recall 0.9563 / Loss 0.1283&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 숫자만 보면 문제가 없다. 그렇다면 문제는 모델 바깥, 즉&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;학습 환경과 추론 환경의 불일치&lt;/b&gt;에 있다고 가설을 세우고 Grad-CAM으로 확인했다. 실제로 모델은 실전 프레임에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;불꽃과 연기가 아닌 엉뚱한 영역(벽면&amp;middot;창문)을 보고 있었다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;901&quot; data-origin-height=&quot;436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQQDvv/dJMcaazbs1R/MY3aXSAt6S5BKfC0DpCxK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQQDvv/dJMcaazbs1R/MY3aXSAt6S5BKfC0DpCxK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQQDvv/dJMcaazbs1R/MY3aXSAt6S5BKfC0DpCxK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQQDvv%2FdJMcaazbs1R%2FMY3aXSAt6S5BKfC0DpCxK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;901&quot; height=&quot;436&quot; data-origin-width=&quot;901&quot; data-origin-height=&quot;436&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 간극을 세 가지 트러블슈팅으로 분해해 제거했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;[Trouble Shooting 1]&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;전처리 중첩&amp;middot;불일치 &amp;mdash; Train-Serving Skew&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;추론 코드를 추적하자 두 가지 결함이 겹쳐 있었다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;전처리 중첩&lt;/b&gt;: 1편에서 소개했듯 모델 그래프 내부에 이미 전처리 Lambda 층(resnet_preprocess)이 내장돼 있는데, 추론 코드에서 preprocess_input을 한 번 더 적용 &amp;rarr; 픽셀 분포가 이중 변환되어 입력 자체가 손상&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전처리 불일치&lt;/b&gt;: ResNet50 기반 모델에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;MobileNetV2용 전처리 함수&lt;/b&gt;를 사용 &amp;rarr; 백본이 기대하는 입력 스케일과 다른 값이 유입&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;추론 코드의 외부 전처리를 전면 제거하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;모델 내부 Lambda 층 단일 경로로 통일&lt;/b&gt;했다. 전처리를 모델 그래프 안에 내장한 원래 설계 의도 &amp;mdash; 학습과 서빙이 물리적으로 같은 코드를 타게 한다 &amp;mdash; 를 지키려면, 바깥에서는 아무것도 하지 말아야 했던 것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;nbsp;교훈: 오프라인 지표와 실전 성능의 간극(Train-Serving Skew)에서 가장 먼저 의심할 곳은 모델이 아니라&amp;nbsp;전처리 경로의 일관성이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;[Trouble Shooting 2]&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;편향 &amp;mdash; 가습기 수증기를 무조건 화재로 찍는 모델&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가습기 수증기가 100% &quot;화재&quot;로 분류되는 현상이 반복됐다. 분석 결과 학습 데이터에서 화재 클래스 비율이 과도해, 모델이 특징을 학습한 것이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;확률적으로 다수 클래스를 찍는 편향(Bias)&lt;/b&gt;이 형성돼 있었다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오탐 단골 이미지(가습기, 자동차 전조등 등)를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;negative로 356장 보강&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;class_weight를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;10배로 조정&lt;/b&gt;해 소수 클래스의 오답에 강한 페널티 부여&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;데이터 재밸런싱과 손실 가중치를 함께 조정해, 모델이 '많이 나온 답'이 아니라 '특징'을 근거로 판별하도록 교정했다. 보강 후에는 화재 감지의 최대 함정인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;설원의 브레이크등&amp;middot;전조등 차량도 Negative 97~100%로 안정 판별&lt;/b&gt;됐다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;899&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oiGJl/dJMcaftFXJE/Vc1himCoskcxO25kAC6nO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oiGJl/dJMcaftFXJE/Vc1himCoskcxO25kAC6nO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oiGJl/dJMcaftFXJE/Vc1himCoskcxO25kAC6nO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoiGJl%2FdJMcaftFXJE%2FVc1himCoskcxO25kAC6nO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;899&quot; height=&quot;316&quot; data-origin-width=&quot;899&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;4.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;[Trouble Shooting 3]&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Squash &amp;rarr; Padding &amp;mdash; CNN이 보는 '형태'를 지켜라&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;영상 테스트에서 후반부로 갈수록, 그리고 특정 각도에서 인식률이 급락했다. 원인은 리사이징 방식이었다. 학습 시에는 이미지를 224&amp;times;224로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;강제 압착(Squash)&lt;/b&gt;했는데, 이 과정에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;불꽃의 가로세로 비율 &amp;mdash; CNN이 인식하는 화재의 고유 형태적 특징 &amp;mdash; 이 왜곡&lt;/b&gt;되고 있었다. 실시간 프레임의 다양한 화면비에서 왜곡 정도가 제각각이니 판별이 흔들릴 수밖에 없다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;tf.image.resize_with_pad 기반&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Padding(Letterbox) 방식&lt;/b&gt;으로 전환해 원본 비율을 100% 보존하고, 학습 환경과 실시간 환경의 입력 일관성을 확보했다.&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;항목&lt;/td&gt;
&lt;td&gt;Squash 모델&lt;/td&gt;
&lt;td&gt;Padding 모델&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;검증 정확도&lt;/td&gt;
&lt;td&gt;97.53%&lt;/td&gt;
&lt;td&gt;&lt;b&gt;98.15%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;검증 Loss&lt;/td&gt;
&lt;td&gt;0.0898&lt;/td&gt;
&lt;td&gt;&lt;b&gt;0.0586&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;훈련 정확도&lt;/td&gt;
&lt;td&gt;99.38%&lt;/td&gt;
&lt;td&gt;99.69%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;수치 개선폭 자체보다 중요한 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Grad-CAM 히트맵이 불꽃&amp;middot;연기 영역에 정확히 정렬되기 시작했다&lt;/b&gt;는 점이다. 웹캠&amp;middot;영상 양쪽에서 실전 인식률이 복구되어 이 모델을 최종본으로 확정했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;5. 시계열 신뢰도 엔진 &amp;mdash; 한 프레임을 믿지 않는다&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전처리를 다 고쳐도 단일 프레임 판정에는 한계가 있다. 빛 반사나 순간 섬광은 그 프레임에서는 진짜 불처럼 보이기 때문이다. 그래서 판정의 단위를 프레임에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;시간의 흐름&lt;/b&gt;으로 바꿨다. 이것이 E-gle Eye의 핵심 차별점인 시계열 신뢰도 엔진이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;프레임 확률(raw_prob)
   │
   ▼
① Sliding Window ─ deque(maxlen=8): 최근 8회 탐지 결과를 버퍼링
   │
   ▼
② EWMA (동적 &amp;alpha;) ─ alpha = 0.6 if raw_prob &amp;gt; FIRE_THRESHOLD else 0.2
   │              위험 신호는 3배 민감하게, 평상시는 안정적으로
   ▼
③ Voting ─ 윈도우 내 화재 판정 비율(fire_ratio) 산출
   │
   ▼
④ 상태 판정 ─   Green(정상) /   Orange(경계) /   Red(위험&amp;middot;대응)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;설계 포인트&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;동적 가중 EWMA&lt;/b&gt;: 확률이 임계치를 넘는 순간 &amp;alpha;를 0.2 &amp;rarr; 0.6으로 전환. 화재 상황에는 현재 신호를 3배 강하게 반영해 빠르게 반응하고, 평상시에는 이전 흐름을 유지해 수치 출렁임(오탐)을 흡수하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;비대칭 반응 곡선&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조기 경보 튜닝&lt;/b&gt;: 초기에는 8회 중 과반(4~5회) 감지 시 경보였으나, 골든타임 도메인 특성상 놓침(False Negative)의 비용이 더 크다고 판단 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;EMERGENCY_COUNT = 2&lt;/b&gt;, 즉 약 1초 구간에서 2프레임만 포착돼도 즉시 EMERGENCY로 격상하도록 재조정했다. 오탐 억제는 EWMA와 투표가, 신속성은 완화된 카운트가 분담하는 구조다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;신호등 상태 머신&lt;/b&gt;: fire_ratio 구간에 따라 Green / Orange / Red 3단계로 상태를 표시해, 관리자가 이진 알람이 아닌&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;위험의 추세&lt;/b&gt;를 읽을 수 있게 했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;결과 &amp;mdash; 동일 프레임 세트 A/B 검증&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;오탐이 잦았던 연속 프레임 33장으로 구모델(단일 프레임 판정)과 신모델+시계열 로직을 비교했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;구분 위험(Danger) 판정 비고&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구모델 (단일 프레임)&lt;/td&gt;
&lt;td&gt;27/33 = 90.0%&lt;/td&gt;
&lt;td&gt;노이즈 프레임까지 위험 판정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;신모델 + 시계열 로직&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;23/33 = 76.7%&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Red 18 + Orange 5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오탐 판정 13.3%p 감소, 실제 화재 구간 탐지율은 유지&amp;nbsp;&amp;mdash; '민감함'과 '신뢰성'을 동시에 잡는 것이 시계열 설계의 목적이었고, 계측으로 확인했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;6. Grad-CAM &amp;mdash; &quot;왜 화재라고 판단했는가&quot;를 보여주기&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;오탐을 줄이는 것만큼 중요한 것이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;판단 근거의 공개&lt;/b&gt;다. 관리자가 AI의 판정을 신뢰하려면, 근거를 눈으로 확인할 수 있어야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;히트맵 추출&lt;/b&gt;: ResNet50 최심층 특징 레이어(conv5_block3_out)에서 클래스 활성화 맵을 추출&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정규화&amp;middot;오버레이&lt;/b&gt;: 중요도를 0~1로 정규화한 열지표를 원본 프레임에 합성, 화재 판정 영역을 Bounding Box와 함께 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Grad-CAM은 이번 프로젝트에서 두 가지 역할을 했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개발 단계에서는 디버깅 도구&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 1장에 나와있는 것처럼 모델이 불꽃 대신 배경을 보고 있음을 밝혀내 전처리 문제(2&amp;middot;4장)를 추적하는 단서가 됐고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;운영 단계에서는 제품 기능&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 관리자가 판단 근거를 즉시 확인하고 오탐 시 &quot;왜 틀렸는지&quot;까지 설명 가능한 인터페이스가 됐다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u3oFo/dJMcaalyRTO/1ce12stgzQkyk5hsks64Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u3oFo/dJMcaalyRTO/1ce12stgzQkyk5hsks64Kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u3oFo/dJMcaalyRTO/1ce12stgzQkyk5hsks64Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu3oFo%2FdJMcaalyRTO%2F1ce12stgzQkyk5hsks64Kk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;393&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;7. 실시간 서빙 &amp;mdash; HTTP를 버리고 WebSocket으로&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Problem&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프레임 단위 분석 결과를 HTTP 요청/응답으로 주고받자 지연과 끊김이 누적됐다. 초당 수 회의 판정을 스트리밍해야 하는 워크로드에 요청-응답 모델은 구조적으로 맞지 않았다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Solution&lt;/h3&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;[영상 소스]                [Python 백엔드]                     [React 프런트]
 YouTube Live/VOD ──&amp;gt; yt-dlp 스트림 추출 + cv2.VideoCapture
 웹캠                       │
                            ▼
                     ResNet50 추론 + 시계열 엔진 + Grad-CAM
                            │
                            ▼
                     WebSocket (FastAPI)  ──0.1초 단위──&amp;gt;  JSON + Base64 프레임
                     ws://&amp;lt;local-ip&amp;gt;:8000/ws/analyze        상태 게이지&amp;middot;히트맵 렌더링
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;통신 전환&lt;/b&gt;: HTTP &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;WebSocket 전면 재구축&lt;/b&gt;. 연결을 유지한 채 분석 결과를 0.1초 단위로 푸시&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유튜브 라이브 파이프라인&lt;/b&gt;: yt-dlp로 라이브/VOD 스트림 주소를 추출해 FPS 30 프레임을 실시간 분석하는 입력 어댑터 구성. 산불 연기 영상 테스트에서 프레임 전 구간 Smoke 99%+로 연속 추적됨을 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배포 판단&lt;/b&gt;: 데모 범위에서는 클라우드 배포의 이득이 없다고 판단, 로컬 네트워크에 서버를 개방해 프런트엔드 팀과 연동 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;범위에 맞는 인프라 최소화&lt;/b&gt;도 의사결정이다&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;902&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cc5J2L/dJMcagMYaFR/5GsHzPgNxJpvJv6JlmWbB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cc5J2L/dJMcagMYaFR/5GsHzPgNxJpvJv6JlmWbB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cc5J2L/dJMcagMYaFR/5GsHzPgNxJpvJv6JlmWbB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcc5J2L%2FdJMcagMYaFR%2F5GsHzPgNxJpvJv6JlmWbB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1108&quot; height=&quot;902&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;902&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;오탐 발생 시 해당 프레임과 분석 JSON은 temp_logs에 저장하고 신뢰도 수치는 CSV로 연속 기록해,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;오탐 데이터가 재학습 셋으로 순환하는 피드백 루프&lt;/b&gt;의 기반을 마련했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;8. Azure ML 트랙 최종 고도화 &amp;mdash; v4 &amp;middot; v5&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1편의 성능 역전(v2, 0.8738) &amp;rarr; ResNet50 반등(v3, 0.9078)에 이어, 실전 오탐을 잡기 위한 마지막 두 단계를 진행했다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;v4 &amp;mdash; 문제를 다시 정의하다: 삼중 분류 &amp;rarr; 이진 분류&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;불/연기/정상 삼중 분류에서 모델이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;불과 연기를 서로 혼동&lt;/b&gt;하며 재현율이 정체됐다 &amp;mdash; 1장의 배치 테스트에서 확인한 오분류가 바로 이 지점이다. 그런데 서비스 관점에서 자문해 보면, 관리자에게 필요한 정보는 &quot;불이냐 연기냐&quot;가 아니라 화재위험의 골든타임을 잡는 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;로직 단순화&lt;/b&gt;: 화재(불+연기) / 정상의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이진 분류로 전환&lt;/b&gt;, 데이터 1:1 재구성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Hard Negative 보강&lt;/b&gt;: Grad-CAM으로 확인된 오탐 유발 사물(브레이크등, 소방복, 안개&amp;middot;수증기, 회색 배경)을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;1:1.2 비율로 강제 삽입&lt;/b&gt;해 판별 경계를 교정&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결과:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;정확도 0.984&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 분류 체계 재설계만으로 7%p 이상 상승했다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;v5 &amp;mdash; Grad-CAM 기반 Targeted Fine-tuning&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;잔존하는 1% 미만의 오차를 잡기 위해, Grad-CAM 전수 조사로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;취약 케이스만 골라 전용 데이터셋을 구성&lt;/b&gt;해 정밀 재학습(Targeted Fine-tuning)을 진행했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;버전&lt;/td&gt;
&lt;td&gt;전략&lt;/td&gt;
&lt;td&gt;정확도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v1 Baseline&lt;/td&gt;
&lt;td&gt;초기 모델 수립&lt;/td&gt;
&lt;td&gt;0.8939&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v2 Big Data&lt;/td&gt;
&lt;td&gt;데이터 양 증대&lt;/td&gt;
&lt;td&gt;0.8738 (&lt;b&gt;성능 역전&lt;/b&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v3 Fine-Tune&lt;/td&gt;
&lt;td&gt;ResNet50 교체 + LR 조정&lt;/td&gt;
&lt;td&gt;0.9078&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v4 Optimized&lt;/td&gt;
&lt;td&gt;이진 분류 + Hard Negative&lt;/td&gt;
&lt;td&gt;0.984&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;v5 Final Polish&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Grad-CAM 취약 케이스 정밀 보강&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;정확도 0.993 &amp;middot; 정밀도 0.993 &amp;middot; 재현율 0.996&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;v1&amp;rarr;v5의 궤적이 곧 이 프로젝트의 요약이다:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;감(양 늘리기) -&amp;gt; 데이터 기반 튜닝(취약점 조준)&lt;/b&gt;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;642&quot; data-origin-height=&quot;373&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GDVDB/dJMcaaMGU69/iZDpWPP4qhL91Im29L6IwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GDVDB/dJMcaaMGU69/iZDpWPP4qhL91Im29L6IwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GDVDB/dJMcaaMGU69/iZDpWPP4qhL91Im29L6IwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGDVDB%2FdJMcaaMGU69%2FiZDpWPP4qhL91Im29L6IwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;642&quot; height=&quot;373&quot; data-origin-width=&quot;642&quot; data-origin-height=&quot;373&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;9. 책임 있는 AI &amp;mdash; 사람이 최종 결정권을 갖는 설계&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;생명과 직결된 도메인인 만큼, 성능 밖의 설계 원칙도 명시했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;재현율 우선 + 관리자 최종 통제&lt;/b&gt;: 인명 피해 방지를 위해 재현율을 높게 유지하되, 위험 팝업에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;최종 판단 버튼은 관리자가 누른다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; AI는 판정하고, 결정은 사람이 한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;판단 투명성&lt;/b&gt;: Grad-CAM 히트맵, 시계열 게이지, 프레임 분석 그래프로 성능 수치가 아닌&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;판단 과정&lt;/b&gt;을 공개&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 휘발성&lt;/b&gt;: 영상 프레임은 Base64 1회성 정보로 표시 후 파기, 시연 시 60초 자동 파기&amp;middot;운영 시 7일 후 영구 삭제 정책으로 개인정보 리스크 차단&lt;/li&gt;
&lt;li&gt;&lt;b&gt;포용성&lt;/b&gt;: 청각 장애인용 화면 테두리 점멸 경보(Barrier-free), 4개국어 TTS, 다크모드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;피드백 루프&lt;/b&gt;: 오탐 신고 데이터를 재학습 셋으로 자동 분류하는 지속 개선 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;10. 마치며 (Retrospective)&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2주라는 일정 속에서 이 프로젝트가 남긴 인사이트는 네 가지다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;b&gt;첫째, 오프라인 지표와 실전 성능 사이에는 '전처리'라는 협곡이 있다.&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;검증 정확도 95%의 모델이 실전에서 무너진 원인은 모델이 아니라 전처리 중첩, 백본 불일치, 리사이징 왜곡이었다. Train-Serving Skew를 의심하는 습관, 그리고 전처리를 모델 그래프 안에 내장하는 설계가 답이었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;b&gt;둘째, 화재 감지의 본질은 분류가 아니라 신뢰다.&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;정확도를 1%p 올리는 것보다, 오탐 판정을 13.3%p 줄여 관리자가 경보를 믿게 만드는 것이 제품 가치에 가깝다. 시계열 엔진과 Grad-CAM은 그 신뢰를 만드는 두 축 &amp;mdash; 하나는 판정의 안정성, 하나는 판단의 투명성 &amp;mdash; 이었다.aa&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;b&gt;셋째, &quot;데이터는 많을수록 좋다&quot;는 조건부 참이다.&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;v2의 성능 역전은 데이터 양&amp;middot;품질&amp;middot;모델 수용량이 함께 설계돼야 함을 보여줬고, v4~v5의 도약은 무차별 증량이 아닌 **문제 재정의(이진 분류)와 조준 사격(Hard Negative, Targeted FT)**에서 나왔다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #781b33;&quot;&gt;&lt;b&gt;넷째, 설명가능성은 도덕이 아니라 품질 기준이다.&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Grad-CAM은 윤리 체크리스트 항목으로 넣었다가, 전처리 버그를 찾아낸 디버깅 도구이자 관리자 신뢰를 만드는 핵심 기능이 됐다. 투명성 장치는 시스템의 품질을 끌어올리는 엔지니어링 자산이었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클라우드(Azure)에서 리소스를 직접 다뤄본 경험, 그리고 모델을 '시스템'으로 완성해 본 경험 &amp;mdash; 이 두 가지가 이번 1차 프로젝트의 가장 큰 수확이다. 아쉬움으로 남은 웹 대시보드 고도화는 다음 프로젝트의 출발점으로 삼는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;nbsp;전체 코드:&amp;nbsp;&lt;a href=&quot;https://github.com/sseungeun/e-gleEye&quot;&gt;github.com/sseungeun/e-gleEye&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;a href=&quot;https://journal7218.tistory.com/3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[1편 &amp;mdash; 모델 편 보러가기]&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>MS ai School</category>
      <author>eun_ll</author>
      <guid isPermaLink="true">https://journal7218.tistory.com/4</guid>
      <comments>https://journal7218.tistory.com/4#entry4comment</comments>
      <pubDate>Sat, 18 Apr 2026 18:21:27 +0900</pubDate>
    </item>
    <item>
      <title>[MS AI School 1차 프로젝트] 검증 정확도 52% &amp;rarr; 98%: 화재 감지 AI 'E-gle Eye' 모델 신뢰도 구축기 (1/2)</title>
      <link>https://journal7218.tistory.com/3</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CCTV 화재 감지의 진짜 적은 불이 아니라 '오탐지'다.&amp;nbsp;1편에서는 과적합(검증 정확도 52.6%)에서 출발해 데이터 정제 4연전과 ResNet50 전이학습으로 검증 정확도 98%대까지 끌어올린 모델 구축 과정을 다룬다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈&amp;nbsp;1편 &amp;mdash; 모델 편&amp;nbsp;(현재 글): 문제 정의 &amp;middot; 데이터 파이프라인 &amp;middot; 과적합 트러블슈팅 &amp;middot; ResNet50 전이학습&lt;br /&gt;[2편 &amp;mdash; 실시간 시스템 편]: &lt;a href=&quot;https://journal7218.tistory.com/4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;오탐 13.3%p 감소 시계열 엔진 &amp;middot; Grad-CAM &amp;middot; WebSocket 실시간 서빙&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;프로젝트 개요&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 8.02326%;&quot;&gt;&lt;b&gt;서비스&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 91.8605%;&quot;&gt;E-gle Eye &amp;mdash; CCTV 기반 실시간 화재 감지 시스템 (화재/연기 분류 + 시계열 검증 + 판단 근거 시각화)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 8.02326%;&quot;&gt;&lt;b&gt;기간&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 91.8605%;&quot;&gt;2026.02.23 ~ 2026.03.10 (약 2주)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 8.02326%;&quot;&gt;&lt;b&gt;팀 구성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 91.8605%;&quot;&gt;6명 (데이터 처리 1 / AI 모델 개발 2 / 로직 처리 1 / UI 개발 2) &amp;mdash; 본인: 기획 + Python 딥러닝 아키텍처 설계 및 시계열 검증 로직&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 8.02326%;&quot;&gt;&lt;b&gt;스택&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 91.8605%;&quot;&gt;Python(TensorFlow, PyTorch, OpenCV) &amp;middot; Azure Custom Vision &amp;middot; Azure ML Designer &amp;middot; Azure Blob Storage &amp;middot; FastAPI(WebSocket) &amp;middot; Flask &amp;middot; React &amp;middot; GitHub&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 글(1편)의 핵심&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오탐지가 만드는 '양치기 소년 효과'를 문제로 정의하고, 정밀도&amp;middot;재현율 중심의 목표 설정&lt;/li&gt;
&lt;li&gt;Custom Vision 베이스라인의 정체 구간(정밀도 70% 벽)을 계측으로 확인하고 커스텀 학습으로 전환&lt;/li&gt;
&lt;li&gt;과적합(훈련 85.3% vs 검증 52.6%)의 원인 3가지 분해와 데이터 정제 트러블슈팅 4연전&lt;/li&gt;
&lt;li&gt;MobileNetV2 &amp;rarr; ResNet50 아키텍처 전환 + 2단계 파인튜닝으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;검증 정확도 52.6% &amp;rarr; 98.15%&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;1. 프로젝트 배경: 불을 찾는 AI가 아니라, 믿을 수 있는 AI&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1-1. 문제 정의&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;화재 진압의 성패는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;골든타임 5분&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;안에 결정된다(국가재난관리정보시스템 통계 기준). 그러나 현장에는 구조적 병목이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관리자 한 명이 수십 대의 CCTV를 실시간 감시하는 것은 물리적으로 불가능하다.&lt;/li&gt;
&lt;li&gt;더 치명적인 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;오탐지의 축적&lt;/b&gt;이다. 빛 반사, 수증기, 순간 섬광이 화재로 분류되는 일이 반복되면, 관리자는 경보를 무시하거나 시스템을 꺼버린다 &amp;mdash; 이른바&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;양치기 소년 효과&lt;/b&gt;다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉 이 도메인의 핵심 지표는 불을 얼마나 잘 찾는지를 말하는 재현율만이 아니라, 울렸을 때 진짜인가를 판단하는 정밀도다. 우리는 시스템의 목표를 '탐지'가 아니라 '관리자가 즉각 신고할 수 있는 신뢰도로 정의했다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구분&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;일반&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;AI 화재 감지 시스템 E-gle Eye&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인프라&lt;/td&gt;
&lt;td&gt;현장 설치형 고가 서버 필수&lt;/td&gt;
&lt;td&gt;Azure 기반 클라우드 배포&amp;middot;확장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;탐지 로직&lt;/td&gt;
&lt;td&gt;단일 프레임/픽셀 기반 판독&lt;/td&gt;
&lt;td&gt;&lt;b&gt;5초 시계열 가중치 분석&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(2편)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;운영 효율&lt;/td&gt;
&lt;td&gt;잦은 오보로 인한 관제 피로&lt;/td&gt;
&lt;td&gt;지능형 필터링으로 오탐지율 최소화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 보안&lt;/td&gt;
&lt;td&gt;불분명한 보관&amp;middot;삭제 주기&lt;/td&gt;
&lt;td&gt;자동 파기 로직으로 리스크 감소&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1-2. 투트랙 모델링 전략: Python(로컬) &amp;times; Azure ML(클라우드)&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;모델 개발을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Python 로컬 트랙과 Azure ML 트랙으로 병행&lt;/b&gt;했다. 단순 백업이 아니라 트레이드오프가 명확했기 때문이다.&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구분&lt;/td&gt;
&lt;td&gt;Python (로컬)&lt;/td&gt;
&lt;td&gt;Azure Machine Learning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모델 정확도&lt;/td&gt;
&lt;td&gt;근소하게 우세 (초기 학습 시)&lt;/td&gt;
&lt;td&gt;로컬과 동등하거나 근소하게 낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배포&amp;middot;확장성&lt;/td&gt;
&lt;td&gt;별도 API 서버 구축&amp;middot;연동 필요&lt;/td&gt;
&lt;td&gt;&lt;b&gt;원클릭 API 배포&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;업데이트&lt;/td&gt;
&lt;td&gt;수동 재학습&amp;middot;재배포&lt;/td&gt;
&lt;td&gt;&lt;b&gt;자동 파이프라인&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;재학습&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모델 관리&lt;/td&gt;
&lt;td&gt;실험 이력 수동 기록&lt;/td&gt;
&lt;td&gt;v1~v5 전 과정 히스토리 자동 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;안정성&lt;/td&gt;
&lt;td&gt;로컬 하드웨어 리스크 존재&lt;/td&gt;
&lt;td&gt;Azure 인프라 기반 24/7 보장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로컬 트랙은 세밀한 커스텀 로직(시계열 검증, Grad-CAM)의 실험장으로, Azure 트랙은 배포&amp;middot;확장성 검증용으로 역할을 분담했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. 데이터 파이프라인: 공개 데이터 + 생성형 AI 증강&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;fire / smoke / negative(일상) 3클래스 체계로 데이터를 구축했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;공개 데이터셋&lt;/b&gt;: AI Hub의 산불&amp;middot;화재 CCTV 데이터셋 2종 + Kaggle 화재 이미지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;생성형 AI 증강&lt;/b&gt;: 공개 데이터에 부족한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;도시 화재, 공장 화재, 특수 연기&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;시나리오는 이미지 생성 AI로 보충. fire/smoke 각 300장 수준으로 클래스 균형을 맞춤&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하드 네거티브 수집&lt;/b&gt;: 노을, 조명, 전조등 등 화재와 혼동하기 쉬운 이미지를 negative로 집중 수집 (어떤 결과가 나왔는지는 2편에서 계속)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-07 오전 2.58.38.png&quot; data-origin-width=&quot;1165&quot; data-origin-height=&quot;513&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bw6VC5/dJMcajv6vPw/JdgJPVwij6dA95PyJ2akek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bw6VC5/dJMcajv6vPw/JdgJPVwij6dA95PyJ2akek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bw6VC5/dJMcajv6vPw/JdgJPVwij6dA95PyJ2akek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbw6VC5%2FdJMcajv6vPw%2FJdgJPVwij6dA95PyJ2akek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1165&quot; height=&quot;513&quot; data-filename=&quot;스크린샷 2026-07-07 오전 2.58.38.png&quot; data-origin-width=&quot;1165&quot; data-origin-height=&quot;513&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. 베이스라인 검증: Custom Vision의 '70% 벽'&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;본격 개발 전, 노코드 도구인 Azure Custom Vision으로 베이스라인을 측정했다. 세 번의 반복(iteration)을 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이 도구로 갈 수 있는 상한선&lt;/b&gt;을 확인했다.&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;단계&lt;/td&gt;
&lt;td&gt;Precision&lt;/td&gt;
&lt;td&gt;Recall&lt;/td&gt;
&lt;td&gt;mAP&lt;/td&gt;
&lt;td&gt;비고&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;초기 학습 (임계치 50%)&lt;/td&gt;
&lt;td&gt;44.0%&lt;/td&gt;
&lt;td&gt;74.3%&lt;/td&gt;
&lt;td&gt;64.4%&lt;/td&gt;
&lt;td&gt;클래스 비율 균등에도 정밀도 절반 미만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;임계치 80% 상향&lt;/td&gt;
&lt;td&gt;63.1%&lt;/td&gt;
&lt;td&gt;83.7%&lt;/td&gt;
&lt;td&gt;83.4%&lt;/td&gt;
&lt;td&gt;임계치를 올려야만 60%대 진입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;생성형 AI 증강 후 재학습&lt;/td&gt;
&lt;td&gt;&lt;b&gt;70.5%&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;87.7%&lt;/td&gt;
&lt;td&gt;85.6%&lt;/td&gt;
&lt;td&gt;fire/smoke 각 300장 균형 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;여기서 정체&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;726&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mdV1b/dJMcac4Mv1j/34vvFd443g2fdYrQI8DjYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mdV1b/dJMcac4Mv1j/34vvFd443g2fdYrQI8DjYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mdV1b/dJMcac4Mv1j/34vvFd443g2fdYrQI8DjYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmdV1b%2FdJMcac4Mv1j%2F34vvFd443g2fdYrQI8DjYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;726&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;726&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;634&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciQBfS/dJMcab5TM1G/cJcqHiqDLULkQlXMntJz0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciQBfS/dJMcab5TM1G/cJcqHiqDLULkQlXMntJz0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciQBfS/dJMcab5TM1G/cJcqHiqDLULkQlXMntJz0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciQBfS%2FdJMcab5TM1G%2FcJcqHiqDLULkQlXMntJz0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;634&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;634&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;임계치를 80%까지 올리고 데이터를 증강해도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;정밀도는 70% 부근에서 더 이상 오르지 않았다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;우리의 목표는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;임계치 50%에서 정밀도 70~80% 이상&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 오탐지를 잡겠다는 서비스에서 이 수준의 오보율은 수용 불가능했다. 태깅 기반 자동 학습의 상한을 수치로 확인하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;커스텀 전이학습으로 전환&lt;/b&gt;을 결정했다. 베이스라인을 버리는 결정도, 계측이 있어야 근거가 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;4.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;[Trouble Shooting 1]&lt;/span&gt;&amp;nbsp; 과적합 &amp;mdash; 훈련 85% vs 검증 52%의 간극&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;첫 번째 장애물: 학습이 도중에 죽는 이유&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;첫 커스텀 학습부터 순탄치 않았다. 에폭 중간에 학습이 강제 중단되는 현상이 반복됐는데, 원인은 steps_per_epoch를 수동 계산한 값이 실제 배치 수와 미세하게 어긋난 것이었다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;# Before &amp;mdash; 수동 계산: 실제 데이터 수와 어긋나 학습 중단 유발
history = model.fit(
    train_generator,
    steps_per_epoch=train_generator.samples // train_generator.batch_size,
    validation_data=val_generator,
    validation_steps=val_generator.samples // val_generator.batch_size,
    epochs=20
)

# After &amp;mdash; 스텝 계산을 generator에 위임
history = model.fit(
    train_generator,
    steps_per_epoch=None,
    validation_data=val_generator,
    validation_steps=None,
    epochs=20
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1132&quot; data-origin-height=&quot;986&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l5276/dJMcabZbdWI/SbiE9L6o1KMDPzFy5gMkw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l5276/dJMcabZbdWI/SbiE9L6o1KMDPzFy5gMkw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l5276/dJMcabZbdWI/SbiE9L6o1KMDPzFy5gMkw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl5276%2FdJMcabZbdWI%2FSbiE9L6o1KMDPzFy5gMkw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1132&quot; height=&quot;986&quot; data-origin-width=&quot;1132&quot; data-origin-height=&quot;986&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;두 번째 장애물: 숫자가 말해주는 과적합&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;학습 루프를 안정화하고 20에폭을 완주하자, 이번엔 결과가 문제였다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;훈련 정확도 85.3% / 검증 정확도 52.6% (val_loss 1.09)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 전형적인 과적합이다. 학습 곡선을 보면 훈련 정확도는 꾸준히 오르는데 검증 정확도는 0.5 부근에서 요동치고, 검증 Loss는 오히려 발산한다. 이 모델은 학습 데이터를 암기했을 뿐, 새로운 화면을 판별하지 못한다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;870&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bi8g9w/dJMcag7a0tk/vKsKX2MZwg9UEvd5uUTUQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bi8g9w/dJMcag7a0tk/vKsKX2MZwg9UEvd5uUTUQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bi8g9w/dJMcag7a0tk/vKsKX2MZwg9UEvd5uUTUQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbi8g9w%2FdJMcag7a0tk%2FvKsKX2MZwg9UEvd5uUTUQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;870&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;870&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;891&quot; data-origin-height=&quot;415&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N4Sw8/dJMcac4Mv4h/lk7YkQNyQ02GP0k9T4fqv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N4Sw8/dJMcac4Mv4h/lk7YkQNyQ02GP0k9T4fqv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N4Sw8/dJMcac4Mv4h/lk7YkQNyQ02GP0k9T4fqv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN4Sw8%2FdJMcac4Mv4h%2Flk7YkQNyQ02GP0k9T4fqv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;891&quot; height=&quot;415&quot; data-origin-width=&quot;891&quot; data-origin-height=&quot;415&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;원인 분해&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 부족&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 클래스당 수백 장 수준으로 심층 CNN을 밑바닥부터 학습시키기엔 절대량 부족&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클래스 불균형&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 특정 클래스로 판정이 쏠리는 통계적 편향 유발&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배경 간섭&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 모델이 불꽃이 아닌 배경 특징을 학습&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Solution&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fire / smoke / negative를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;유사 비율로 재밸런싱&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;image_dataset_from_directory 기반 TensorFlow 데이터셋 파이프라인으로 로딩 표준화&lt;/li&gt;
&lt;li&gt;밑바닥 학습 대신&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ImageNet 사전학습 가중치 기반 전이학습(MobileNetV2)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;구조로 전환&lt;br /&gt;&amp;mdash; 적은 데이터로도 일반화 성능을 확보하는 정석 경로&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;5.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;[Trouble Shooting 2]&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;데이터 정제 4연전 &amp;mdash; 학습을 멈추는 것은 모델이 아니라 데이터였다&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전이학습 전환 후에도 학습 중단이 반복됐다. 원인은 전부&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터 품질&lt;/b&gt;이었고, 네 차례에 걸쳐 파이프라인을 방어했다. 실제로 이후 ResNet 학습 로그에서도 Premature end of JPEG data 에러가 학습 도중 출력되는 것을 확인할 수 있다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;mdash; 손상 파일은 언제든 파이프라인을 습격한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 153px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;회차&lt;/td&gt;
&lt;td&gt;증상&lt;/td&gt;
&lt;td&gt;원인&lt;/td&gt;
&lt;td&gt;해결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1차&lt;/td&gt;
&lt;td&gt;InvalidArgumentError (Input is empty)&lt;/td&gt;
&lt;td&gt;0KB&amp;middot;비정상 파일 혼입&lt;/td&gt;
&lt;td&gt;os.path.getsize로 100바이트 미만 파일 자동 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2차&lt;/td&gt;
&lt;td&gt;Operation timed out (Errno 60)&lt;/td&gt;
&lt;td&gt;iCloud 동기화 폴더의 파일 접근 지연&lt;/td&gt;
&lt;td&gt;cache() + prefetch()로 메모리 선적재&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3차&lt;/td&gt;
&lt;td&gt;Invalid JPEG data&lt;/td&gt;
&lt;td&gt;헤더는 정상이나 끝이 잘린 손상 JPEG&lt;/td&gt;
&lt;td&gt;Pillow img.verify() 전수 조사 + tf.data.experimental.ignore_errors()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4차&lt;/td&gt;
&lt;td&gt;cv2.imread가 None 반환&lt;/td&gt;
&lt;td&gt;한글 파일명 경로 인식 실패&lt;/td&gt;
&lt;td&gt;바이너리로 읽어 메모리에서 디코딩하는 커스텀 로더&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4차의 한글 경로 문제는 국내 데이터셋을 다룰 때 반드시 만나는 함정이라 해결 코드를 남긴다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;읽기만 우회하는 게 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;정규 JPEG로 재인코딩해 덮어써서 문제를 근본 제거&lt;/b&gt;하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;import os, cv2
import numpy as np

for root, dirs, files in os.walk(data_dir):
    for file in files:
        if file.lower().endswith(('.jpg', '.jpeg', '.png')):
            file_path = os.path.join(root, file)
            # 1. 한글 경로 대응: 바이너리로 읽어 메모리에서 디코딩
            with open(file_path, &quot;rb&quot;) as f:
                chunk = np.frombuffer(f.read(), dtype=np.uint8)
                img = cv2.imdecode(chunk, cv2.IMREAD_COLOR)
            if img is not None:
                # 2. 깨끗한 정규 JPEG로 재인코딩 후 덮어쓰기
                ok, buffer = cv2.imencode(&quot;.jpg&quot;, img)
                if ok:
                    with open(file_path, &quot;wb&quot;) as f:
                        f.write(buffer)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기에 AUTOTUNE 프리페치를 더해 CPU 전처리와 GPU 연산이 겹치도록 최적화 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;일부 파일이 깨져 있어도 멈추지 않고(안정성), 유휴 없이 도는(효율성)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;학습 파이프라인을 완성했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 시점의 MobileNetV2 재학습 결과:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;훈련 93.9% / 검증 94.6% (val_loss 0.1667)&lt;/b&gt;. 검증 곡선이 훈련 곡선과 나란히 수렴하며 과적합 구간을 완전히 탈출했다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;919&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crYPMC/dJMcabdGkXg/KKPuINKfcBzC9pQkNLqbr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crYPMC/dJMcabdGkXg/KKPuINKfcBzC9pQkNLqbr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crYPMC/dJMcabdGkXg/KKPuINKfcBzC9pQkNLqbr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrYPMC%2FdJMcabdGkXg%2FKKPuINKfcBzC9pQkNLqbr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;919&quot; height=&quot;480&quot; data-origin-width=&quot;919&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;891&quot; data-origin-height=&quot;413&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/edc2bs/dJMcacRdzYP/S35k4bCHzTfURWQOTFqfiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/edc2bs/dJMcacRdzYP/S35k4bCHzTfURWQOTFqfiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/edc2bs/dJMcacRdzYP/S35k4bCHzTfURWQOTFqfiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fedc2bs%2FdJMcacRdzYP%2FS35k4bCHzTfURWQOTFqfiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;891&quot; height=&quot;413&quot; data-origin-width=&quot;891&quot; data-origin-height=&quot;413&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;6. 아키텍처 선정: MobileNetV2 vs ResNet50&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;동일 데이터로 두 백본을 비교 실험했다. 모델 설계에서 눈여겨볼 부분은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;전처리를 모델 그래프 내부에 Lambda 층으로 내장&lt;/b&gt;했다는 점이다 &amp;mdash; 학습과 서빙이 같은 전처리 코드를 타게 하려는 의도였다(이 설계가 2편의 전처리 중첩 버그와 어떻게 얽히는지는 뒤에서 밝혀진다).&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;# ResNet50 모델 설계
base_model = ResNet50(weights='imagenet', include_top=False, input_shape=(224, 224, 3))
base_model.trainable = False

model = models.Sequential([
    layers.Input(shape=(224, 224, 3)),
    layers.Lambda(preprocess_input, name='resnet_preprocess'),  # 전처리를 모델 내부에 내장
    base_model,
    layers.GlobalAveragePooling2D(),
    layers.Dense(128, activation='relu'),   # ResNet 깊이에 맞춰 노드 확장
    layers.Dropout(0.5),
    layers.Dense(3, activation='softmax')   # fire / smoke / negative
])
&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;지표&lt;/td&gt;
&lt;td&gt;MobileNetV2&lt;/td&gt;
&lt;td&gt;ResNet50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;훈련 정확도&lt;/td&gt;
&lt;td&gt;93.92%&lt;/td&gt;
&lt;td&gt;94.38%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;검증 정확도&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;94.64%&lt;/td&gt;
&lt;td&gt;&lt;b&gt;95.24%&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;검증 Loss&lt;/td&gt;
&lt;td&gt;0.1667&lt;/td&gt;
&lt;td&gt;&lt;b&gt;0.1178&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;특성&lt;/td&gt;
&lt;td&gt;경량&amp;middot;저지연 (엣지 디바이스 지향)&lt;/td&gt;
&lt;td&gt;깊은 레이어로 정밀 특징 추출 (서버 지향)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;870&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xDzlg/dJMcac4Mv57/yAuGKZekMA54mp4v0D7lKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xDzlg/dJMcac4Mv57/yAuGKZekMA54mp4v0D7lKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xDzlg/dJMcac4Mv57/yAuGKZekMA54mp4v0D7lKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxDzlg%2FdJMcac4Mv57%2FyAuGKZekMA54mp4v0D7lKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;870&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;870&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;화재 감지는 프레임당 수 ms를 아끼는 것보다&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;오보 한 건을 줄이는 것이 가치가 큰 도메인&lt;/b&gt;이고, 추론 환경도 서버급이었다. 정확도&amp;middot;Loss 모두 우위인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ResNet50을 채택&lt;/b&gt;했다. 오탐지 단골손님인 연기/안개 구분 테스트에서도 실제 화재 현장의 연기는 Smoke 98.24%, 산안개는 Negative 90.25%로 안정적인 판별을 확인했다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1264&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQtOtD/dJMcaccEBvv/MZM0AnBhkXNvUAo8Qhcvo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQtOtD/dJMcaccEBvv/MZM0AnBhkXNvUAo8Qhcvo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQtOtD/dJMcaccEBvv/MZM0AnBhkXNvUAo8Qhcvo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQtOtD%2FdJMcaccEBvv%2FMZM0AnBhkXNvUAo8Qhcvo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1264&quot; height=&quot;648&quot; data-origin-width=&quot;1264&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;7. 2단계 전이학습 파인튜닝 &amp;mdash; 얼리고, 다시 녹인다&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ResNet50 기반으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Freeze &amp;rarr; Fine-tune 2단계 학습 전략&lt;/b&gt;을 적용했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;# 1단계: 백본 동결, 상부 레이어만 학습
model.fit(train_ds, validation_data=val_ds,
          epochs=10, class_weight=class_weights)

# 2단계: 백본 상위 블록만 해동 후 초저속 파인튜닝
base_model.trainable = True
for layer in base_model.layers[:-30]:
    layer.trainable = False          # 하위 레이어는 유지

model.compile(optimizer=tf.keras.optimizers.Adam(1e-5),  # LR 대폭 축소
              loss='categorical_crossentropy',
              metrics=['accuracy', 'precision', 'recall'])
&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;단계&lt;/td&gt;
&lt;td&gt;Epoch&lt;/td&gt;
&lt;td&gt;정확도&lt;/td&gt;
&lt;td&gt;Loss&lt;/td&gt;
&lt;td&gt;비고&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1단계 (Freeze)&lt;/td&gt;
&lt;td&gt;8/10&lt;/td&gt;
&lt;td&gt;96.9%&lt;/td&gt;
&lt;td&gt;0.0773&lt;/td&gt;
&lt;td&gt;기본 분류 능력 확보&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2단계 (Fine-tune)&lt;/td&gt;
&lt;td&gt;19/20&lt;/td&gt;
&lt;td&gt;&lt;b&gt;97.5%&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;0.1013&lt;/td&gt;
&lt;td&gt;화재 특화 정밀도 향상&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사전학습 특징을 보존한 채(1단계) 화재 도메인에 최상위 블록만 미세 적응(2단계)시키는 구조로, ImageNet의 일반 시각 지식과 화재 특화 판별력을 모두 확보했다. 여기서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;입력 전처리 방식(Squash &amp;rarr; Padding)까지 교정하면 검증 정확도는 98.15%, Loss는 0.0586까지 개선&lt;/b&gt;되는데, 이 전처리 이야기는 실전 오탐 문제와 얽혀 있어 2편에서 상세히 다룬다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;8. Azure ML 트랙: &quot;데이터를 늘렸더니 성능이 떨어졌다&quot;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;병행 중이던 Azure ML Designer 트랙도 순탄치 않았다. 첫 파이프라인은 Train PyTorch Model 단계에서 실패했는데, 원인은 로컬 트랙과 동일한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이미지 해상도&amp;middot;전처리 부재&lt;/b&gt;였다. 디자이너에 Init Image Transformation &amp;rarr; Apply Image Transformation 모듈을 삽입해 파이프라인 안에서 전처리를 수행하도록 수정하자 학습이 완주됐고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;v1 Baseline 정확도 0.8939&lt;/b&gt;를 확보했다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;765&quot; data-origin-height=&quot;743&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDC8Qb/dJMcai4Wvm5/KM9tlOm8xb0FCKTpIK0ALK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDC8Qb/dJMcai4Wvm5/KM9tlOm8xb0FCKTpIK0ALK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDC8Qb/dJMcai4Wvm5/KM9tlOm8xb0FCKTpIK0ALK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDC8Qb%2FdJMcai4Wvm5%2FKM9tlOm8xb0FCKTpIK0ALK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;765&quot; height=&quot;743&quot; data-origin-width=&quot;765&quot; data-origin-height=&quot;743&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;성능 역전 (v1 &amp;rarr; v2)&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이후 데이터를 대량 추가 투입했다. 결과는 교과서적 통념을 깨는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;정확도 0.8939 &amp;rarr; 0.8738, 오히려 2%p 하락&lt;/b&gt;.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;645&quot; data-origin-height=&quot;631&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DSXn2/dJMcagMYaz2/hOjH8Du66tOqylpdawApG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DSXn2/dJMcagMYaz2/hOjH8Du66tOqylpdawApG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DSXn2/dJMcagMYaz2/hOjH8Du66tOqylpdawApG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDSXn2%2FdJMcagMYaz2%2FhOjH8Du66tOqylpdawApG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;645&quot; height=&quot;631&quot; data-origin-width=&quot;645&quot; data-origin-height=&quot;631&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;원인 분석:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 노이즈 간섭&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 양이 늘며 판별을 방해하는 저품질 데이터의 비중도 함께 증가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모델 수용량 한계&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 기존 ResNet18 구조로는 늘어난 데이터의 복잡도를 담지 못함&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;대응 (v3)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ResNet18 &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ResNet50 업그레이드&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(수용량 확대)&lt;/li&gt;
&lt;li&gt;Learning Rate&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;0.0005로 미세 조정&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(안정적 수렴 유도)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결과:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;정확도 0.9078로 반등&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(Macro Recall 0.9102).&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;295&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGknnC/dJMcad3Ewnw/Mtzdl2WxK1ZFWInKkFQ591/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGknnC/dJMcad3Ewnw/Mtzdl2WxK1ZFWInKkFQ591/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGknnC/dJMcad3Ewnw/Mtzdl2WxK1ZFWInKkFQ591/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGknnC%2FdJMcad3Ewnw%2FMtzdl2WxK1ZFWInKkFQ591%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;756&quot; height=&quot;295&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;295&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;데이터는 많을수록 좋다가 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터 양&amp;middot;품질&amp;middot;모델 수용량은 함께 설계해야 하는 삼각 변수&lt;/b&gt;임을 확인한 사례였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Azure 트랙의 v4~v5 최종 고도화(이진 분류 전환, Hard Negative 보강 &amp;rarr; 정확도 0.993)는 2편에서 이어진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기까지 오프라인 지표는 훌륭했다 &amp;mdash; 검증 정확도 95%+, Loss 0.12. 그런데&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;웹캠과 실제 영상에 물리자 오답이 속출했다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;심지어 불이 활활 타는 장면을 Negative로 판정하는 치명적 미탐까지. 지표와 실전의 간극은 어디서 왔을까?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2편(실시간 시스템 편)에서 다루는 내용:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;전처리 중첩&amp;middot;불일치&lt;/b&gt;: 성능 지표는 좋은데 실전에서 틀리는 이유 (Train-Serving Skew) &amp;mdash; 모델 안에 내장한 그 Lambda 전처리 층이 복선이었다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Squash &amp;rarr; Padding&lt;/b&gt;: CNN이 인식하는 형태 특징을 지키는 리사이징&lt;/li&gt;
&lt;li&gt;&lt;b&gt;시계열 신뢰도 엔진&lt;/b&gt;: deque + EWMA + 투표 로직으로 오탐 판정 13.3%p 감소&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Grad-CAM&lt;/b&gt;: &quot;AI가 어디를 보고 판단했는가&quot;를 관리자에게 보여주기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WebSocket 실시간 서빙&lt;/b&gt;: 유튜브 라이브를 프레임 단위로 분석하는 파이프라인&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;[2편 &amp;mdash; 실시간 시스템 편 바로가기]&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>MS ai School</category>
      <author>eun_ll</author>
      <guid isPermaLink="true">https://journal7218.tistory.com/3</guid>
      <comments>https://journal7218.tistory.com/3#entry3comment</comments>
      <pubDate>Sat, 18 Apr 2026 18:20:59 +0900</pubDate>
    </item>
    <item>
      <title>GitHub - 협업 편</title>
      <link>https://journal7218.tistory.com/2</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개발 프로젝트를 하게 되면 필수적으로 사용해야 하는게 깃허브이다. 나는 깃허브를 혼자서만 사용할때와 팀원들과 다같이 쓸 때 알아야할 규칙이 달라 처음에 애를 많이 썼던 기억이 나, 이렇게 정리를 하게 되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  정리: 협업 흐름 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 사용하는 repository를 연결한 후 이 명령어 순서들만 알아도 크게 어려울 건 없다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;repository 연결하기&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;b&gt;repository의 주소는 메인화면&amp;gt;code(초록색버튼)을 누르면 뜬다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xUlaP/dJMcaadVXKb/JW5eIsCE8Mudol8aJseDNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xUlaP/dJMcaadVXKb/JW5eIsCE8Mudol8aJseDNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xUlaP/dJMcaadVXKb/JW5eIsCE8Mudol8aJseDNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxUlaP%2FdJMcaadVXKb%2FJW5eIsCE8Mudol8aJseDNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;249&quot; height=&quot;670&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;670&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1776500991887&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. 작업할 폴더 생성
2. 툴에 그 폴더 열기
3. git clone &amp;lt;사용할 리포지토리의 주소&amp;gt;
4. cd &amp;lt;클론된 폴더명 입력&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;연결이 된 걸 확인 후, 다음과 같은 명령어를 입력해 깃허브에 올린다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. git checkout -b feat/기능명 (브랜치 생성)
2. 기능 개발 &amp;rarr; commit &amp;rarr; push
3. GitHub에서 PR 생성
4. 코드 리뷰 후(생략 가능) merge
5. git pull origin main 받아 최신화
6. git branch로 자신의 브랜치 확인 후 개발 시작
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-path-to-node=&quot;3&quot; data-ke-size=&quot;size26&quot;&gt;Pull Request란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 수정한 코드를 프로젝트의 메인 코드라인(보통 main 또는 master 브랜치)에 &lt;b data-index-in-node=&quot;85&quot; data-path-to-node=&quot;0&quot;&gt;합쳐달라고 요청하는 과정&lt;/b&gt;을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 거치는 이유는 혹시라도 코드가 잘못 올라가면 서로 꼬이게 되면서 코드 충돌 등을 예방할 수 있기 때문이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size16&quot;&gt;단순히 코드를 옮기는 것이 아니라, 팀원들과 소통하고 검토받는 '관문'이라고 이해해야한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;PR은 보통 다음과 같은 단계로 진행된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;5&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,0&quot;&gt;브랜치 생성 및 작업:&lt;/b&gt; 원본 코드에 영향을 주지 않도록 별도의 '브랜치'를 만들어 기능을 개발하거나 버그를 수정&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,0&quot;&gt;Push:&lt;/b&gt; 내 컴퓨터에서 작업한 내용을 깃허브 서버(원격 저장소)로 올린다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,2,0&quot;&gt;PR 생성:&lt;/b&gt; 깃허브 웹사이트에서 &quot;내가 이 브랜치에서 이런 내용을 수정했으니, &lt;b&gt;확인&lt;/b&gt;하고 합쳐줘(Merge)!&quot;라고 버튼을 누른다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,3,0&quot;&gt;코드 리뷰 (Code Review):&lt;/b&gt; 동료들이 코드를 보고 피드백을 남깁니다. &quot;이 부분은 이렇게 고치면 좋겠어요&quot; 같은 리뷰를 단다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,4,0&quot;&gt;머지 (Merge):&lt;/b&gt; 리뷰가 끝나고 문제가 없으면 메인 브랜치에 코드를 최종적으로 합친다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;3&quot; data-ke-size=&quot;size26&quot;&gt;  협업 규칙 예시&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PR 제목 컨벤션: feat: 기능명, fix: 버그명, refactor: 리팩토링&lt;/li&gt;
&lt;li&gt;Commit 메시지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋 메시지는&amp;nbsp;타입: 개발한 내용의 형식을 갖추어 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*주로 feat, fix를 많이 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입 설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 150px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style5&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;feat&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;새로운 기능 추가&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;fix&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;버그 수정&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;refactor&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;코드 리팩토링&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;docs&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;문서 수정 (README 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;style&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;코드 스타일 변경 (세미콜론 추가 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;chore&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;빌드 및 패키지 설정 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;test&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;테스트 코드 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) git commit -m &quot;feat: 화면 전환 기능&quot; / git commit -m &quot;fix: API 응답 오류 수정&quot;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;매 작업 전 git pull origin main(main 브랜치 또는 develop 브랜치 따로 만들어서 작업 추천)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;PR 전 rebase 또는 merge로 충돌 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Git 충돌(Conflict) 해결 프로세스&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계: 상황 파악하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;발생 시점:&lt;/b&gt; 내가 수정한 파일과 깃허브(또는 다른 브랜치)의 수정 사항이 같은 줄을 건드렸을 때 발생.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;신호:&lt;/b&gt; 터미널에 CONFLICT (content): Merge conflict in [파일명] 문구가 뜸.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계: 코드 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VS Code 같은 에디터에서 충돌이 난 파일을 열면 아래와 같은 기호가 보임&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD: 현재 내 브랜치의 변경 사항 (로컬)&lt;/li&gt;
&lt;li&gt;=======: 이 선을 기준으로 위아래가 싸우는 중&lt;/li&gt;
&lt;li&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; [커밋 해시]: 깃허브에서 넘어온 변경 사항&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ 해결법:&lt;/b&gt; 1. 위 기호들을 포함해서 불필요한 코드를 싹 지우기(옛날에 main 브랜치에 있었지만 수정하면서 지운 코드같은 경우) 2. 최종적으로 남길 코드만 남기기&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1. 수정한 파일을 스테이징 
git add [파일명]  # 또는 git add .

2. 해결 완료 커밋 
git commit -m &quot;fix: resolve merge conflict in [파일명]&quot;

3. push해서 브랜치에 다시 올리기 (pull request에서도 merge가 활성화되어있는지 확인)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Git 충돌(Conflict) vscode에서 수정하기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;git checkout main&lt;/b&gt;&lt;/span&gt; - main 브랜치로 이동&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;git pull origin main&lt;/b&gt;&lt;/span&gt; - main 최신화 내려받기&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;git checkout 내브랜치명&lt;/b&gt;&lt;/span&gt; - 내 브랜치로 이동&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;git merge main&lt;/b&gt;&lt;/span&gt; - 내 브랜치와 main 합치기&lt;/li&gt;
&lt;li&gt;충돌된 파일 수정하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후 다시 git add . -&amp;gt;commit-&amp;gt;push 순서&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자주 뜨는 문제&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚠️ Detached HEAD&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;증상:&lt;/b&gt; push 해도 Everything up-to-date라고 뜨면서 반영이 안 됨. &amp;rArr; git branch를 했을때 다음과 같이 뜨면 브랜치 위에 있는게 아닌 특정 커밋 위에 있는 경우임&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dRmwiu/dJMcah5cd0C/uYxVxhDoofBpv2cEzK3C91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dRmwiu/dJMcah5cd0C/uYxVxhDoofBpv2cEzK3C91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dRmwiu/dJMcah5cd0C/uYxVxhDoofBpv2cEzK3C91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdRmwiu%2FdJMcah5cd0C%2FuYxVxhDoofBpv2cEzK3C91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;622&quot; height=&quot;146&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;탈출법:&lt;/b&gt; git checkout [브랜치명]으로 먼저 돌아온 뒤, 작업했던 커밋을 git merge [커밋번호]로 합치기.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚠️ 리베이스(Rebase) 중일 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리베이스 도중 충돌이 나면 이도저도 아닌 상태&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;충돌난 파일들 수정 후, 커밋 대신&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;git rebase --continue&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어 사용하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 꼬여서 뒤로 돌아가야하는 상황 (합치기 전으로 돌아가고 싶을 때)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;git rebase --abort&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하여 리베이스 시작 전으로 돌아가기&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚠️ 충돌 수정 전으로 돌아가고 싶을 때&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;git merge --abort&lt;/b&gt; &lt;/span&gt;머지 취소&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;git reset --hard HEAD&lt;/span&gt; &lt;/b&gt;내 브랜치 상태 초기화( 작업한 내용이 날려도 되는 경우만!! )&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;git checkout main&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;git pull origin main&lt;/b&gt;&lt;/span&gt; main 브랜치 최신 정보 가져오기&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;git checkout 내브랜치명&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;git reset --hard main&lt;/b&gt;&lt;/span&gt; 내 브랜치로 돌아가서 main 최신 내용 덮어쓰기 : main이랑 똑같은 상태되기(작업한 내용이 없을 경우!!)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발</category>
      <category>Git</category>
      <category>github</category>
      <category>프로젝트</category>
      <category>협업</category>
      <author>eun_ll</author>
      <guid isPermaLink="true">https://journal7218.tistory.com/2</guid>
      <comments>https://journal7218.tistory.com/2#entry2comment</comments>
      <pubDate>Fri, 17 Apr 2026 22:40:46 +0900</pubDate>
    </item>
    <item>
      <title>[MS ai school] 최종합격 후기</title>
      <link>https://journal7218.tistory.com/1</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;3학년이 끝난 후... 휴학을 너무 하고 싶었다. 하지만 어떠한 목표를 가지고 의미있는 휴학 생활을 보내면 좋을까 생각한 그때, ai 부트캠프를 하는게 좋다고 생각했다.&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;4&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[현재 나의 상황] &lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;개발과 디자인을 병행하는 학과에서 개발에 중점적으로 초점을 맞춰 공부하고 싶었다. 그런데 우리 과는 개발에 관해 얕은 부분만 배운다. 실직적으로 프론트, 백엔드, 게임 개발 분야를 &lt;b data-index-in-node=&quot;112&quot; data-path-to-node=&quot;4&quot;&gt;찍먹&lt;/b&gt;할 수 있는 기회를 제공해주는 학과였다는 것이다. 나는 이러한 과에서 원하는 개발 분야를 찾고, 역량을 키워나가는건 내가 휴학에서 채워나가는 수밖에 없다고 생각하였다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;백엔드에 관심이 생겼고 백엔드 개발을 1년정도 해본 결과, 현재 사회에서 ai가 급격하게 발전하면서 ai와 같이 접목하여 개발을 하면 좀 더 경쟁력이 생길 거라고 생각했다. 그래서 내가 선택하게 된 건 &lt;b data-index-in-node=&quot;113&quot; data-path-to-node=&quot;5&quot;&gt;ms ai school&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;275&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1Yb3p/dJMcabDLbfi/KE5LYUgcuvaipd3kxcWaD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1Yb3p/dJMcabDLbfi/KE5LYUgcuvaipd3kxcWaD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1Yb3p/dJMcabDLbfi/KE5LYUgcuvaipd3kxcWaD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1Yb3p%2FdJMcabDLbfi%2FKE5LYUgcuvaipd3kxcWaD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;275&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;275&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ms-academy.kr/&quot; data-ved=&quot;0CAAQ_4QMahcKEwj609yNxNqTAxUAAAAAHQAAAAAQXA&quot; data-hveid=&quot;0&quot;&gt;ms ai school 홈페이지 바로가기&lt;/a&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;내가 여기에서 가장 좋다고 생각한 부분은,&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;8&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;직접 microsoft의 azure을 자유롭게 쓸 수 있다는 점 (현업에서 사용하는 ai사용)&lt;/li&gt;
&lt;li&gt;모델과 서비스를 같이 알 수 있는 흐름으로 커리큘럼으로 구성되어있다는 점&lt;/li&gt;
&lt;li&gt;비전공자도 듣기 쉽게 ai를 완전기초부터 설명해준다는 점&lt;/li&gt;
&lt;li&gt;k-digital Training으로 수강료를 국비지원 해준다는 점&lt;/li&gt;
&lt;li&gt;수료하면 자격증 취득 가능&lt;/li&gt;
&lt;li&gt;ms 현직자분들과의 네트워킹 가능 이었다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GViNN/dJMcagE5tDX/2zYnmVkkM16UdIvhRe7nH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GViNN/dJMcagE5tDX/2zYnmVkkM16UdIvhRe7nH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GViNN/dJMcagE5tDX/2zYnmVkkM16UdIvhRe7nH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGViNN%2FdJMcagE5tDX%2F2zYnmVkkM16UdIvhRe7nH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;282&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;282&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;홈페이지에서 잘 보면 커리큘럼도 확인할 수가 있다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ms-academy.kr/29&quot; data-ved=&quot;0CAAQ_4QMahcKEwj609yNxNqTAxUAAAAAHQAAAAAQXQ&quot; data-hveid=&quot;0&quot;&gt;커리큘럼 확인하러 가기&lt;/a&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;10&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[ms ai school 서류&amp;amp;면접] &lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;나는 얼리버드 전형으로 지원하였다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;41&quot; data-path-to-node=&quot;10&quot;&gt;얼리버드 전형이란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;원래 모집 기간보다 일찍 지원을 하는 것을 의미한다. ms ai school은 얼리버드 전형으로 지원한 지원자들에게는 서류전형을 안보고 바로 면접으로 간다. 생각이 있다면 바로 얼리버드 전형으로 지원하는 걸 추천한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFufI4/dJMcaf0qHgG/q9V0KE99TtGpi0iZUlK1B1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFufI4/dJMcaf0qHgG/q9V0KE99TtGpi0iZUlK1B1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFufI4/dJMcaf0qHgG/q9V0KE99TtGpi0iZUlK1B1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFufI4%2FdJMcaf0qHgG%2Fq9V0KE99TtGpi0iZUlK1B1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1278&quot; height=&quot;378&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;378&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;면접 가능 시간을 정한 후, 면접 준비를 바로 하였다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;--면접은 비대면이었다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;면접 대기실에서 간단한 코딩 테스트가 있다는 말에 급하게 ai 기본 언어인 파이썬을 공부하였다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;나는 파이썬을 20살때 이후로 한번도 보지 않았었는데 (그동안 백엔드를 하며 java만 하였다 ㅎㅎ..) 오랜만에 보니까 새로웠다ㅎㅎ 파이썬 코딩 테스트는 말 그대로 기본적인 부분을 물어보았다.(이 기반으로 팀구성? 을 한다고 들었는데 딱히 그러지는 않는다.) 면접 대기실에서 마이크 카메라 확인 후 면접을 보러 들어갔다. 면접 질문은 어렵지 않았다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14&quot;&gt;1. 자기소개&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;8&quot; data-path-to-node=&quot;14&quot;&gt;2. 해본 언어가 있는지&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;22&quot; data-path-to-node=&quot;14&quot;&gt;3. ms ai school에서 기대되는 점&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;47&quot; data-path-to-node=&quot;14&quot;&gt;4&lt;/b&gt;&lt;b data-index-in-node=&quot;47&quot; data-path-to-node=&quot;14&quot;&gt;. 잘 참여하지 않은 팀원이 있다면 어떻게 할건지&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;등등 스펙보다는 얼마나 열정이 있고 참여를 할 수 있는지를 보는거 같았다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;면접은 부담없이 자신이 하고자하는 목표가 뚜렷하다면 부담없이 봐도 될거같다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;면접이 끝난 후 2일 뒤 바로 연락이 왔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1264&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Tzmas/dJMcadnZ4nG/cCDYwKj5zuwgZkUSBzp330/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Tzmas/dJMcadnZ4nG/cCDYwKj5zuwgZkUSBzp330/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Tzmas/dJMcadnZ4nG/cCDYwKj5zuwgZkUSBzp330/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTzmas%2FdJMcadnZ4nG%2FcCDYwKj5zuwgZkUSBzp330%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1264&quot; height=&quot;270&quot; data-origin-width=&quot;1264&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;합격 메일 &lt;/p&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;ms ai school 부트 캠프를 하면서 얼마나 배울지가 기대된다!!!&lt;/p&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;앞으로의 기록을 천천히 해나갈생각이다. 혹시라도 고민이 되는 사람이라면 도움이 되었으면 좋겠다.&lt;/p&gt;</description>
      <category>MS ai School</category>
      <author>eun_ll</author>
      <guid isPermaLink="true">https://journal7218.tistory.com/1</guid>
      <comments>https://journal7218.tistory.com/1#entry1comment</comments>
      <pubDate>Tue, 7 Apr 2026 10:04:48 +0900</pubDate>
    </item>
  </channel>
</rss>