<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>E:\</title>
    <link>https://u-bvm.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 16 Apr 2026 13:55:37 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>BVM</managingEditor>
    <item>
      <title>제11회 빅데이터분석기사 합격 후기</title>
      <link>https://u-bvm.tistory.com/184</link>
      <description>&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 합격했으니 참으로 다행이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;점수&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;491&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QDEaf/dJMcaiaPntF/I6e89xOoJkRyeCZM0ipQy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QDEaf/dJMcaiaPntF/I6e89xOoJkRyeCZM0ipQy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QDEaf/dJMcaiaPntF/I6e89xOoJkRyeCZM0ipQy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQDEaf%2FdJMcaiaPntF%2FI6e89xOoJkRyeCZM0ipQy0%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;491&quot; height=&quot;320&quot; data-origin-width=&quot;491&quot; data-origin-height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;488&quot; data-origin-height=&quot;275&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CIHhx/dJMcag49I0w/4RBa0Nc6KBekZMP9R4WpK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CIHhx/dJMcag49I0w/4RBa0Nc6KBekZMP9R4WpK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CIHhx/dJMcag49I0w/4RBa0Nc6KBekZMP9R4WpK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCIHhx%2FdJMcag49I0w%2F4RBa0Nc6KBekZMP9R4WpK1%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;275&quot; data-origin-width=&quot;488&quot; data-origin-height=&quot;275&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 난 점수는 아니지만...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합격했다는 것이 중요하다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 컴퓨터공학 전공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 파이썬으로 필요한 프로그램 작성해 활용한 경험 有&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 통계 문외한&lt;br /&gt;&amp;nbsp; &amp;nbsp; - 학부 때 통계과목 1학기 수강&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 코드 작성에만 좀 익숙한 완전한 노베이스 상태였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용한 교재&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=343937260&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=343937260&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1766125914562&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;books.book&quot; data-og-title=&quot;2025 이기적 빅데이터분석기사 필기 기본서 | 2025 이기적 빅데이터분석기사  | 나홍석 외&quot; data-og-description=&quot;최신 출제기준을 적용한 도서로, 빅데이터분석기사 필기 시험의 출제 경향을 철저히 분석하여 수험생들이 혼자서도 학습할 수 있도록 한 완벽 대비서이다. 시행처인 한국데이터산업진흥원에서&quot; data-og-host=&quot;www.aladin.co.kr&quot; data-og-source-url=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=343937260&quot; data-og-url=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=343937260&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cjoyrt/hyZPL8Xh4j/JUHMWPNlz0zxlKb2NK5NsK/img.jpg?width=500&amp;amp;height=676&amp;amp;face=0_0_500_676,https://scrap.kakaocdn.net/dn/b5ThGR/hyZP6Zm7aN/r18I82smONDwBNISsGpNM0/img.jpg?width=500&amp;amp;height=676&amp;amp;face=0_0_500_676&quot;&gt;&lt;a href=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=343937260&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=343937260&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cjoyrt/hyZPL8Xh4j/JUHMWPNlz0zxlKb2NK5NsK/img.jpg?width=500&amp;amp;height=676&amp;amp;face=0_0_500_676,https://scrap.kakaocdn.net/dn/b5ThGR/hyZP6Zm7aN/r18I82smONDwBNISsGpNM0/img.jpg?width=500&amp;amp;height=676&amp;amp;face=0_0_500_676');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;2025 이기적 빅데이터분석기사 필기 기본서 | 2025 이기적 빅데이터분석기사 | 나홍석 외&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;최신 출제기준을 적용한 도서로, 빅데이터분석기사 필기 시험의 출제 경향을 철저히 분석하여 수험생들이 혼자서도 학습할 수 있도록 한 완벽 대비서이다. 시행처인 한국데이터산업진흥원에서&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.aladin.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&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;다른 교재나 강의를 구매하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20251219_223626.png&quot; data-origin-width=&quot;683&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/seeUw/dJMcaiBQNdg/30aeOzU4PKe0wcSyJDsfZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/seeUw/dJMcaiBQNdg/30aeOzU4PKe0wcSyJDsfZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/seeUw/dJMcaiBQNdg/30aeOzU4PKe0wcSyJDsfZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FseeUw%2FdJMcaiBQNdg%2F30aeOzU4PKe0wcSyJDsfZ0%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;683&quot; height=&quot;466&quot; data-filename=&quot;20251219_223626.png&quot; data-origin-width=&quot;683&quot; data-origin-height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #c8c3bc; text-align: start;&quot; data-darkreader-inline-color=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;반년을 함께한 이기적 교재로만 해결이 다 됐다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공부 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;필기&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기간&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 공부 기간은 1달 정도.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하루에 2시간 정도 공부했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간을 딱 정해서 공부한 게 아니라, '어느 파트까지 오늘 하겠다'라는 기준을 잡고 진행한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 날은 그 기준을 크게 잡아, 하루에 8시간 이상 공부하는 때도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1회독은 그냥 쭉 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무것도 모르는 상태이므로 끝까지 보면서 얕게나마 기억할 수 있게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 자기가 뭐에 강하고 뭐에 약한지 어느 정도 인식할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1회독을 마치고 기출이나 모의고사를 1 ~ 2회라도 풀어보며 다시 강점과 약점을 점검한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2회독부터 약점 위주로 공략한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 경우는 2, 3파트가 약점이었는데, 실제로 수식을 적용해 계산하는 파트에서 수식이 기억이 안 나 어려웠고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머신러닝에 대해 너무 얕고 부정확하게 알고 있는 것들이 독이 되는 부분이 있었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2회독 이후, 남은 기출과 모의고사를 계속 풀며 깎아나간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찍어서 맞춘 문제, 긴가민가했던 문제나 틀린 문제들은, 다음처럼 해당 개념이 책의 몇 페이지에 나와있는지 체크하고 넘어갔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H5oqS/dJMcaiaPnhA/1Mc1kY5zzanhy9ptVeR1uk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H5oqS/dJMcaiaPnhA/1Mc1kY5zzanhy9ptVeR1uk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H5oqS/dJMcaiaPnhA/1Mc1kY5zzanhy9ptVeR1uk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH5oqS%2FdJMcaiaPnhA%2F1Mc1kY5zzanhy9ptVeR1uk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;688&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;688&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강점이 계속 강점일 수 있게 하고, 약점을 계속 보완하며 최종적으로 깎아나간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교재에 필기 기출은 물론, 충분한 양의 모의고사가 수록돼 있었다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://cbt.youngjin.com/exam/index.php?no=69&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://cbt.youngjin.com/exam/index.php?no=69&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1766125970196&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;이기적 CBT, 영진닷컴&quot; data-og-description=&quot;빅데이터분석 기사 필기 답안 표기란 과목명 문항수 합격점수 1 과목 빅데이터 분석기획 20개 60점 2 과목 빅데이터 탐색 20개 60점 3 과목 빅데이터 모델링 20개 60점 4 과목 빅데이터 결과 해석 20개&quot; data-og-host=&quot;cbt.youngjin.com&quot; data-og-source-url=&quot;https://cbt.youngjin.com/exam/index.php?no=69&quot; data-og-url=&quot;https://cbt.youngjin.com/exam/index.php?no=69&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://cbt.youngjin.com/exam/index.php?no=69&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://cbt.youngjin.com/exam/index.php?no=69&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;이기적 CBT, 영진닷컴&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;빅데이터분석 기사 필기 답안 표기란 과목명 문항수 합격점수 1 과목 빅데이터 분석기획 20개 60점 2 과목 빅데이터 탐색 20개 60점 3 과목 빅데이터 모델링 20개 60점 4 과목 빅데이터 결과 해석 20개&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;cbt.youngjin.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영진닷컴이 제공하는 CBT도 시험 1 ~ 2일 전에 활용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 문제 가짓수가 많지 않으므로 너무 많이 활용하는 것은 진짜 실력을 확인하기 어려워질 수 있으므로 지양하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4번 정도까지만 활용하는 것이 좋다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;평가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;11회 필기는 2, 3파트가 확실히 어려웠다.&lt;br /&gt;생소한&amp;nbsp;내용이&amp;nbsp;많았지만,&amp;nbsp;공부한&amp;nbsp;내용도&amp;nbsp;나왔기&amp;nbsp;때문에,&lt;br /&gt;개념을 확실히 알고 간다면 충분히 합격을 노릴 수 있다고 생각된다.&lt;br /&gt;계산 문제의 비중이 크지 않기 때문에 기조가 유지된다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 암기에 집중하는 것도 괜찮은 것 같다.&lt;br /&gt;&lt;br /&gt;나와 같은 비전공자들은 더 많은 시간을 투자해야 하기 때문에 시간이 매우 부족하다.&lt;br /&gt;공부할 내용이 많고, 유사한 개념들이 많이 등장해 매우 헷갈리게 된다.&lt;br /&gt;나는 조금 급해서 공부를 제대로 못했는데, 2달 정도면 충분한 안전마진인 것 같다.&lt;br /&gt;&lt;br /&gt;특히 이번 11회 필기에서 책에서 본 적 없는 개념이 등장했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 문제를 딱 봤을 때의 당혹감이란...&lt;br /&gt;앞으로의 시험 기조에 대한 힌트를 던졌다는 느낌이 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RPG로 비유해 보자면, &quot;모르면 죽어야지&quot; 식의 패턴과 같다.&lt;br /&gt;이런 것까지 완벽하게 공부하기가 어려울 것이다.&lt;br /&gt;하지만, 이번 시험처럼만 나오게 된다면 책에 있는 개념을 확실히 공부하면 문제없을 것이다.&lt;br /&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실기&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기간&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실기도 필기와 유사하게 한 달 정도 공부했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공부 구간을 정하고 평균 1 ~ 2시간 정도 공부했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부족한 파트를 볼 때는 6 ~ 8시간을 볼 때도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1회독은 그냥 쭉 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필기에서 했던 것처럼 얕게라도 기억하게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N회독은 진행하지 않았는데, 실기 특성상 개념에 대한 N회독은 무의미하다고 생각했기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 바로 기출을 풀어보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 몇 개의 문제들은 오픈북을 한다는 느낌으로 앞의 개념들을 참고하며 풀었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 진행하며 어떤 파트에선 어떤 유형이 주로 나오고, 어떤 패턴이 있는지 파악할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://lab.statisticsplaybook.com/portal/space/bigbungi/home&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;슬기로운 통계생활 빅데이터분석기사 커뮤니티&lt;/a&gt;에서 제공하는 정보들도 활용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이기적 실기 교재 구매를 인증하면 추가 자료도 제공하는 것이 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 기출 복원이 사람마다 다르게 진행되는 만큼,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색을 통해 다양한 실기 복원을 확인하고 풀어보는 연습도 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유사한 문제라도 미묘하게 달라지는 부분이 있어, 간접적으로 다양한 풀이 유형을 경험할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난 책에 뭘 쓰면서 한 게 아니라, 다음과 같이 에디터에 주석과 함께 작성하면서 공부했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;258&quot; data-origin-height=&quot;317&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ArFPm/dJMcafFbdUH/xamfkTHBRuIls4aIabA5yK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ArFPm/dJMcafFbdUH/xamfkTHBRuIls4aIabA5yK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ArFPm/dJMcafFbdUH/xamfkTHBRuIls4aIabA5yK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FArFPm%2FdJMcafFbdUH%2FxamfkTHBRuIls4aIabA5yK%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;258&quot; height=&quot;317&quot; data-origin-width=&quot;258&quot; data-origin-height=&quot;317&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 정리하며 공부했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제는 AI를 통해 작성한 예제고, 체험환경에서 제공하는 문제도 풀이를 저장했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;304&quot; data-origin-height=&quot;352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdSIsc/dJMcabpelBy/MAjfJ29NiSF5ur2IOdHGx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdSIsc/dJMcabpelBy/MAjfJ29NiSF5ur2IOdHGx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdSIsc/dJMcabpelBy/MAjfJ29NiSF5ur2IOdHGx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdSIsc%2FdJMcabpelBy%2FMAjfJ29NiSF5ur2IOdHGx1%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;304&quot; height=&quot;352&quot; data-origin-width=&quot;304&quot; data-origin-height=&quot;352&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 폴더 내용은 이렇게 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;681&quot; data-origin-height=&quot;619&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WCI6x/dJMcad1Bzvr/CGDXpwdID3eZ1A4HLnX1B0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WCI6x/dJMcad1Bzvr/CGDXpwdID3eZ1A4HLnX1B0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WCI6x/dJMcad1Bzvr/CGDXpwdID3eZ1A4HLnX1B0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWCI6x%2FdJMcad1Bzvr%2FCGDXpwdID3eZ1A4HLnX1B0%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;681&quot; height=&quot;619&quot; data-origin-width=&quot;681&quot; data-origin-height=&quot;619&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;572&quot; data-origin-height=&quot;553&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rpbDC/dJMcaf6feok/HSZ0nHEVmUkNbBqMpGttQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rpbDC/dJMcaf6feok/HSZ0nHEVmUkNbBqMpGttQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rpbDC/dJMcaf6feok/HSZ0nHEVmUkNbBqMpGttQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrpbDC%2FdJMcaf6feok%2FHSZ0nHEVmUkNbBqMpGttQk%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;572&quot; height=&quot;553&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;553&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 정답 스크립트 파일에 주석과 함께 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초에 푼 후 정답을 확인했을 때 내가 작성한 코드보다 좋은 코드였다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 내 풀이와 함께 그 코드도 가져와 설명을 추가해 그 코드가 왜 좋은 지도 기록했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;평가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;11회 실기는 평이한 난이도였다고 생각한다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복기는 다음 커뮤니티에 잘 돼 있으니 참고한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://lab.statisticsplaybook.com/portal/space/bigbungi/post/i-oe-11isoe-e-e-i-i-e-i-e-i-i-e&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://lab.statisticsplaybook.com/portal/space/bigbungi/post/i-oe-11isoe-e-e-i-i-e-i-e-i-i-e&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1766147705683&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;슬통LAB - 데이터분석, 통계 커뮤니티&quot; data-og-description=&quot;복기 남기고 슬통 할인쿠폰 &amp;amp; 스벅 기프티콘 받자! 안녕하세요, 슬통연구소 빅데이터 분석기사 커뮤니티 운영진입니다. 시험 보시느라 정말 수고 많으셨습니다! 이번 시험에 어떤 문제가 나왔는&quot; data-og-host=&quot;lab.statisticsplaybook.com&quot; data-og-source-url=&quot;https://lab.statisticsplaybook.com/portal/space/bigbungi/post/i-oe-11isoe-e-e-i-i-e-i-e-i-i-e&quot; data-og-url=&quot;https://lab.statisticsplaybook.com/portal/space/bigbungi/post/i-oe-11isoe-e-e-i-i-e-i-e-i-i-e&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/qZ0x6/hyZPxYDhwM/FP8VROnbSDn25wkBLBeiN1/img.jpg?width=900&amp;amp;height=1200&amp;amp;face=0_0_900_1200,https://scrap.kakaocdn.net/dn/cgAJV9/hyZOPrBW1p/EBk7Vu2UTxEKckQSjKyIsk/img.png?width=400&amp;amp;height=200&amp;amp;face=0_0_400_200,https://scrap.kakaocdn.net/dn/kVFQ7/hyZPJpMqyt/3cssKV0fguxrN9RgOV6810/img.png?width=400&amp;amp;height=200&amp;amp;face=0_0_400_200,https://scrap.kakaocdn.net/dn/eh83gI/hyZPDC5DNe/55K54kfDH0qCiWr2yTGRC0/img.jpg?width=900&amp;amp;height=1200&amp;amp;face=0_0_900_1200&quot;&gt;&lt;a href=&quot;https://lab.statisticsplaybook.com/portal/space/bigbungi/post/i-oe-11isoe-e-e-i-i-e-i-e-i-i-e&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://lab.statisticsplaybook.com/portal/space/bigbungi/post/i-oe-11isoe-e-e-i-i-e-i-e-i-i-e&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/qZ0x6/hyZPxYDhwM/FP8VROnbSDn25wkBLBeiN1/img.jpg?width=900&amp;amp;height=1200&amp;amp;face=0_0_900_1200,https://scrap.kakaocdn.net/dn/cgAJV9/hyZOPrBW1p/EBk7Vu2UTxEKckQSjKyIsk/img.png?width=400&amp;amp;height=200&amp;amp;face=0_0_400_200,https://scrap.kakaocdn.net/dn/kVFQ7/hyZPJpMqyt/3cssKV0fguxrN9RgOV6810/img.png?width=400&amp;amp;height=200&amp;amp;face=0_0_400_200,https://scrap.kakaocdn.net/dn/eh83gI/hyZPDC5DNe/55K54kfDH0qCiWr2yTGRC0/img.jpg?width=900&amp;amp;height=1200&amp;amp;face=0_0_900_1200');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;슬통LAB - 데이터분석, 통계 커뮤니티&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;복기 남기고 슬통 할인쿠폰 &amp;amp; 스벅 기프티콘 받자! 안녕하세요, 슬통연구소 빅데이터 분석기사 커뮤니티 운영진입니다. 시험 보시느라 정말 수고 많으셨습니다! 이번 시험에 어떤 문제가 나왔는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;lab.statisticsplaybook.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1유형&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 온실가스 데이터&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연도별 온실가스 배출량 1위 국가를 구하는 문제였는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 구하는 방법을 기억해내지 못해 억지로 패턴 매칭 코드를 작성해 답을 도출했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에 어떤 값의 최댓값을 구하는 부분은 간단하게 진행됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 결측치를 대체하고 지정된 값을 구하는 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``SimpleImputer``를 사용해 결측치를 처리했다.&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;3. 거래 데이터 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제에서 단 한 곳만 제외하고 나머지 코드는 다 맞게 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거래가 취소한 건의 거래액에는 -1을 곱해 음수로 만드는 문제였는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``loc``을 안 쓰고 무식하게 음수로 만드려고 하니 동작하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766148124326&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Df[df['canceled'] == True]['amount] = - (df['canceled'] == True]['amount])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 하니까 당연히 동작하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pandas에선 이러한 체인 인덱싱을 사용해 슬라이싱 후 값을 할당하려고 하면 문제가 생긴다.&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;``df[df['canceled'] == True]``는 원본 DataFrame df의 복사본(Copy) 일 수도 있고, 뷰(View) 일 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pandas는 이를 보장하지 않는다.&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;두 번째 인덱싱 ``(['amount'] = ...)``에 값을 할당하려고 할 때,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pandas는 할당 대상이 원본 df의 복사본인지 뷰인지 알 수 없으므로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자에게 경고(SettingWithCopyWarning)를 발생시키며 원본 데이터가 변경되지 않을 위험이 있다.&lt;br /&gt;만약 첫 번째 인덱싱 결과가 복사본이었다면, 그 복사본에만 값을 할당하게 되고, 원본 df는 전혀 변경되지 않는다.&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;이는 ``loc``을 통한 명시적 인덱싱으로 해결해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1766148411201&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 'canceled'가 True인 행의 'amount' 열에만 -1을 곱하여 할당
df.loc[df['canceled'] == True, 'amount'] = -df['amount']&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명 공부한 내용이고, 신경 써서 몇 번 확인했던 내용인데도 떠올리지 못해서 틀려버리고 말았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``loc``을 사용해야 하는 문제는 계속 나올 가능성이 높다고 생각하므로, 확실히 공부해 두는 것이 좋겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2유형&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2유형에 대해선 난 다음의 전략을 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;그리드 서치 같은 짓 하지 말고 RF로만 빠르게 작성하고 다른 문제에 시간을 더 투자하겠다.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난 이 전략에 따라 그리드 서치를 실제로 어떻게 활용할지에 대한 고민을 하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터에 결측치도 없어서 그냥 빠르게 ``RandomForestClassifier``를 사용해 간결하게 끝냈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난 검증 데이터 분할 없이 기본으로 주어지는 학습 데이터 모두를 학습에 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나처럼 그냥 탐색 없이 그냥 랜덤 포레스트를 사용한 사람들은 25점을 받았다고 했는데, 나의 경우는 30점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난 아마 분할 없이 데이터를 그대로 다 써서 그랬던 것일지도 모르겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3유형&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 ttest 문제를 하나 틀렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매장 홍보 전후의 매출 차이에 대한 계수를 구하는 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;홍보 전의 평균 고객 당 매출은 35000원이었고, 주어진 데이터는 홍보 후의 고객 별 매출 데이터였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 문제는 다음과 같은 형태로 풀어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766149095925&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import scipy.stats as stats
import pandas as pd

# 예시 데이터 생성
# data = pd.read_csv(&quot;data.csv&quot;)['purchase_amount']
data = [36000, 37000, 34500, 38000, 35500, 36500, 37500, 34000, 39000, 36000]

# 기존 알려진 평균 (홍보 전)
popmean = 35000

# 1. 정규성 검정 (데이터 수가 적을 경우 수행, n &amp;gt; 30이면 생략 가능하기도 함)
# 귀무가설: 데이터가 정규분포를 따른다.
# p-value &amp;gt; 0.05 이면 정규성 만족으로 간주 -&amp;gt; t-test 진행
stat, p_val_norm = stats.shapiro(data)
print(f&quot;Shapiro P-value: {p_val_norm:.4f}&quot;)

# 2. 일표본 t-검정 (One-Sample t-test) 수행
# stats.ttest_1samp(데이터, 비교할_평균)
# 기본값은 양측 검정(two-sided). (단순 차이 확인)
t_statistic, p_value = stats.ttest_1samp(data, popmean)

print(f&quot;T-statistic: {t_statistic:.4f}&quot;)
print(f&quot;P-value: {p_value:.4f}&quot;)

# 3. 결과 해석 (유의수준 0.05 기준)
if p_value &amp;lt; 0.05:
    print(&quot;귀무가설 기각: 홍보 후 구매 금액은 35,000원과 유의미한 차이가 있습니다.&quot;)
else:
    print(&quot;귀무가설 채택: 홍보 후 구매 금액은 35,000원과 차이가 있다고 보기 어렵습니다.&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주어진 비교 데이터가 평균 하나밖에 없는 상황에서 어떤 걸 써야 할지 제대로 기억하지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 난 억지로 평균이 35000인 같은 크기의 배열을 생성해 &quot;독립표본 T-검정&quot;으로 풀었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 오답일 수밖에 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3유형에서 20점을 얻은 것을 보면 다른 2개 문제는 다 제대로 정답을 제시한 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실기에서 중요한 것&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;help()와 dir()의 사용법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 VSCode와 같은 자동완성이 지원되는 에디터에서 코드 작성을 연습했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기에 연습 과정에서 ``help()``와 ``dir()``라는 함수를 사용할 일이 일절 없었다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나도 시험 이틀 정도 전부터 자동완성 없이 문제를 푸는 연습을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 연습하면서 어떤 패키지가 어디에 있고, 함수 이름이 뭐고 정도는 대강 알았기 때문에 큰 문제는 없었다.&lt;/p&gt;
&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시험 시작 전 환경에서 연습하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험 시작이 10시일 때, 9시 30분부터 컴퓨터를 켜고 환경을 점검할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 그냥 멍하게 있는 것이 아니라, ``help()``같은 함수들을 사용하며 감각을 익혀야 한다.&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;p data-ke-size=&quot;size16&quot;&gt;이 시간에 여러 코드를 작성해 테스트해 보면서 나름대로 준비를 한 것이 꽤 도움이 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시험 대비 자료 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험 전까지 계속 머리에 넣기 위해 개념 등을 정리한 자료를 AI를 통해 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766149956506&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# [기본 데이터 처리]
import pandas as pd
import numpy as np

# [데이터 전처리]
from sklearn.preprocessing import LabelEncoder, OneHotEncoder, MinMaxScaler, StandardScaler
from sklearn.impute import SimpleImputer

# [모델링 &amp;amp; 평가]
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.metrics import f1_score, roc_auc_score, accuracy_score, mean_squared_error, r2_score

# [통계 검정 - scipy]
from scipy import stats
stats.ttest_ind, stats.ttest_rel, stats.ttest_1samp # (T-test)
stats.chi2_contingency # (카이제곱)
stats.f_oneway # (ANOVA)
stats.shapiro, stats.kstest # (정규성 검정)
stats.wilcoxon, stats.kruskal # (비모수 검정)
stats.pearsonr # (상관분석)

# [통계 분석 - statsmodels]
import statsmodels.api as sm
from statsmodels.formula.api import ols, logit, glm&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1766149980729&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 3-2. 범주형 (최빈값)
# strategy='most_frequent' (최빈값)
imputer_cat = SimpleImputer(strategy='most_frequent')
df[['cat_col']] = imputer_cat.fit_transform(df[['cat_col']])

# [Tip] 모든 컬럼을 한 번에 채우고 싶을 때 (df[:] 사용)
# df[:] = ... 하면 컬럼명/인덱스 유지하면서 값만 교체됨 (매우 유용!)
imputer = SimpleImputer(strategy='mean')
df[:] = imputer.fit_transform(df)

df_train[:] = imputer.fit_transform(df_train)
df_test[:] = imputer.transform(df_test)

# 4. [Expert] 그룹별 결측치 대치 (9회 기출 패턴)
# 예: '학과'별 '성적'의 평균으로 결측치 채우기
# transform('mean')은 그룹별 평균을 원래 데이터 인덱스에 맞춰 확장해줌
df['score'] = df['score'].fillna(df.groupby('major')['score'].transform('mean'))

# 5. 삭제
df = df.dropna()

# 라벨 인코딩 (범주형 -&amp;gt; 수치형)
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
df['category_col'] = le.fit_transform(df['category_col'])

# 원-핫 인코딩 (주의: Train/Test 컬럼 개수 불일치 해결 필수!)
# [Method 1] 자동 변환 (편리함): 문자열(object) 컬럼 자동 감지
X_train = pd.get_dummies(X_train)
X_test = pd.get_dummies(X_test)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 내용을 꽉 채운 마크다운 문서를 만들고, ``pdf``로 변환해 폰에 저장 후 시험장까지 이동하며 공부했다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 자료 정리뿐만 아니라, 공부하면서도 AI의 덕을 많이 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 &quot;이렇게 해야 정답이다&quot;가 아니라, &quot;왜 이걸 써야만 하는지&quot;에 대해 까지 자세하게 알려주니 공부하기 매우 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼떨결에 준비하게 된 빅데이터 분석기사, 아는 게 없으니 쉽지 않았다.&lt;/p&gt;
&lt;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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험도 좀 변해야 할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험이라는 것의 의도에 대해선 충분히 공감하고 이해하지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI까지 등장해 실생활에 깊이 침투한 상황에서 자동완성 같은 것도 사용하지 못하게 하는 것은 아쉽다는 생각이 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널 환경에서도 Neovim 같은 것들을 사용해서 편리한 개발을 할 수 있는 시대이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TL;DR: 자동완성 정도는 해줘도 되지 않나?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 정처기에 이어, 빅분기까지 따면서 쌍기사가 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 이 필드에서 쌍기사가 얼마나 메리트가 있나 싶지만...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따 둬서 나쁠 건 하나도 없을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 계기로 AI와 관련한 일을 하게 될지도 모르고 말이다.&lt;/p&gt;</description>
      <category>Study/Others</category>
      <author>BVM</author>
      <guid isPermaLink="true">https://u-bvm.tistory.com/184</guid>
      <comments>https://u-bvm.tistory.com/184#entry184comment</comments>
      <pubDate>Fri, 19 Dec 2025 16:07:23 +0900</pubDate>
    </item>
    <item>
      <title>AI로 MSA 서버 만들어 보기 #4 : Write-Behind</title>
      <link>https://u-bvm.tistory.com/183</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 컨셉&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순한 서버는 &quot;DB 트랜잭션이 끝나야만 유저에게 OK를 보낸다&quot;는 &lt;b&gt;Write-Through&lt;/b&gt; 방식을 많이 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 이는 DB가 병목이 될 경우 전체 서버의 반응성을 크게 저하할 수 있다.&lt;br /&gt;&lt;b&gt;Write-Behind (Write-Back)&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;Memory First&lt;/b&gt;: 메모리(또는 빠른 캐시인 Redis)에 먼저 쓰고, 유저에게 즉시 OK를 준다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Disk Later&lt;/b&gt;: 실제 견고한 저장소(RDBMS)에는 별도의 워커가 천천히, 모아서 기록한다.&lt;br /&gt;본 서버는 이 패턴을 &lt;b&gt;Redis Streams&lt;/b&gt;를 통해 구현했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 아키텍처&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled diagram-2025-12-09-080146.png&quot; data-origin-width=&quot;2311&quot; data-origin-height=&quot;331&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DmcyV/dJMcadUMph2/BOK1DPTQn5Q8fltwu1BNSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DmcyV/dJMcadUMph2/BOK1DPTQn5Q8fltwu1BNSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DmcyV/dJMcadUMph2/BOK1DPTQn5Q8fltwu1BNSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDmcyV%2FdJMcadUMph2%2FBOK1DPTQn5Q8fltwu1BNSk%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;2311&quot; height=&quot;331&quot; data-filename=&quot;Untitled diagram-2025-12-09-080146.png&quot; data-origin-width=&quot;2311&quot; data-origin-height=&quot;331&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;Decoupling&lt;/b&gt;: 채팅 로직(Fast)과 저장 로직(Slow)이 완벽히 분리된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Peak Load Handling&lt;/b&gt;: 트래픽이 폭주하면 Redis Stream에 데이터가 쌓일 뿐, 서버의 응답 속도는 느려지지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 생산자 구현 (&lt;code&gt;emit_write_behind_event&lt;/code&gt;)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 코드(&lt;code&gt;server/src/chat/chat_service_core.cpp&lt;/code&gt;)에서 이벤트를 발행하는 부분이다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ChatService::emit_write_behind_event(const std::string&amp;amp; type, 
                                          const std::string&amp;amp; session_id,
                                          ...) {
    // 1. 데이터 준비 (Key-Value Vector)
    std::vector&amp;lt;std::pair&amp;lt;std::string, std::string&amp;gt;&amp;gt; fields;
    fields.emplace_back(&quot;type&quot;, type);
    fields.emplace_back(&quot;ts_ms&quot;, std::to_string(now_ms));
    fields.emplace_back(&quot;session_id&quot;, session_id);
    // ... (user_id, room_id 등 추가 필드)
    // 2. Redis Stream XADD (Non-blocking)
    // &quot;stream:events&quot; 라는 키에 데이터를 append 한다.
    // maxlen을 설정하여 스트림이 무한히 커지는 것을 방지한다. (Ring Buffer 효과)
    redis_-&amp;gt;xadd(&quot;stream:events&quot;, fields, nullptr, 100000 /*maxlen*/, true /*approx*/);
}&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;JSON Free&lt;/b&gt;: 문자열 파싱/생성 비용을 줄이기 위해 Redis의 Native Map 구조를 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Fire and Forget&lt;/b&gt;: &lt;code&gt;xadd&lt;/code&gt;의 결과를 기다리지 않거나, 실패해도 로그만 남기고 넘어간다 (서버 동작이 우선).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Redis Stream&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 사용하는 주요 Redis Stream 커맨드이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;Command&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Usage&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;XADD key * field value ...&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Producer&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;스트림 끝에 새 항목 추가. ID는 자동 생성(&lt;code&gt;timestamp-sequence&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;XREADGROUP GROUP g1 CONSUMER c1 BLOCK 2000 STREAMS key &amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Consumer&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;컨슈머 그룹 &lt;code&gt;g1&lt;/code&gt;의 멤버 &lt;code&gt;c1&lt;/code&gt;이 되어, 아직 아무도 안 읽은(&lt;code&gt;&amp;gt;&lt;/code&gt;) 메시지를 읽어옴. 없으면 2초 대기.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;XACK key g1 id ...&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Consumer&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&quot;처리 완료&quot; 서명. 이 처리가 되어야 PEL(Pending Entries List)에서 제거됨.&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;h2 data-ke-size=&quot;size26&quot;&gt;5. 소비자 구현 (The Worker)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구현체는 &lt;b&gt;독립 프로그램&lt;/b&gt; (&lt;code&gt;wb_worker&lt;/code&gt;)으로 제공된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;scripts/smoke_wb.ps1&lt;/code&gt;을 통해 빌드 및 간단한 스모크 테스트를 수행할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;흐름 (&lt;code&gt;wb_worker&lt;/code&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;Fetch&lt;/b&gt;: &lt;code&gt;XREADGROUP&lt;/code&gt;으로 배치 단위(예: 100개)로 이벤트를 가져온다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Transform&lt;/b&gt;: 이벤트 타입(&lt;code&gt;type=chat_msg&lt;/code&gt;)에 따라 적절한 SQL(&lt;code&gt;INSERT INTO messages ...&lt;/code&gt;)을 생성한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Batch Insert&lt;/b&gt;: DB 부하를 줄이기 위해 &lt;code&gt;INSERT&lt;/code&gt;를 한 번의 트랜잭션으로 묶어서 실행한다 (&lt;code&gt;Bulk Insert&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Acknowledge&lt;/b&gt;: DB 커밋이 성공하면 &lt;code&gt;XACK&lt;/code&gt;를 날려 Redis에서 해당 항목을 처리 완료로 마킹한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 문제 대응&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;메모리에만 썼는데 서버가 죽으면 어떡하지?&quot;&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;Redis Persistence&lt;/b&gt;: Redis 자체의 &lt;b&gt;AOF (Append Only File)&lt;/b&gt; 기능을 켜두면, Redis가 죽었다 살아나도 스트림 데이터는 보존된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Consumer Crash&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;워커가 데이터를 읽어갔는데(&lt;code&gt;XREADGROUP&lt;/code&gt;) DB에 넣기 전에 죽었다면?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;XACK&lt;/code&gt;를 못 보냈으므로 해당 메시지는 &lt;b&gt;PEL (Pending Entries List)&lt;/b&gt;에 남는다.&lt;/li&gt;
&lt;li&gt;워커가 재가동되면 &lt;code&gt;XREAD ... 0&lt;/code&gt; (또는 &lt;code&gt;XAUTOCLAIM&lt;/code&gt;)을 통해 처리되지 않은 메시지를 다시 가져와서 재시도한다. -&amp;gt; &lt;b&gt;At-least-once Delivery&lt;/b&gt; 보장.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Trade-offs&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;Consistency Gap&lt;/b&gt;: 아주 짧은 순간(수 ms ~ 수 초) DB와 캐시(메모리) 간의 데이터 불일치가 존재할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Complexity&lt;/b&gt;: 시스템 구성 요소가 늘어난다. (Redis 관리, 워커 모니터링 필요)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하지만 큰 트래픽을 받는 서버에서 &quot;압도적인 쓰기 성능&quot;을 얻기 위해 이 정도의 복잡도는 충분히 감수할 가치가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;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;클라이언트는 ImGui로 다시 작성하고 나면 그 때 글로 정리해 볼 수 있을 것 같다.&lt;/p&gt;</description>
      <category>Study/C++ &amp;amp; C#</category>
      <author>BVM</author>
      <guid isPermaLink="true">https://u-bvm.tistory.com/183</guid>
      <comments>https://u-bvm.tistory.com/183#entry183comment</comments>
      <pubDate>Tue, 9 Dec 2025 17:11:15 +0900</pubDate>
    </item>
    <item>
      <title>AI로 MSA 서버 만들어 보기 #3 : Gateway</title>
      <link>https://u-bvm.tistory.com/182</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 아키텍처 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gateway는 클라이언트와 서버(Game Server) 사이의 &lt;b&gt;Single Entry Point&lt;/b&gt;이다.&lt;br /&gt;Nginx나 HAProxy 같은 범용 LB와 달리, &lt;b&gt;세션의 상태&lt;/b&gt;를 파악하고 &lt;b&gt;세션 유지(Stickiness)&lt;/b&gt;를 수행하는 커스텀 L7 프록시이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled diagram-2025-12-09-051531.png&quot; data-origin-width=&quot;1186&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfwkXL/dJMcacO9thF/sOMZWWdf7QV8yNjKbluMX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfwkXL/dJMcacO9thF/sOMZWWdf7QV8yNjKbluMX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfwkXL/dJMcacO9thF/sOMZWWdf7QV8yNjKbluMX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfwkXL%2FdJMcacO9thF%2FsOMZWWdf7QV8yNjKbluMX0%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;1186&quot; height=&quot;424&quot; data-filename=&quot;Untitled diagram-2025-12-09-051531.png&quot; data-origin-width=&quot;1186&quot; data-origin-height=&quot;424&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 지속적으로 Redis에 하트비트를 보내 살아있음을 알린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gateway는 Redis에서 서버 상태를 읽고 적절히 트래픽을 분산한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 주요 책임&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;L7 Load Balancing&lt;/b&gt;: 서버들의 부하(접속자 수)를 모니터링하여 최적의 서버로 트래픽을 분산한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Session Binding (Stickiness)&lt;/b&gt;: 유저가 잠시 연결이 끊겼다 재접속해도, 로직 처리를 위해 반드시 이전에 접속했던 서버 인스턴스로 보내준다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지금의 단순한 채팅 서버에선 필요가 없는 기능이긴 하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Connection Management&lt;/b&gt;: 클라이언트 연결(&lt;code&gt;GatewayConnection&lt;/code&gt;)과 백엔드 연결(&lt;code&gt;BackendSession&lt;/code&gt;)을 1:1로 매핑하여 관리한다. (Note: 현재 구현은 Connection Pooling이 아닌, &lt;b&gt;1:1 Bridging&lt;/b&gt; 방식)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&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;두 개의 물리적 소켓&lt;/b&gt;을 유지하고 브릿징한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1. &lt;code&gt;GatewayConnection&lt;/code&gt;: Client-Side&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트와 직접 연결되는 소켓.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Hive&lt;/code&gt;(I/O Engine)에 의해 관리된다.&lt;/li&gt;
&lt;li&gt;패킷이 들어오면 &lt;code&gt;BackendSession&lt;/code&gt;으로 &lt;code&gt;send()&lt;/code&gt; 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2. &lt;code&gt;BackendSession&lt;/code&gt;: Server-Side&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 게임 서버와 연결되는 소켓.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GatewayApp&lt;/code&gt; 내부 클래스로 구현되어 있다.&lt;/li&gt;
&lt;li&gt;패킷이 들어오면 &lt;code&gt;GatewayConnection&lt;/code&gt;으로 반사한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// gateway/src/gateway_connection.cpp
void GatewayConnection::on_read(const uint8_t* data, size_t length) {
    if (!backend_session_) {
        // 첫 패킷이면(아직 목적지 서버가 없으면) 로드밸런싱 수행
        open_backend_session(); 
    }
    // 패킷 전달 (Forwarding)
    backend_session_-&amp;gt;send({data, data + length});
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 로드밸런싱 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;GatewayApp::select_best_server&lt;/code&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;Stickiness Check&lt;/b&gt;: 이 유저가 원래 있던 서버가 있는가? (&lt;code&gt;SessionDirectory&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Liveness Check&lt;/b&gt;: 그 서버가 지금 살아있는가? (&lt;code&gt;BackendRegistry&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Least Connections&lt;/b&gt;: 1, 2가 아니라면, 현재 가장 널널한(접속자가 적은) 서버를 선택.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// gateway/src/gateway_app.cpp
std::optional&amp;lt;pair&amp;lt;string, uint16&amp;gt;&amp;gt; GatewayApp::select_best_server(const string&amp;amp; client_id) {
    auto instances = backend_registry_-&amp;gt;list_instances();

    // 1. 유저의 기존 서버 확인 (Redis 조회)
    if (auto backend_id = session_directory_-&amp;gt;find_backend(client_id)) {
        // ...Alive Check...
        return {host, port}; // Sticky Connection
    }
    // 2. 가장 접속자 적은(Least Connections) 서버 선택
    std::sort(available.begin(), available.end(), [](auto&amp;amp; a, auto&amp;amp; b) {
        return a.active_sessions &amp;lt; b.active_sessions;
    });

    return {available.front().host, available.front().port};
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Session Stickiness&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 활용한 분산 세션 관리 시스템이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1. The &quot;Split-Brain&quot; Strategy (Cache + Redis)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 Redis 조회는 매번 RTT가 발생하여 느릴 수 있다. &lt;code&gt;SessionDirectory&lt;/code&gt;는 &lt;b&gt;Local Mutex Cache&lt;/b&gt;와 &lt;b&gt;Redis&lt;/b&gt;를 혼용한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;Layer&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;역할&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;L1: Local Cache&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;std::unordered_map&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;락이 걸려있지만 가장 빠름. 짧은 TTL.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;L2: Redis&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;set_if_not_exists&lt;/code&gt; (SETNX)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;다른 게이트웨이와 정보 공유. Single Source of Truth.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// gateway/src/session_directory.cpp
std::optional&amp;lt;std::string&amp;gt; SessionDirectory::find_backend(const std::string&amp;amp; client_id) {
    // 1. 로컬 캐시 확인
    {
        std::lock_guard lk(mutex_);
        if (cache_.has(client_id)) return cache_.get(client_id);
    }

    // 2. Redis 확인 (Cache Miss)
    auto value = redis_-&amp;gt;get(&quot;gateway/session/&quot; + client_id);
    if (value) {
        // 캐시 채우기 (Read-Repair)
        update_cache(client_id, *value);
    }
    return value;
}&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;Design Decision&lt;/b&gt;: 게이트웨이 인스턴스가 여러 대여도, 유저는 &lt;b&gt;어떤 게이트웨이로 접속하든 항상 자신의 서버로 라우팅&lt;/b&gt;된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 부트스트랩&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;GatewayApp&lt;/code&gt; 생성 시 인프라를 설정한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;환경 변수&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;기본값&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;GATEWAY_LISTEN&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;0.0.0.0:6000&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;클라이언트를 기다릴 주소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;GATEWAY_ID&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;gateway-default&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;게이트웨이 식별자 (로그/메트릭용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;REDIS_URI&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;127.0.0.1:6379&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;서버 레지스트리 및 세션 정보 조회용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;METRICS_PORT&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Prometheus Exporter 포트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 트래픽 흐름&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Client Connect&lt;/b&gt;: 클라이언트와 TCP 연결이 수립된다. -&amp;gt; &lt;code&gt;GatewayConnection&lt;/code&gt; 생성.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;First Packet Inspection&lt;/b&gt;: 첫 데이터 패킷을 분석한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Login Frame&lt;/b&gt;: &lt;code&gt;MSG_LOGIN_REQ&lt;/code&gt; 헤더가 보이면 유저 ID를 추출한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Legacy Token&lt;/b&gt;: &lt;code&gt;token:client_id&lt;/code&gt; 포맷인지 확인한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Anonymous&lt;/b&gt;: 둘 다 아니면 익명 접속으로 간주한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재는 무조건 익명 접속으로 진행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Routing&lt;/b&gt;: &lt;code&gt;GatewayConnection&lt;/code&gt;이 &lt;code&gt;GatewayApp::create_backend_session&lt;/code&gt;을 호출한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Backend Selection&lt;/b&gt;: &lt;code&gt;select_best_server&lt;/code&gt;가 Redis를 뒤져서 목적지 서버(예: &lt;code&gt;10.0.0.4:5000&lt;/code&gt;)를 찾는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Connect Backend&lt;/b&gt;: &lt;code&gt;BackendSession&lt;/code&gt;을 만들고 &lt;code&gt;async_connect&lt;/code&gt;를 시도한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Bridge Established&lt;/b&gt;: 연결이 맺어지면, 클라이언트가 보낸 첫 패킷을 백엔드로 &lt;code&gt;flush&lt;/code&gt; 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Proxying&lt;/b&gt;: 이후 양쪽에서 오는 모든 데이터(&lt;code&gt;on_read&lt;/code&gt;)를 상대방에게 &lt;code&gt;send&lt;/code&gt; 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;7.1. Graceful Shutdown&lt;code&gt;GatewayApp&lt;/code&gt;은 &lt;code&gt;SIGINT&lt;/code&gt;, &lt;code&gt;SIGTERM&lt;/code&gt; 시그널을 감지하면 &lt;code&gt;stop()&lt;/code&gt;을 호출하여 모든 세션을 정상 종료하고 리소스를 정리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. Observability&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게이트웨이 자체도 모니터링이 필요하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Log&lt;/b&gt;: 주요 이벤트(Backend Connect/Close, Auth Fail)를 콘솔에 남긴다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Metrics&lt;/b&gt;: &lt;code&gt;http://gateway:9091/metrics&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;gateway_sessions_active&lt;/code&gt;: 현재 중계 중인 연결 수.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;// gateway/src/gateway_app.cpp
metrics_server_ = std::make_unique&amp;lt;MetricsHttpServer&amp;gt;(port, [this]() {
    // 세션 맵의 크기를 실시간으로 반환
    return &quot;gateway_sessions_active &quot; + std::to_string(sessions_.size()); 
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Study/C++ &amp;amp; C#</category>
      <category>C++</category>
      <category>gateway</category>
      <author>BVM</author>
      <guid isPermaLink="true">https://u-bvm.tistory.com/182</guid>
      <comments>https://u-bvm.tistory.com/182#entry182comment</comments>
      <pubDate>Tue, 9 Dec 2025 15:04:31 +0900</pubDate>
    </item>
    <item>
      <title>AI로 MSA 서버 만들어 보기 #2 : Server</title>
      <link>https://u-bvm.tistory.com/181</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 Core 라이브러리에 대해 설명했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기선 해당 라이브러리를 활용해 작성된 Server에 대해 얘기해 보자.&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;Server는 &lt;b&gt;&quot;Massively Chat Server&quot;&lt;/b&gt;를 지향하며, 다음 3가지 핵심 철학을 지향한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Gateway-Centric Architecture&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트는 오직 Gateway(w/ L7 Load Balancer)하고만 연결한다.&lt;/li&gt;
&lt;li&gt;Gateway는 뒷단에 있는 N개의 Server Node 중 하나로 트래픽을 전달한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Effect&lt;/b&gt;: 서버 노드 추가/삭제가 자유롭고(Elasticity), 클라이언트는 IP 변경 없이 그대로 사용 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Safety First (Concurrency)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Global Mutex + JobQueue&lt;/b&gt;: 모든 패킷 처리는 I/O 스레드에서 분리되어 &lt;code&gt;JobQueue&lt;/code&gt;를 통해 Worker 스레드에서 실행된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Strand per Room&lt;/b&gt;: 단일 방(Room)에 대한 처리는 &lt;code&gt;boost::asio::strand&lt;/code&gt;를 통해 순차 실행을 보장하여, Lock 경합을 줄이면서도 동시성 문제(Race Condition)를 원천 차단한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Hybrid State Management&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Memory&lt;/b&gt;: 빠른 응답을 위해 방 참여자 목록(&lt;code&gt;state_.rooms&lt;/code&gt;)은 메모리에 들고 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Redis&lt;/b&gt;: 서버 간 동기화, 서비스 발견(Registry), 세션 스티키니스(Stickiness)는 Redis에 위임(Gateway에서 처리).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB (Postgres)&lt;/b&gt;: 영구 보존이 필요한 데이터(메시지 로그, 유저 정보)는 DB에 저장하되, &lt;b&gt;Write-Behind&lt;/b&gt; 패턴으로 지연 기록하여 성능 저하를 막는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 디렉토리 및 모듈 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스 코드는 ``server/src`` 하위에 기능별로 분리되어 있다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;server/src/
├── app/                  # [Application Layer]
│   ├── bootstrap.cpp     # 서버 구동의 시작점 (main 흐름)
│   ├── config.cpp        # 환경변수 파싱 및 Config 객체 생성
│   ├── router.cpp        # Dispatcher(Opcode -&amp;gt; Handler) 매핑
│   └── metrics_server.cpp# Prometheus 메트릭 수집 서버 (HTTP)
├── chat/                 # [Business Logic Layer]
│   ├── chat_service_core.cpp # ChatService 클래스 공통 로직, 멤버 변수
│   ├── handlers_login.cpp    # 로그인 처리
│   ├── handlers_join.cpp     # 방 입장 처리
│   ├── handlers_leave.cpp    # 방 퇴장 처리
│   ├── handlers_chat.cpp     # 메시지 전송, 귓속말, 슬래시 커맨드
│   ├── handlers_ping.cpp     # Ping/Pong (Liveness Check)
│   └── session_events.cpp    # 세션 종료(Disconnect) 처리
├── state/                # [Distributed State Layer]
│   └── instance_registry.cpp # Redis 기반 서버 인스턴스 등록/조회
└── storage/              # [Infrastructure Layer]
    ├── postgres/         # PostgreSQL 연결 풀 및 Repository 구현
    └── redis/            # Redis 클라이언트 Wrapper&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 부트스트랩 시퀀스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 시작될 때(``bootstrap.cpp``) 수행되는 **초기화 시퀀스**는 시스템의 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나라도 실패하면 서버는 뜨지 못한다 (Fail-Fast).&lt;/p&gt;
&lt;table style=&quot;height: 390px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;th style=&quot;height: 18px;&quot; align=&quot;left&quot;&gt;Step&lt;/th&gt;
&lt;th style=&quot;height: 18px;&quot; align=&quot;left&quot;&gt;Components&lt;/th&gt;
&lt;th style=&quot;height: 18px;&quot; align=&quot;left&quot;&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;io_context&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;Boost.Asio의 핵심 객체. 모든 비동기 작업(Timer, Network I/O)이 여기에 등록된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;JobQueue&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;server::core::JobQueue&lt;/code&gt;를 생성한다. 네트워크 I/O 스레드가 로직 처리로 바빠지는 것을 막기 위해, 실제 로직은 이 큐를 통해 Worker 스레드로 넘긴다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;ServiceRegistry&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot; align=&quot;left&quot;&gt;전역 의존성 주입(DI) 컨테이너(&lt;code&gt;services::set()&lt;/code&gt;)를 초기화한다. 싱글톤 패턴보다 테스트 용이성이 뛰어남.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;DB Pool&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot; align=&quot;left&quot;&gt;PostgreSQL Connection Pool을 생성하고 &lt;code&gt;health_check()&lt;/code&gt;를 수행한다. DB가 죽어있으면 서버도 시작하지 않는다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;Redis Client&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;Redis 연결을 맺고 &lt;code&gt;PING&lt;/code&gt;을 날려 확인한다. Redis 없이는 서버 구실을 못하므로 필수 체크 항목이다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 36px;&quot;&gt;
&lt;td style=&quot;height: 36px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;6&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 36px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;InstanceRegistry&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 36px;&quot; align=&quot;left&quot;&gt;내 서버의 ID와 IP/Port 정보를 담은 &lt;code&gt;InstanceRecord&lt;/code&gt;를 생성하고, Redis에 등록할 준비를 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;7&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;Scheduler&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;주기적인 작업(Heartbeat, Metric Tick)을 수행할 &lt;code&gt;TaskScheduler&lt;/code&gt;를 시작한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;8&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;ChatService&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;로직의 본체인 &lt;code&gt;ChatService&lt;/code&gt;를 생성한다. 이때 DB, Redis, JobQueue 객체를 주입받아 완성다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;9&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;Dispatcher&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;Opcode&lt;/code&gt;와 &lt;code&gt;ChatService&lt;/code&gt;의 메소드를 매핑한다. (예: &lt;code&gt;MSG_CHAT_SEND&lt;/code&gt; -&amp;gt; &lt;code&gt;chat.on_chat_send&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;10&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;Metrics Server&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot; align=&quot;left&quot;&gt;Prometheus가 긁어갈 수 있도록 별도의 포트에 HTTP 서버를 띄운다. 메인 로직과 스레드가 분리되어 있음.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;11&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;Acceptor&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; align=&quot;left&quot;&gt;실제 클라이언트(또는 Gateway)가 접속할 TCP 포트를 연다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 42px;&quot;&gt;
&lt;td style=&quot;height: 42px;&quot; align=&quot;left&quot;&gt;&lt;b&gt;12&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot; align=&quot;left&quot;&gt;&lt;code&gt;Workers &amp;amp; Heartbeat&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 42px;&quot; align=&quot;left&quot;&gt;CPU 코어 수만큼 Worker 스레드를 생성하여 &lt;code&gt;JobQueue&lt;/code&gt;를 소비하기 시작하고, Redis에 첫 Heartbeat를 전송하여 서비스를 개시한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 주요 컴포넌트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1. &lt;code&gt;ChatService&lt;/code&gt;&lt;/h3&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;// server/include/server/chat/chat_service.hpp
class ChatService {
    struct State {
        std::unordered_map&amp;lt;string, RoomSet&amp;gt; rooms; // Memory State
        std::unordered_map&amp;lt;Session*, string&amp;gt; user;
    } state_;

    // Core Dependencies
    server::core::JobQueue&amp;amp; job_queue_;
    std::shared_ptr&amp;lt;IRedisClient&amp;gt; redis_;
    std::shared_ptr&amp;lt;IConnectionPool&amp;gt; db_pool_;
};&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;b&gt;CPU&lt;/b&gt;. 모든 로직(로그인, 채팅, 방 관리)을 총괄하고, DB와 Redis 사이를 중재(Mediator Pattern)한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Design Patterns&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Facade&lt;/b&gt;: 복잡한 서브시스템을 숨기고, 네트워크 계층에는 &lt;code&gt;on_login&lt;/code&gt;, &lt;code&gt;on_chat_send&lt;/code&gt; 같은 단순한 인터페이스만 노출한다. Dispatcher는 내부 구현을 알 필요가 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Mediator&lt;/b&gt;: 컴포넌트 간의 복잡한 통신(N:M 관계)을 한 곳으로 집중시킨다. &quot;유저&quot; 객체는 &quot;Redis&quot;를 모르며 당연히 알 필요가 없다. 오직 &lt;code&gt;ChatService&lt;/code&gt;를 통해서만 상호작용한다. 이를 통해 결합도를 낮출 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2. &lt;code&gt;InstanceRegistry&lt;/code&gt;&lt;/h3&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;// server/src/state/instance_registry.cpp
bool RedisInstanceStateBackend::write_record(const InstanceRecord&amp;amp; record) {
    // 30초 TTL로 내 생존 신고 (Heartbeat)
    return client_-&amp;gt;setex(&quot;server:registry:&quot; + record.instance_id, 30);
}&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;: &quot;나 살아있다&quot;고 Redis에 주기적으로 알린다. (Heartbeat)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이점&lt;/b&gt;: 중앙 관리 서버(Master) 없이도, 모든 서버가 서로의 존재를 알 수 있게 한다. (Peer-to-Peer Discovery) Gateway는 이 정보를 보고 각 서버로 트래픽을 분산시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3. &lt;code&gt;Dispatcher&lt;/code&gt; (Packet Router)&lt;/h3&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;// server/src/app/router.cpp
dispatcher.register_handler(MSG_CHAT_SEND,
    [&amp;amp;chat](Session&amp;amp; s, std::span&amp;lt;const uint8_t&amp;gt; payload) { 
        chat.on_chat_send(s, payload); 
    });&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;code&gt;Opcode&lt;/code&gt;(메시지 ID)와 처리 함수(Handler)를 1:1로 매핑한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구현&lt;/b&gt;: &lt;code&gt;std::unordered_map&lt;/code&gt;이나 배열을 사용하여 O(1) 조회 속도를 보장.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특징&lt;/b&gt;: 핸들러는 &lt;code&gt;std::function&lt;/code&gt;으로 래핑되며, &lt;code&gt;server::app::register_routes&lt;/code&gt; 함수에서 일괄 등록된다. 이를 통해 네트워크 레이어(&lt;code&gt;Core&lt;/code&gt;)와 애플리케이션 레이어(&lt;code&gt;App&lt;/code&gt;)가 분리된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4. &lt;code&gt;MetricsServer&lt;/code&gt; (Observability)&lt;/h3&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;// server/src/app/metrics_server.cpp
void MetricsServer::do_accept() {
    // 9090 포트로 들어오는 HTTP 요청을 수동으로 파싱
    if (target == &quot;/metrics&quot;) {
        auto snap = runtime_metrics::snapshot();
        // Prometheus 포맷 텍스트 생성
        stream &amp;lt;&amp;lt; &quot;chat_job_queue_depth &quot; &amp;lt;&amp;lt; snap.job_queue_depth &amp;lt;&amp;lt; &quot;\n&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할&lt;/b&gt;: Prometheus가 서버 상태를 수집(Scrape)할 수 있는 HTTP 엔드포인트(&lt;code&gt;/metrics&lt;/code&gt;)를 제공한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이점&lt;/b&gt;: 별도의 무거운 웹 프레임워크 없이 &lt;code&gt;boost::asio&lt;/code&gt;만으로 가볍게 구현되었다. Main Loop와는 별도의 스레드에서 돌아가므로, 메인 로직이 바빠도 모니터링은 죽지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 요청 처리 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패킷 하나가 들어왔을 때의 여정을 추적한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Network I/O&lt;/b&gt;: &lt;code&gt;Session::read_loop&lt;/code&gt;가 TCP 버퍼에서 데이터를 읽는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Framing&lt;/b&gt;: &lt;code&gt;PacketHeader&lt;/code&gt;를 파싱하여 패킷을 확실히 식별한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Dispatch&lt;/b&gt;: &lt;code&gt;router.cpp&lt;/code&gt;에 등록된 &lt;code&gt;dispatcher&lt;/code&gt;가 Opcode를 확인한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Job Enqueue&lt;/b&gt;: &lt;code&gt;ChatService::on_chat_send&lt;/code&gt;가 호출되지만, 로직을 직접 수행하지 않고 &lt;b&gt;람다(Lambda)로 감싸서 &lt;code&gt;JobQueue&lt;/code&gt;에 집어넣고 즉시 리턴&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Job Dequeue&lt;/b&gt;: Worker 스레드 중 하나가 람다를 꺼내 실행한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Logic Execution&lt;/b&gt;: 권한 검사, DB 저장, Redis Pub/Sub, 브로드캐스트 패킷 생성이 진행된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Send&lt;/b&gt;: 결과 패킷들을 &lt;code&gt;Session::async_send&lt;/code&gt; 대기열에 넣는다. (IO 스레드가 나중에 모아서 보냄 - Gather Write)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 핸들러&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1. Login Handler (&lt;code&gt;handlers_login.cpp&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 복잡한 핸들러 중 하나로, 세션의 시작을 알린다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Logic&lt;/b&gt;:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;get_or_create_session_uuid&lt;/code&gt;: 현재 TCP 세션에 고유 UUID(v4)를 부여한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ensure_unique_or_error&lt;/code&gt;: 닉네임 중복 체크. &quot;guest&quot;로 들어오면 자동으로 &quot;guest-a1b2...&quot; 같은 이름을 부여한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;state_.mu&lt;/code&gt; Lock: 전역 유저 맵(&lt;code&gt;state_.user&lt;/code&gt;)에 &lt;code&gt;(Session Ptr -&amp;gt; Nickname)&lt;/code&gt; 매핑을 등록한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB Upsert&lt;/b&gt;: &lt;code&gt;users&lt;/code&gt; 테이블에 접속 기록(IP, 시간)을 남기고, 필요하면 신규 유저 레코드를 생성한다. (Postgres UPSERT)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Audit Log&lt;/b&gt;: &quot;누가 접속했다&quot;는 시스템 로그를 로비 채팅창에 날린다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Redis Touch&lt;/b&gt;: &lt;code&gt;presence:user:{uid}&lt;/code&gt; 키에 TTL을 설정하여 &quot;나 온라인이야&quot;라고 표시한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Write-Behind&lt;/b&gt;: &lt;code&gt;emit_write_behind_event(&quot;session_login&quot;, ...)&lt;/code&gt;를 통해 로그인 이벤트를 Redis Stream에 던진다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2. Join Handler (&lt;code&gt;handlers_join.cpp&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 방을 옮길 때 호출된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Logic&lt;/b&gt;:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Redis Password Check&lt;/b&gt;: 입장하려는 방의 비밀번호를 Redis에서 먼저 조회한다. (Redis가 Master of Truth)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;state_.mu&lt;/code&gt; Lock:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 방에서 &lt;code&gt;erase&lt;/code&gt; (퇴장 처리).&lt;/li&gt;
&lt;li&gt;비밀번호 입력값 검증 (&lt;code&gt;hash_room_password&lt;/code&gt;). -&amp;gt; 비밀번호가 없다면 그냥 입장할 수 있다.&lt;/li&gt;
&lt;li&gt;새 방에 &lt;code&gt;insert&lt;/code&gt; (입장 처리).&lt;/li&gt;
&lt;li&gt;만약 이전 방이 비어버렸으면(&lt;code&gt;empty()&lt;/code&gt;), 방 객체를 메모리에서 제거함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Broadcasting&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새 방 유저들에게: &quot;XXX님이 입장했습니다.&quot; 등 전체 공지를 쏘는 식.&lt;/li&gt;
&lt;li&gt;Redis Pub/Sub을 통해 모든 서버에 브로드캐스팅된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Sync Membership&lt;/b&gt;: DB &lt;code&gt;memberships&lt;/code&gt; 테이블에 &lt;code&gt;(user_id, room_id, role)&lt;/code&gt; 정보를 업데이트한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Snapshot&lt;/b&gt;: 클라이언트 UI 갱신을 위해 방 정보, 참여자 목록, 최근 메시지 20개를 한 번에 담은 &lt;code&gt;MSG_STATE_SNAPSHOT&lt;/code&gt;을 전송한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3. Chat &amp;amp; Whisper Handler (&lt;code&gt;handlers_chat.cpp&lt;/code&gt;)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Regular Chat&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt;로 시작하면 슬래시 커맨드 핸들러로 분기한다. (&lt;code&gt;/refresh&lt;/code&gt;, &lt;code&gt;/who&lt;/code&gt; 등)&lt;/li&gt;
&lt;li&gt;권한 확인: 현재 사용자가 해당 방(Room)에 실제로 있는지 &lt;code&gt;state_&lt;/code&gt;를 통해 검증한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Fan-out&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Local&lt;/b&gt;: 내 서버 메모리에 있는 같은 방 유저들에게 &lt;code&gt;async_send&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Global&lt;/b&gt;: &lt;code&gt;redis_-&amp;gt;publish(&quot;fanout:room:{room}&quot;, msg)&lt;/code&gt;로 다른 서버에 있는 유저들에게도 메시지 전달.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Persistence&lt;/b&gt;: DB &lt;code&gt;messages&lt;/code&gt; 테이블에 저장한다. 실패해도 채팅은 계속되어야 하므로 &lt;code&gt;try-catch&lt;/code&gt;로 감싸고 에러 로그만 남긴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Whisper&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TODO&lt;/li&gt;
&lt;li&gt;클라이언트를 TUI에서 ImGui를 활용한 GUI로 변환하며 구현할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.4. Leave &amp;amp; Disconnect Handler (&lt;code&gt;handlers_leave.cpp&lt;/code&gt;, &lt;code&gt;session_events.cpp&lt;/code&gt;)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Leave&lt;/b&gt;: 유저가 명시적으로 &quot;나가기&quot; 요청을 한 경우.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로직은 &lt;code&gt;Join&lt;/code&gt;의 앞부분(퇴장 루틴)과 동일하다.&lt;/li&gt;
&lt;li&gt;항상 &quot;Lobby&quot;로 강제 이동시킨다. (고아 세션 방지)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Lobby가 가장 기본인 방이므로 무조건 여기로 보내야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Disconnect&lt;/b&gt;: 갑자기 랜선이 뽑히거나 강제종료된 경우.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;session_events.cpp&lt;/code&gt;의 &lt;code&gt;on_session_close()&lt;/code&gt;가 호출된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JobQueue&lt;/code&gt;에 정리 작업을 예약한다. (소멸자가 아님! ``shared_ptr`` 수명 관리 중요)&lt;/li&gt;
&lt;li&gt;Redis Presence 키(&lt;code&gt;presence:user:{uid}&lt;/code&gt;)를 즉시 삭제하지 않고 만료되게 두거나, 명시적으로 지울 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 데이터 지속성 &amp;amp; Write-Behind&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 서버엔 &lt;b&gt;데이터 쓰기 지연(Write-Behind)&lt;/b&gt; 패턴이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1. 흐름&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;핸들러가 &lt;code&gt;ChatService::emit_write_behind_event()&lt;/code&gt; 호출.&lt;/li&gt;
&lt;li&gt;이벤트 타입(&lt;code&gt;chat_msg&lt;/code&gt;, &lt;code&gt;login&lt;/code&gt;, &lt;code&gt;join&lt;/code&gt;)과 데이터를 JSON이 아닌 Key-Value 쌍으로 변환.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REDIS XADD stream:events * type chat_msg user bvm&amp;nbsp;...&lt;/code&gt; 명령 실행.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;끝.&lt;/b&gt; (DB 저장을 기다리지 않음 -&amp;gt; 매우 빠름)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2. 컨슈머 (Worker)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도의 프로세스(또는 스레드)가 &lt;code&gt;XREADGROUP&lt;/code&gt;으로 스트림을 읽어와서 &lt;code&gt;Postgres&lt;/code&gt;에 &lt;code&gt;INSERT&lt;/code&gt; 한다. DB가 일시적으로 느려지거나 죽어도, Redis Stream에 데이터가 쌓일 뿐 서비스는 멈추지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Redis는 단일 인스턴스이므로 SPOF이기 때문에 클러스터링이 필요하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 분산 상태 관리 (Redis)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 단순 캐시가 아니라 &lt;b&gt;제2의 메모리&lt;/b&gt;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;server:registry:{id}&lt;/code&gt;: 서버 생존 신고 (Heartbeat).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;room:users:{room}&lt;/code&gt;: 해당 방에 누가 있는지 (Set). &lt;code&gt;user_count&lt;/code&gt; 조회용.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;room:password:{room}&lt;/code&gt;: 방 비밀번호 (Single Source of Truth).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;presence:user:{uid}&lt;/code&gt;: 유저가 온라인인지 여부.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fanout:room:{room}&lt;/code&gt;: 채팅 메시지 Pub/Sub 채널.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. Configuration &amp;amp; Metrics&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.1. Environment Variables&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;GATEWAY_ID&lt;/code&gt;: 나를 관리하는 Gateway 식별자.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WRITE_BEHIND_ENABLED&lt;/code&gt;: 1이면 활성화.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REDIS_URI&lt;/code&gt;: &lt;code&gt;tcp://127.0.0.1:6379&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DB_DSN&lt;/code&gt;: Postgres 접속 정보.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.2. Prometheus Metrics (&lt;code&gt;/metrics&lt;/code&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;code&gt;chat_session_active&lt;/code&gt;: 현재 접속자 수 (Gauge).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chat_job_queue_depth&lt;/code&gt;: 처리 대기 중인 작업 수 (Gauge). 위험 신호 감지용.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chat_dispatch_total&lt;/code&gt;: 처리한 패킷 총량 (Counter).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chat_db_job_failed_total&lt;/code&gt;: DB 저장 실패 횟수 (Counter).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. TODO&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;SPOF 처리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;Gateway&lt;/li&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;위 셋 다 단일 인스턴스임!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인증 및 보안 관련 구현&lt;/li&gt;
&lt;li&gt;k8s를 통한 오케스트레이션&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 구두로 지시해 AI가 작성한 코드긴 하지만, 그걸 내가 확실하게 짚고 갈 수 있느냐는 다른 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 생각하는 코드보다 더 깔끔한 코드를 대부분의 상황에서 작성해 주기 때문에 좋은 공부가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음엔 클라이언트에 대해 얘기해 보자.&lt;/p&gt;</description>
      <category>Study/C++ &amp;amp; C#</category>
      <category>C++</category>
      <category>Server</category>
      <author>BVM</author>
      <guid isPermaLink="true">https://u-bvm.tistory.com/181</guid>
      <comments>https://u-bvm.tistory.com/181#entry181comment</comments>
      <pubDate>Sun, 7 Dec 2025 20:06:55 +0900</pubDate>
    </item>
    <item>
      <title>AI로 MSA 서버 만들어 보기 #1 : Core</title>
      <link>https://u-bvm.tistory.com/180</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드란 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 그게 IOCP라면 더욱더.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크에 대한 지식뿐만 아니라, IOCP에 대해서도 이해가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 ``Boost.Asio``를 통한 서버 공부를 진행했지만, 마음처럼 되지 않았다.&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;br /&gt;대 AI 시대가 열렸고, 엄청난 성능의 모델이 등장했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금이라면 내가 원하는 형태의 코드를 AI가 작성해 줄 수 있고, 실제로 동작하는 그러한 코드들을 보며 공부할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때는 ``GPT-5``가 갓 나왔을 때. &quot;AI로 ``Boost.Asio``를 기반으로 한 Windows와 Linux 양쪽에서 사용할 수 있는 멀티 플랫폼 서버를 만들어 보자&quot;라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작은 ``GPT-5``였지만, ``GPT-5.1``, ``Claude Sonnet 4.5``, ``Gemini 2.5 Pro``, ``Gemini 3.0 Pro``의 다양한 모델을 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 형태는 ``Codex-CLI``를 통해 ``GPT-5``를 사용하며 잡았고, 나머지 모델은 자잘한 작업에 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Antigravity가 출시된 이후로는 ``Gemini 3.0 Pro``를 주로 사용하고, 일부 작업은 ``Sonnet 4.5``로 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거의 대부분을 과정을 ``구두지시``로 진행했고, AI가 갈피를 못 잡을 땐 직접 손을 댔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰면서 느끼는 건...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이야 이거 참 인간 개발자가 많이 필요 없겠는데?&quot;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성형 AI로 패러다임이 전환된 지 얼마 되지도 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데도 벌써 이런 엄청난 성능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``Gemini 3.0``은 그야말로 게임 체인저다.&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;p data-ke-size=&quot;size16&quot;&gt;먼저 프로젝트 전체의 아키텍처부터 확인해 보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 전체 아키텍처&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Overall Architecture.png&quot; data-origin-width=&quot;1267&quot; data-origin-height=&quot;2149&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGk8Jx/dJMcaiV40FT/YJLp8qFbWWpcavyrbUgMT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGk8Jx/dJMcaiV40FT/YJLp8qFbWWpcavyrbUgMT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGk8Jx/dJMcaiV40FT/YJLp8qFbWWpcavyrbUgMT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGk8Jx%2FdJMcaiV40FT%2FYJLp8qFbWWpcavyrbUgMT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;1018&quot; data-filename=&quot;Overall Architecture.png&quot; data-origin-width=&quot;1267&quot; data-origin-height=&quot;2149&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 구성돼 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA를 위한 구색만 갖춘 수준이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 Dokcer Compose를 통해 리눅스에 올려서 테스트하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 k8s로 넘겨볼지도 모르겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게든 코어 라이브러리를 전반적으로 사용하게 해 꼬임을 방지하고자 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일종의 간단한 서버엔진을 흉내 내 본 것이기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 여기선 ``Core``에 대해 얘기해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Core&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. 아키텍처&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled diagram-2025-12-03-113756.png&quot; data-origin-width=&quot;1415&quot; data-origin-height=&quot;2113&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wxLAG/dJMcacImsDi/r0QqTCUAzr0kk4MQO2QMsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wxLAG/dJMcacImsDi/r0QqTCUAzr0kk4MQO2QMsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wxLAG/dJMcacImsDi/r0QqTCUAzr0kk4MQO2QMsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwxLAG%2FdJMcacImsDi%2Fr0QqTCUAzr0kk4MQO2QMsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;896&quot; data-filename=&quot;Untitled diagram-2025-12-03-113756.png&quot; data-origin-width=&quot;1415&quot; data-origin-height=&quot;2113&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core의 아키텍처를 간략히 표현한 것이다.&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;p data-ke-size=&quot;size16&quot;&gt;서버는 이 코어에 전적으로 의존하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Network&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Network에서 모든 비동기 I/O 처리와 통신을 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쌩으로 IOCP를 구현한 것이 아닌, Boost.Asio를 통해 구현되었으므로 ``io_context``에 기반한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. Hive&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``Hive`` 클래스는 ``io_context``의 수명 주기를 관리하는 핵심 클래스이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``io_context``는 등록된 작업이 없으면 ``run()``이 리턴되어 종료돼 버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이를 ``executor_work_guard``를 사용해 종료를 방지한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// core/src/net/hive.cpp

Hive::Hive(io_context&amp;amp; io)
    : io_(io)
    , guard_(boost::asio::make_work_guard(io_)) { 
    // make_work_guard를 통해 io_context에 &quot;가상의 작업&quot;을 등록하여,
    // 실제 I/O 작업이 없더라도 run()이 리턴하지 않고 대기하도록 합니다.
}

void Hive::run() {
    // 이 함수를 호출한 스레드는 I/O 이벤트 루프의 워커가 됩니다.
    // 여러 스레드에서 동시에 호출하면 Thread-Pool 기반의 Proactor 패턴이 됩니다.
    stopped_.store(false, std::memory_order_relaxed);
    io_.run();
}

void Hive::stop() {
    // 명시적으로 stop()을 호출해야만 guard가 해제되고 io_context가 종료됩니다.
    // 이는 서버의 Graceful Shutdown을 구현하는 기초가 됩니다.
    guard_.reset();
    io_.stop();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``Hive``는 FF14의 서버 에뮬레이팅 프로젝트인 Sapphire에 있는 구조를 가져온 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/SapphireServer/Sapphire/blob/master/src/common/Network/Hive.h&quot;&gt;https://github.com/SapphireServer/Sapphire/blob/master/src/common/Network/Hive.h&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765641605563&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Sapphire/src/common/Network/Hive.h at master &amp;middot; SapphireServer/Sapphire&quot; data-og-description=&quot;A Final Fantasy XIV 4.0+ Server Emulator written in C++ - SapphireServer/Sapphire&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/SapphireServer/Sapphire/blob/master/src/common/Network/Hive.h&quot; data-og-url=&quot;https://github.com/SapphireServer/Sapphire/blob/master/src/common/Network/Hive.h&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/nZ6zm/hyZPfqeiK4/kbkARhKYzO0apf9pDKDqg1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/PcNDm/hyZOzIIuvs/NS5lkhLzI1m2TRbikeeLYk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/SapphireServer/Sapphire/blob/master/src/common/Network/Hive.h&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/SapphireServer/Sapphire/blob/master/src/common/Network/Hive.h&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/nZ6zm/hyZPfqeiK4/kbkARhKYzO0apf9pDKDqg1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/PcNDm/hyZOzIIuvs/NS5lkhLzI1m2TRbikeeLYk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Sapphire/src/common/Network/Hive.h at master &amp;middot; SapphireServer/Sapphire&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A Final Fantasy XIV 4.0+ Server Emulator written in C++ - SapphireServer/Sapphire&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. Session&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``Session`` 클래스는 단일 TCP 연결의 생명 주기화 통신 처리를 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 스레드 환경에서 단일 소켓에 대한 동시 접근은 레이스 컨디션을 유발하고 데이터 정합성을 위협한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 방지하기 위해 ``mutex``를 사용하게 되지만, 이 또한 레이스 컨디션을 유발한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 ``strand``를 사용해 ``Lock-Free``를 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``strand``는 해당 세션 바인딩 된 모든 것들이 순차 실행 됨을 보장하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 명시적인 Lock 없이도 작업을 처리할 수 있게 된다.&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;// server/core/net/session.cpp

void Session::async_send(BufferManager::PooledBuffer data, size_t packet_size) {
    // asio::dispatch를 사용하여 작업을 strand로 전달합니다.
    // 현재 스레드가 이미 strand 안에 있다면 즉시 실행되고, 아니면 큐에 넣습니다.
    asio::dispatch(strand_, [self = shared_from_this(), data = std::move(data), packet_size]() mutable {
        if (self-&amp;gt;stopped_) return;

        // 여기는 strand에 의해 보호되는 영역이므로, Mutex 없이 안전하게 큐에 접근합니다.
        bool kick_write = self-&amp;gt;send_queue_.empty();
        self-&amp;gt;send_queue_.push({std::move(data), packet_size});

        if (kick_write) {
            self-&amp;gt;do_write();
        }
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 성능을 위해 메모리 처리에도 신경 써야 하는데, 이때 특히 복사 비용에 대한 고민이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``Session``은 별도 버퍼 관리 클래스와의 연계를 통해 ``Zero-Copy``에 가깝도록 했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Read
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;``BufferManager``에서 할당받은 버퍼를 ``async_read``에 직접 제공하여, 커널에서 유저로의 복사 외의 추가 복사를 방지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Write
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;직렬화된 데이터가 담긴 ``PooledBuffer``의 소유권을 큐로 이동시켜 복사를 방지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// server/core/net/session.cpp

void Session::do_read_header() {
    // 메모리 풀에서 버퍼를 하나 가져옵니다. (O(1))
    read_buf_ = buffer_manager_.Acquire(); 

    // 소켓이 이 버퍼에 직접 데이터를 쓰도록 합니다.
    asio::async_read(socket_, asio::buffer(read_buf_.get(), server::core::protocol::k_header_bytes),
        asio::bind_executor(strand_, [this, self](const error_code&amp;amp; ec, std::size_t n) {
            if (!ec) {
                // ... 헤더 디코딩 ...
                do_read_body(header_.length);
            }
        }));
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. Protocol &amp;amp; Packets&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버와 클라이언트 간의 통신은 명확하게 정의된 &lt;b&gt;패킷(Packet)&lt;/b&gt; 단위로 이루어진다.&lt;br /&gt;TCP는 스트림 기반 프로토콜이므로 데이터의 경계가 없지만, 애플리케이션 레벨에서는 메시지 경계가 필요하다.&lt;br /&gt;이를 위해 &lt;code&gt;Length-Prefixed&lt;/code&gt; 방식을 변형한 헤더 구조를 사용한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Packet Structure&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 패킷은 &lt;b&gt;14바이트의 고정 길이 헤더&lt;/b&gt;와 &lt;b&gt;가변 길이 바디&lt;/b&gt;로 구성된다.&lt;br /&gt;네트워크 전송 시에는 표준인 &lt;b&gt;Big-Endian&lt;/b&gt; 엔디안을 따른다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Offset&lt;/th&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Length&lt;/td&gt;
&lt;td&gt;uint16&lt;/td&gt;
&lt;td&gt;바디 데이터의 길이 (헤더 제외)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;MsgID&lt;/td&gt;
&lt;td&gt;uint16&lt;/td&gt;
&lt;td&gt;메시지 식별자 (Opcode)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Flags&lt;/td&gt;
&lt;td&gt;uint16&lt;/td&gt;
&lt;td&gt;압축, 암호화 여부 등의 플래그&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Sequence&lt;/td&gt;
&lt;td&gt;uint32&lt;/td&gt;
&lt;td&gt;패킷 순서 보장 및 중복 방지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Timestamp&lt;/td&gt;
&lt;td&gt;uint32&lt;/td&gt;
&lt;td&gt;전송 시간 (Latency 측정용)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 구조는 다음과 같은 이점을 제공한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;명확한 경계&lt;/b&gt;: &lt;code&gt;Length&lt;/code&gt; 필드를 통해 다음 패킷의 시작 위치를 정확히 알 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안성&lt;/b&gt;: 바디를 읽기 전에 헤더를 먼저 검증하여, 비정상적으로 큰 패킷이나 잘못된 프로토콜을 사전에 차단할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;관측성&lt;/b&gt;: &lt;code&gt;Sequence&lt;/code&gt;와 &lt;code&gt;Timestamp&lt;/code&gt;를 통해 패킷 유실이나 지연 시간을 추적할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) Packet Processing Flow&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패킷 처리는 &lt;b&gt;Double-Read&lt;/b&gt; 전략을 취한다.&lt;br /&gt;즉, 헤더를 먼저 읽고(14바이트), 그 결과에 따라 바디를 읽는다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// core/src/net/session.cpp

void Session::do_read_header() {
    // 1. 14바이트 고정 길이 헤더 읽기 요청
    asio::async_read(socket_, asio::buffer(read_buf_.get(), 14),
        [this](const error_code&amp;amp; ec, size_t n) {
            if (!ec) {
                // 2. 헤더 디코딩
                server::core::protocol::decode_header(read_buf_.get(), header_);

                // 3. 유효성 검사 (너무 큰 패킷 차단)
                if (header_.length &amp;gt; MAX_PAYLOAD) {
                    stop(); 
                    return;
                }

                // 4. 바디 읽기 시작
                do_read_body(header_.length);
            }
        });
}

void Session::do_read_body(size_t body_len) {
    if (body_len == 0) {
        // 바디가 없는 패킷(예: Ping)도 처리 가능
        dispatch(header_.msg_id, {});
        do_read_header();
        return;
    }

    // 5. 바디 길이만큼 읽기 요청
    asio::async_read(socket_, asio::buffer(read_buf_.get(), body_len),
        [this](const error_code&amp;amp; ec, size_t n) {
            if (!ec) {
                // 6. 핸들러로 전달
                dispatch(header_.msg_id, read_buf_);

                // 7. 다음 패킷 대기
                do_read_header();
            }
        });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) Dispatcher&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수신된 패킷은 &lt;code&gt;Dispatcher&lt;/code&gt;를 통해 적절한 핸들러로 전달된다.&lt;br /&gt;&lt;code&gt;std::unordered_map&lt;/code&gt;을 사용하여 O(1) 속도로 핸들러를 찾으며, 예외가 발생하더라도 해당 세션만 종료되도록 격리하여 서버 전체의 안정성을 보장한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// core/src/net/dispatcher.cpp

bool Dispatcher::dispatch(uint16_t msg_id, Session&amp;amp; s, span&amp;lt;const uint8_t&amp;gt; payload) {
    if (auto it = table_.find(msg_id); it != table_.end()) {
        try {
            it-&amp;gt;second(s, payload); // 핸들러 실행
            return true;
        } catch (...) {
            // 핸들러 오류가 서버 전체를 죽이지 않도록 방어
        }
    }
    return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-4. Flow Control (Backpressure)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 서버에서 가장 흔히 발생하는 문제는 &lt;b&gt;Producer-Consumer 속도 불균형&lt;/b&gt;이다.&lt;br /&gt;클라이언트의 인터넷 상태가 좋지 않아 데이터를 빨리 받지 못하는데, 서버가 계속해서 데이터를 보낸다면 어떻게 될까?&lt;br /&gt;&lt;code&gt;Session&lt;/code&gt;의 송신 큐(Send Queue)에 데이터가 무한히 쌓이다가 결국 &lt;b&gt;OOM(Out Of Memory)&lt;/b&gt;으로 서버가 죽게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 방지하기 위해 송신 큐에 한계점(High Watermark)을 설정하고, 이를 초과하면 가차 없이 세션을 끊어버리는 &lt;b&gt;Backpressure&lt;/b&gt; 메커니즘을 적용했다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// core/src/net/session.cpp

void Session::async_send(...) {
    // ...
    // 현재 큐에 쌓인 데이터 크기가 설정된 한계(예: 10MB)를 초과하면
    if (queued_bytes_ + packet_size &amp;gt; options_-&amp;gt;send_queue_max) {
        log::warn(&quot;Send queue limit exceeded; stopping session&quot;);
        stop(); // 세션 강제 종료 (보호 차원)
        return;
    }
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-5. Connection Setup&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결이 맺어지자마자 바로 비즈니스 로직을 수행하지는 않는다.&lt;br /&gt;가장 먼저 &lt;code&gt;MSG_HELLO&lt;/code&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;Handshake&lt;/b&gt;: 서버는 연결 직후 &lt;code&gt;MSG_HELLO&lt;/code&gt;를 보내 프로토콜 버전, 지원 기능(압축 등), Heartbeat 주기를 알린다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Heartbeat&lt;/b&gt;: 아무런 통신이 없으면 연결이 죽었는지 알 수 없으므로, 주기적으로 &lt;code&gt;MSG_PING&lt;/code&gt;을 보내 연결 생존을 확인한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// server/core/net/session.cpp

void Session::start() {
    send_hello();       // 1. 헬로 패킷 전송
    do_read_header();   // 2. 수신 대기 시작
    arm_read_timeout(); // 3. 읽기 타임아웃 타이머 가동 (Zombie Connection 방지)
    arm_heartbeat();    // 4. 하트비트 타이머 가동
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-6. Security &amp;amp; Optimization&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 코어는 강력한 보안과 최적화 기능을 기본적으로 내장하고 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Cipher (AES-256-GCM)&lt;/b&gt;: &lt;code&gt;OpenSSL&lt;/code&gt; 기반의 &lt;code&gt;Cipher&lt;/code&gt; 클래스를 제공하여, 데이터의 기밀성과 무결성을 동시에 보장한다. 단순 암호화뿐만 아니라 인증(Authentication)까지 수행하므로 패킷 변조를 원천 차단한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// core/src/security/cipher.cpp

std::vector&amp;lt;uint8_t&amp;gt; Cipher::encrypt(std::span&amp;lt;const uint8_t&amp;gt; plaintext, ...) {
    // OpenSSL EVP Interface 사용
    ScopedCtx ctx(EVP_CIPHER_CTX_new());

    // AES-256-GCM 초기화
    EVP_EncryptInit_ex(ctx.get(), EVP_aes_256_gcm(), nullptr, nullptr, nullptr);
    EVP_EncryptInit_ex(ctx.get(), nullptr, nullptr, key.data(), iv.data());

    // 암호화 수행
    EVP_EncryptUpdate(ctx.get(), ciphertext.data(), &amp;amp;out_len, plaintext.data(), ...);
    EVP_EncryptFinal_ex(ctx.get(), ciphertext.data() + out_len, &amp;amp;final_len);

    // Authentication Tag 추출 (변조 방지 핵심)
    EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_GET_TAG, TAG_SIZE, ...);

    return ciphertext;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Compressor (LZ4)&lt;/b&gt;: &lt;code&gt;LZ4&lt;/code&gt; 기반의 &lt;code&gt;Compressor&lt;/code&gt; 클래스를 제공한다. 압축 효율이 매우 높으며, 이를 통해 대역폭 비용을 획기적으로 절감할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// core/src/compression/compressor.cpp

std::vector&amp;lt;uint8_t&amp;gt; Compressor::compress(std::span&amp;lt;const uint8_t&amp;gt; data) {
    // 압축 후 최대 크기 계산
    int max_dst_size = LZ4_compressBound(static_cast&amp;lt;int&amp;gt;(data.size()));
    std::vector&amp;lt;uint8_t&amp;gt; compressed(max_dst_size);

    // LZ4 기본 압축 수행
    int compressed_size = LZ4_compress_default(
        reinterpret_cast&amp;lt;const char*&amp;gt;(data.data()),
        reinterpret_cast&amp;lt;char*&amp;gt;(compressed.data()),
        static_cast&amp;lt;int&amp;gt;(data.size()),
        max_dst_size
    );

    compressed.resize(compressed_size);
    return compressed;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Concurrency&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``Concurrency`` 클래스는 스레드 관리와 스케줄링을 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순하게 말하자면 병렬 처리를 가능케 하는 클래스이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. ThreadManager&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``ThreadManager``는 워커 스레드의 생명주기를 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 로직에서 직접 ``std::thread``를 다루는 일이 없어야 하고, 스레드가 모든 작업을 제대로 처리할 수 있게 관리해 데이터 유실도 방지한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// server/core/concurrent/thread_manager.cpp

void ThreadManager::Start(int num_threads) {
    stopped_.store(false, std::memory_order_relaxed);
    threads_.reserve(num_threads);
    for (int i = 0; i &amp;lt; num_threads; ++i) {
        threads_.emplace_back([this] { WorkerLoop(); });
    }
}

void ThreadManager::Stop() {
    bool expected = false;
    if (!stopped_.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
        return;
    }

    job_queue_.Stop(); // Pop()에서 대기 중인 워커들을 모두 깨워 종료 신호를 전달합니다.

    for (auto&amp;amp; t : threads_) {
        if (t.joinable()) {
            t.join();
        }
    }
    threads_.clear();
}

// 각 워커 스레드는 JobQueue::Pop()이 nullptr를 반환할 때까지 반복 실행됩니다.
void ThreadManager::WorkerLoop() {
    while (!stopped_.load(std::memory_order_acquire)) {
        Job job = job_queue_.Pop();
        if (!job) { // nullptr 작업이 오면 종료합니다.
            break;
        }
        job();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. JobQueue&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``JobQueue``는 순차적 실행을 보장하는 FIFO 큐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 ``std::mutex``와 ``std::condition_variable``을 사용하는 전형적 Producer - Consumer 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 단위마다 ``JobQueue``를 사용해 순서를 보장하고 Lock 관리에서 어느 정도 해방시켜 준다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// server/core/concurrent/job_queue.cpp

void JobQueue::Push(Job job) {
    {
        std::lock_guard&amp;lt;std::mutex&amp;gt; lock(mutex_);
        jobs_.push(std::move(job));
        // 메트릭 기록: 현재 큐 깊이
        runtime_metrics::record_job_queue_depth(jobs_.size());
    }
    // 대기 중인 워커 스레드 하나를 깨웁니다.
    cv_.notify_one();
}

Job JobQueue::Pop() {
    std::unique_lock&amp;lt;std::mutex&amp;gt; lock(mutex_);
    // 큐에 작업이 들어오거나 종료 신호가 올 때까지 대기합니다.
    cv_.wait(lock, [this] { return !jobs_.empty() || stopping_; });

    if (stopping_ &amp;amp;&amp;amp; jobs_.empty()) {
        runtime_metrics::record_job_queue_depth(jobs_.size());
        return nullptr; // nullptr이면 종료 신호로 간주합니다.
    }

    Job job = std::move(jobs_.front());
    jobs_.pop();
    runtime_metrics::record_job_queue_depth(jobs_.size());
    return job;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. TaskScheduler&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``TaskScheduler`` 지연이나 주기적 실행이 필요한 작업들을 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수만 또는 수십만 이상의 타이머가 필요한 서버에서 각 작업마다 ``asio::steady_timer``를 생성해 사용하는 것은 엄청나게 큰 오버헤드가 될 것임이 자명하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 단일 ``std::priority_queue``를 사용해 모든 작업들을 관리한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// server/core/concurrent/task_scheduler.cpp

void TaskScheduler::schedule(Task task, Clock::duration delay) {
    std::lock_guard&amp;lt;std::mutex&amp;gt; lock(mutex_);
    // 만료 시간(due)을 기준으로 정렬되는 우선순위 큐에 삽입합니다.
    delayed_.push(DelayedTask{Clock::now() + delay, std::move(task)});
}

std::size_t TaskScheduler::poll(std::size_t max_tasks) {
    // ...
    auto now = Clock::now();
    // 현재 시간보다 이전에 만료된 작업들을 모두 꺼내서 실행 대기열(ready_)로 옮깁니다.
    while (!delayed_.empty() &amp;amp;&amp;amp; delayed_.top().due &amp;lt;= now) {
        ready_.push(std::move(delayed_.top().task));
        delayed_.pop();
    }
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Memory &amp;amp; Utils&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-1. MemoryPool&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``MemoryPool``은 고정 크기 메모리 블록의 할당 및 해제를 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈번한 동적 할당은 메모리 파편화를 유발할 가능성이 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``MemoryPool``을 통해 사전에 큰 청크를 할당하고, 그 청크를 쪼개 관리하여 파편화를 방지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 가용 메모리 블록의 포인터들을 스택으로 관리해 O(1)의 접근속도를 보장한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// core/src/memory/memory_pool.cpp

class MemoryPool {
    std::vector&amp;lt;std::byte&amp;gt; memoryChunk_; // 실제 메모리가 할당된 공간
    std::stack&amp;lt;void*&amp;gt; freeList_;         // 사용 가능한 블록들의 포인터 스택

    void* Acquire() {
        std::lock_guard&amp;lt;std::mutex&amp;gt; lock(mutex_);
        if (freeList_.empty()) return nullptr; // 풀 고갈
        void* ptr = freeList_.top();
        freeList_.pop();
        return ptr;
    }

    void Release(void* ptr) {
        std::lock_guard&amp;lt;std::mutex&amp;gt; lock(mutex_);
        freeList_.push(ptr);
    }
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-2. Dispatcher&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``Dispatcher``는 수신된 패킷의 Opcode에 따라 그에 맞는 핸들러 함수를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``std::unordered_map``을 사용하여 O(1)의 Opcode 조회 성능을 보장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``std::span``을 사용해 버퍼 길이를 초과하는 접근을 막는다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// core/src/net/dispatcher.cpp

class Dispatcher {
    using handler_t = std::function&amp;lt;void(Session&amp;amp;, std::span&amp;lt;const std::uint8_t&amp;gt;)&amp;gt;;
    std::unordered_map&amp;lt;std::uint16_t, handler_t&amp;gt; table_;

    bool dispatch(std::uint16_t msg_id, Session&amp;amp; s, std::span&amp;lt;const std::uint8_t&amp;gt; payload) const {
        if (auto it = table_.find(msg_id); it != table_.end()) {
            it-&amp;gt;second(s, payload); // 핸들러 호출
            return true;
        }
        return false; // 알 수 없는 메시지 ID
    }
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Observability&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 환경에서의 디버깅과 모니터링을 위한 유틸리티를 제공한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-1. Logging&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``server::core::log``를 통해 Thread-Safe 로깅을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤 패턴을 통해 전체에서 단 하나의 로깅 인스턴스만 사용하도록 한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;// core/src/util/log.cpp

class AsyncLogger {
public:
    // 로그 메시지를 큐에 넣고 워커 스레드를 깨웁니다.
    // 이 함수는 메인 로직 스레드에서 호출되므로 최대한 빨리 리턴해야 합니다.
    void push(const std::string&amp;amp; msg) {
        {
            std::lock_guard&amp;lt;std::mutex&amp;gt; lock(mutex_);
            queue_.push(msg);
        }
        cv_.notify_one();
    }

private:
    // 생성자는 private으로 선언하여 싱글톤 패턴을 강제합니다.
    AsyncLogger() : stop_(false) {
        // 백그라운드 워커 스레드를 시작합니다.
        worker_ = std::thread([this] { worker_loop(); });
    }

    // 소멸자에서 워커 스레드가 안전하게 종료되도록 대기합니다.
    ~AsyncLogger() {
        // ...
    }

    // 워커 스레드에서 실행될 메인 루프입니다.
    void worker_loop() {
        while (true) {
            std::string msg;
            // ...
        }
    }

    std::mutex mutex_; // 큐 접근을 위한 뮤텍스
    std::condition_variable cv_; // 큐 상태 변경을 알리는 조건 변수
    std::queue&amp;lt;std::string&amp;gt; queue_; // 로그 메시지를 저장하는 큐
    std::thread worker_; // 로그 처리를 담당하는 백그라운드 스레드
    bool stop_; // 스레드 종료를 위한 플래그

    // AsyncLogger 인스턴스를 얻기 위한 friend 함수 선언
    friend AsyncLogger&amp;amp; get_logger();
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-2. Metrics&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus에 제공하기 위한 메트릭 수집 인터페이스를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``Counter``, ``Gauge``, ``Histogram`` 인터페이스를 제공한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;// core/include/server/core/metrics/metrics.cpp

struct NoopCounter final : Counter { void inc(double, Labels) override {} };
struct NoopGauge final : Gauge { void set(double, Labels) override {} void inc(double, Labels) override {} void dec(double, Labels) override {} };
struct NoopHistogram final : Histogram { void observe(double, Labels) override {} };

std::mutex&amp;amp; mu() { static std::mutex m; return m; }
std::unordered_map&amp;lt;std::string, NoopCounter&amp;gt;&amp;amp; counters() { static std::unordered_map&amp;lt;std::string, NoopCounter&amp;gt; m; return m; }
std::unordered_map&amp;lt;std::string, NoopGauge&amp;gt;&amp;amp; gauges() { static std::unordered_map&amp;lt;std::string, NoopGauge&amp;gt; m; return m; }
std::unordered_map&amp;lt;std::string, NoopHistogram&amp;gt;&amp;amp; histos() { static std::unordered_map&amp;lt;std::string, NoopHistogram&amp;gt; m; return m; }
}

// Prometheus exporter가 붙지 않은 초기 단계에서도 공통 metrics API를 호출할 수 있도록
// Noop 객체를 미리 준비해두고, 실제 exporter가 연결되면 즉시 교체되는 구조다.
// 이를 통해 비즈니스 로직은 메트릭 시스템의 유무와 상관없이 항상 동일하게 동작한다.
Counter&amp;amp; get_counter(const std::string&amp;amp; name) {
    std::lock_guard&amp;lt;std::mutex&amp;gt; lk(mu());
    return counters().try_emplace(name).first-&amp;gt;second;
}

// ...


// 실제 로직에서 사용할 때
auto&amp;amp; req_count = server::core::metrics::get_counter(&quot;requests_total&quot;);
req_count.inc(1.0, {{&quot;method&quot;, &quot;login&quot;}});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``Boost.Asio``는 정말 엄청난 도구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 쌩으로 IOCP 서버를 개발하는 공부를 해 본 입장에서, 개발에 엄청난 편의성을 제공한다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로우레벨에 대해 신경 쓸 부분이 줄었다는 것은 실수를 많이 줄일 수 있다는 뜻이기도 하니 여하튼 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음엔 서버에 대한 내용이다.&lt;/p&gt;</description>
      <category>Study/C++ &amp;amp; C#</category>
      <category>Boost.Asio</category>
      <category>C++</category>
      <category>Network</category>
      <author>BVM</author>
      <guid isPermaLink="true">https://u-bvm.tistory.com/180</guid>
      <comments>https://u-bvm.tistory.com/180#entry180comment</comments>
      <pubDate>Wed, 3 Dec 2025 18:02:28 +0900</pubDate>
    </item>
    <item>
      <title>제11회 빅데이터분석기사 필기 짧은 후기</title>
      <link>https://u-bvm.tistory.com/179</link>
      <description>&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언젠간 내 인생에 도움이 되지 않겠나?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kZcAY/btsQG9lHCIS/52Ibk6v0dIQPNt5vsYWUD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kZcAY/btsQG9lHCIS/52Ibk6v0dIQPNt5vsYWUD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kZcAY/btsQG9lHCIS/52Ibk6v0dIQPNt5vsYWUD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkZcAY%2FbtsQG9lHCIS%2F52Ibk6v0dIQPNt5vsYWUD1%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;507&quot; height=&quot;100&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;p data-ke-size=&quot;size16&quot;&gt;게으름 이슈도 있고 통계라곤 대학 시절 1학기만 들은 것이 다였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그것조차 제대로 하지 못했기에 저거 다 보는 것만으로도 벅찼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 마디로 요약하자면 &quot;아는 게 하나도 없는 노 베이스&quot; 상태였다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 기억이 나지 않거나 처음 보는 개념들을 열심히 외워야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CRISP-DM 방법론의 진행 순서는 뭐고 데이터의 품질 지표는 뭐고...&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기출과 모의고사도 열심히 풀었고, 출판사에서 제공하는 CBT도 열심히 풀었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과락은 1~2번 있었던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 거의 대부분 합격선이었고, 실전도 기출에서 크게 벗어나지는 않을 것이라 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 문제를 한번 쫙 푸는데 1시간도 걸리지 않았기에 점검할 시간도 충분했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 약간의 자신을 가지고 9월 6일에 시험장으로 향했다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험 시작 전에 파본을 확인하는 때에 문제들이 슬쩍 보이는 것을 보니...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;아 씨발 좆된거 같은데?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 생각했다.&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;그래도 토탈 80문제고, 그건 일부였기에 그건 뭐 그냥 버리는 문제라고 합리화를 하며 시험이 시작됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1과목은 무난했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2, 3과목이 문제였다.&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;p data-ke-size=&quot;size16&quot;&gt;각 개념들의 미묘한 차이까지 확실히 알아야 정답을 제대로 건들 수 있는 문제들이었다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 문제들은 듣도 보도 못한 것들을 묻는 문제들이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소거법을 쓰기도 어려웠다. 이건 진짜 아닌 것 같다고 생각한 보기를 제외해도 3개 중에서 찍어야 하니까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전후로 유사한 개념을 묻는 문제도 없었기 때문에 대조도 할 수 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 2, 3과목은 거의 60% 이상을 마킹하지 못하고 일단 넘어갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이기적 교재의 내용이 부실한 건가? 다른 출판사의 교재엔 이 내용들이 나왔을 수도 있지 않을까?라는 생각을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4과목은 할만했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숨통이 트이는 느낌.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 1과목과 4과목에서 번 점수로 2, 3과목을 커버쳐야 하는 상황.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험 시간을 거의 최후의 최후까지 쓰며 남은 문제들을 열심히 찍었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번이 너무 많이 나와서 &quot;출제를 장난치듯이 하네&quot;라는 생각도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험이 종료되고 나와서 이기적 카페에서 후기들을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실히 어려웠다는 평.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;음~ 나만 좆박은 게 아니구만~&quot; 이라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭐 내가 할 수 있는 게 없으니 그냥 기다리기만 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난 2, 3과목 어디서 과락이 나거나 턱걸이로 합격했으리라 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 19일이 찾아왔다.&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;p data-ke-size=&quot;size16&quot;&gt;그렇게 발표 페이지로 들어가니...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;158&quot; data-origin-height=&quot;132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qEKX8/btsQJFwNHVw/JZo9cRQzj8N4mDpLA91c9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qEKX8/btsQJFwNHVw/JZo9cRQzj8N4mDpLA91c9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qEKX8/btsQJFwNHVw/JZo9cRQzj8N4mDpLA91c9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqEKX8%2FbtsQJFwNHVw%2FJZo9cRQzj8N4mDpLA91c9K%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;158&quot; height=&quot;132&quot; data-origin-width=&quot;158&quot; data-origin-height=&quot;132&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어????&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이거 뭐지?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세 결과를 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;488&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btxYPc/btsQHflWu0c/DQaST07h2APfmfQOEailn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btxYPc/btsQHflWu0c/DQaST07h2APfmfQOEailn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btxYPc/btsQHflWu0c/DQaST07h2APfmfQOEailn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtxYPc%2FbtsQHflWu0c%2FDQaST07h2APfmfQOEailn1%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;316&quot; data-origin-width=&quot;488&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하~~~~&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;됐다~~~~&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1, 4과목이 내 생각보다 점수가 잘 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2, 3과목도 과락에 근접하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;턱걸이 합격을 예상했는데, 70점으로 여유롭게 합격했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안도의 깊은 한숨이 쫙 나오는 게 아니겠나.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 고비를 넘었다는 생각에 기분이 썩 괜찮았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 실기가 남았지만 공부할 시간은 충분하다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬이야 뭐 익숙하고 pandas도 사용은 해 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;help 명령어도 쓸 수 있으니 필기보다 할만하지 않겠는가.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한방에 실기 합격까지 가보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이기적 교재로 공부하는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나쁜 선택은 아니라고 생각한다.&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;p data-ke-size=&quot;size16&quot;&gt;사실상의 노 베이스 상태로 공부해서 한방에 합격했으니 남에게 추천해 줄 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 보니 짧은 후기가 아닌가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 길어졌다.&lt;/p&gt;</description>
      <category>Study/Others</category>
      <category>11회</category>
      <category>빅데이터분석기사</category>
      <category>빅분기</category>
      <category>필기</category>
      <author>BVM</author>
      <guid isPermaLink="true">https://u-bvm.tistory.com/179</guid>
      <comments>https://u-bvm.tistory.com/179#entry179comment</comments>
      <pubDate>Sun, 21 Sep 2025 16:47:58 +0900</pubDate>
    </item>
    <item>
      <title>[JS] 디스코드 봇에 PLL 정보 출력하게 하기</title>
      <link>https://u-bvm.tistory.com/178</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gitlab.com/kupo-bot/KupoBot&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;KupoBot&lt;/a&gt;이라는 FF14 관련한 많은 기능을 제공하는 봇이 있다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 요청하게 되는 정보는 점검과 PLL 관련 정보인데,&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점검 같은 경우는 정규식으로 RSS Feed를 파싱해 최신 점검 정보를 가져오게 해 놨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한번 파싱 하면 그걸 저장해 놨다가 재활용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PLL도 그런 식으로 파싱 할 수 있으므로 똑같은 흐름으로 명령어를 만들게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1741076621410&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function getMaintData() {
  const feedUrl = &quot;https://jp.finalfantasyxiv.com/lodestone/news/news.xml&quot;;
  const now = Date.now() / 1000;

  try {
    const savedData = await loadData();
    if (savedData.MAINTINFO?.end_stamp &amp;gt; now) {
      return savedData;
    }

    const feed = await parser.parseURL(feedUrl);
    const targetItem = feed.items.find((item) =&amp;gt; item.title?.startsWith(&quot;全ワールド&quot;));
    if (!targetItem) return null;
# ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 FF14의 전체 점검 공지는 「全ワールド」라는 접두어를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 기반해서 해당하는 점검 피드를 찾는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 현재 기준으로 다음의 점검 공지를 찾을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jp.finalfantasyxiv.com/lodestone/news/detail/b3b68af95965de482df4eca20157b12b96f97c34&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[メンテナンス]全ワールド&amp;nbsp;メンテナンス作業のお知らせ(3/4)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 내용에서 시간 정보를 뽑아내야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;615&quot; data-origin-height=&quot;719&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGNEyq/btsMz0Nm7Gm/ekps6mLU6cmgq8tw8lABy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGNEyq/btsMz0Nm7Gm/ekps6mLU6cmgq8tw8lABy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGNEyq/btsMz0Nm7Gm/ekps6mLU6cmgq8tw8lABy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGNEyq%2FbtsMz0Nm7Gm%2Fekps6mLU6cmgq8tw8lABy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;615&quot; height=&quot;719&quot; data-origin-width=&quot;615&quot; data-origin-height=&quot;719&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간 내용은 필요 없고 가장 아래의 시간 정보만 정규식으로 뽑아낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1741076831224&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function extractMaintenanceInfo(item) {
  const startTimeRegex = /日　時：(\d{4}年\d{1,2}月\d{1,2}日\(.\)) (\d{1,2}:\d{2})より/;
  const endTimeRegex = /(\d{4}年\d{1,2}月\d{1,2}日\(.\))? ?(\d{1,2}:\d{2})頃まで/;
  const startTimeMatch = item.content.match(startTimeRegex);
  const endTimeMatch = item.content.match(endTimeRegex);
  if (!startTimeMatch || !endTimeMatch) return null;

  // 중복되는 moment.tz 호출을 헬퍼 함수로 분리
  const parseDate = (dateStr, timeStr) =&amp;gt;
    moment.tz(`${dateStr} ${timeStr}`, &quot;YYYY年MM月DD日(ddd) HH:mm&quot;, &quot;ja&quot;, &quot;Asia/Tokyo&quot;).unix();

  const startTime = parseDate(startTimeMatch[1], startTimeMatch[2]);
  const endTime = endTimeMatch[1]
    ? parseDate(endTimeMatch[1], endTimeMatch[2])
    : parseDate(startTimeMatch[1], endTimeMatch[2]);

  return { start_stamp: startTime, end_stamp: endTime };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 시간을 뽑아서 유닉스 타임스탬프 형태로 리턴한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 바탕으로 디스코드 임베드에 정보들을 넣어 보여주게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;455&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgdQv4/btsMA51wfNK/uuQ4fNLLbaspZWFKWRezf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgdQv4/btsMA51wfNK/uuQ4fNLLbaspZWFKWRezf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgdQv4/btsMA51wfNK/uuQ4fNLLbaspZWFKWRezf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgdQv4%2FbtsMA51wfNK%2FuuQ4fNLLbaspZWFKWRezf1%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;455&quot; height=&quot;250&quot; data-origin-width=&quot;455&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 임베드로 출력하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. PLL도 같은 식으로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PLL은 다음과 같은 형식의 제목을 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[「第n回 FFXIV PLL」m月dd日（金）放送決定！]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 가장 중요한 건 &lt;span style=&quot;color: #c8c3bc; text-align: start;&quot; data-darkreader-inline-color=&quot;&quot;&gt;FFXIV PLL이 포함되는 부분이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #c8c3bc; text-align: start;&quot; data-darkreader-inline-color=&quot;&quot;&gt;저 부분은 형태가 바뀌지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #c8c3bc; text-align: start;&quot; data-darkreader-inline-color=&quot;&quot;&gt;이걸 통해서 피드에서 PLL 관련 정보들을 가져올 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #c8c3bc; text-align: start;&quot; data-darkreader-inline-color=&quot;&quot;&gt;현재 기준으로 다음의 PLL 공지를 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jp.finalfantasyxiv.com/lodestone/topics/detail/4d553c07963fd3e8914fc0d48d01b0481393f898&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;「第86回&amp;nbsp;FFXIV&amp;nbsp;PLL」3月14日（金）放送決定！&lt;/a&gt;&lt;span style=&quot;color: #c8c3bc; text-align: start;&quot; data-darkreader-inline-color=&quot;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;820&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdtaIY/btsMzQEgtGA/SW4WU0wS64ze2kdGEGY6bK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdtaIY/btsMzQEgtGA/SW4WU0wS64ze2kdGEGY6bK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdtaIY/btsMzQEgtGA/SW4WU0wS64ze2kdGEGY6bK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdtaIY%2FbtsMzQEgtGA%2FSW4WU0wS64ze2kdGEGY6bK%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;617&quot; height=&quot;820&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;820&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시 필요한 건 시간 정보뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1741077159368&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 회차 번호 추출
const $ = cheerio.load(summary, { decodeEntities: false });
const h3Element = $(&quot;h3.mdl-title__heading--lg&quot;).first();
let roundNumber = &quot;&quot;;
const roundMatch = (h3Element.text() || title).match(/第(\d+)回/);
if (roundMatch) {
  roundNumber = roundMatch[1];
}

// 방송 시작 시각 추출 (전각 괄호 대응)
let start_stamp = null;
const dateRegex = /(\d{4}年\d{1,2}月\d{1,2}日（[^）]+）)\s?(\d{1,2}:\d{2})頃?～/;
const dateMatch = summary.match(dateRegex);
if (dateMatch) {
  // dateMatch[1] = &quot;2025年3月14日（金）&quot;, dateMatch[2] = &quot;19:00&quot;
  const dateStrClean = dateMatch[1].replace(/（[^）]+）/, &quot;&quot;); // &amp;rarr; &quot;2025年3月14日&quot;
  const finalStr = `${dateStrClean} ${dateMatch[2]}`; // &amp;rarr; &quot;2025年3月14日 19:00&quot;
  const parsed = moment.tz(finalStr, &quot;YYYY年M月D日 HH:mm&quot;, &quot;Asia/Tokyo&quot;);
  if (parsed.isValid()) {
    start_stamp = parsed.unix();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정규식을 통해 현재 몇 회 차인지, 진행 시간은 언제인지 추출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1741077194658&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let fixedTitle = &quot;제 XX회 프로듀서 레터 라이브 X월 XX일 방송 결정!&quot;;
if (start_stamp) {
  const formattedDate = moment.unix(start_stamp).tz(&quot;Asia/Seoul&quot;).format(&quot;M월 D일&quot;);
  const roundText = roundNumber ? `제 ${roundNumber}회` : `제 XX회`;
  fixedTitle = `${roundText} 프로듀서 레터 라이브 ${formattedDate} 방송 결정!`;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 타이틀을 추출한 정보에 맞게 수정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1741077238475&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 캐싱 만료 시간 설정
const expireTime = Math.floor(now + 12 * 60 * 60);
const newData = {
  PLLINFO: {
    fixedTitle,
    url: link,
    start_stamp,
    expireTime,
  },
};

await saveData(newData);
return newData.PLLINFO;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후에 현재 저장된 정보가 만료된 것인지 판별하기 위해 만료 시간을 계산해 같이 캐싱한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;465&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/059fc/btsMCoeApdN/2vMmDqksSaAkVPtVWbkL50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/059fc/btsMCoeApdN/2vMmDqksSaAkVPtVWbkL50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/059fc/btsMCoeApdN/2vMmDqksSaAkVPtVWbkL50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F059fc%2FbtsMCoeApdN%2F2vMmDqksSaAkVPtVWbkL50%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;465&quot; height=&quot;225&quot; data-origin-width=&quot;465&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추출한 정보를 통해 점검 정보와 유사하게 이런 식으로 출력할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답답하면 내가 만들면 되는 게 아닌가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 만들었다.&lt;br /&gt;훨씬 보기도 좋다.&lt;/p&gt;</description>
      <category>Study/Javascript</category>
      <category>bot</category>
      <category>discord</category>
      <category>FF14</category>
      <category>PLL</category>
      <author>BVM</author>
      <guid isPermaLink="true">https://u-bvm.tistory.com/178</guid>
      <comments>https://u-bvm.tistory.com/178#entry178comment</comments>
      <pubDate>Tue, 4 Mar 2025 17:38:15 +0900</pubDate>
    </item>
    <item>
      <title>[TIL #35] 데드락에 죽다 살다</title>
      <link>https://u-bvm.tistory.com/177</link>
      <description>&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;a href=&quot;https://github.com/OptimalBits/bull&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Bull&lt;/a&gt;을 도입하게 됐다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Bull만 도입한다고 되는 것은 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bull 자체의 ``Job``에 대해선 원자적 처리를 보장하지만 그 이외의 부분에 대해선 원자적 처리가 보장되지 않는 상황이다.&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;a href=&quot;https://www.npmjs.com/package/async-lock&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;async-lock&lt;/a&gt;을 도입했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1734261077365&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// pendingGroups 접근을 안전하게 하기 위한 헬퍼 메서드
async withPendingGroupsLock(fn) {
  return this.pendingGroupsLock.acquire('pendingGroups', fn);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 락을 걸기 위한 헬퍼를 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``fn``의 형태로 함수를 인자로 받는데, 그 함수는 락이 걸린 상태로 동작하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1734261195376&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;await this.withPendingGroupsLock(async () =&amp;gt; {
  for (const [groupId, group] of this.pendingGroups.entries()) {
    const allUsersExist = Array.from(group.userIds).every((uid) =&amp;gt; sessionManager.getUser(uid));
    if (!allUsersExist) {
      logger.info(`그룹 ${groupId}의 일부 유저가 존재하지 않아 그룹 제거`);
      this.pendingGroups.delete(groupId);
    }
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 락을 걸어 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 데드락을 항상 조심해야 한다.&lt;/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;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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1734261533302&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async acceptUserInGroup(user, groupId) {
    // ...
    
    
    // 유효한 플레이어 모두 수락 처리
    for (const p of actualMatchedPlayers) {
      await this.removeAcceptQueueInUser(p);
      p.setMatched(false);
    }
    
    // ...
  }
  return null;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``acceptUserInGroup``을 호출하는 상위 함수엔 이미 ``withPendingGroupsLock``이 걸려있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 상태에서 ``this.removeAcceptQueueInUser``가 정상 진행 시 호출되는데, 여기가 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1734261665743&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async removeAcceptQueueInUser(user) {
  return this.withPendingGroupsLock(async () =&amp;gt; {
    //...
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 잡은 락을 또 잡고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러니 진행이 될 수가 있나...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``removeAcceptQueueInUser``의 락을 제거하고 나니 정상적으로 동작하는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 에러도 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티를 안 내니 찾기가 쉽지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시성 문제라는 것이 정말 만만하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람 잡는 요물이라고 하기에 손색없으리라.&lt;/p&gt;</description>
      <category>Camp/T.I.L.</category>
      <author>BVM</author>
      <guid isPermaLink="true">https://u-bvm.tistory.com/177</guid>
      <comments>https://u-bvm.tistory.com/177#entry177comment</comments>
      <pubDate>Sun, 15 Dec 2024 20:24:57 +0900</pubDate>
    </item>
    <item>
      <title>[TIL #34] Docker 및 Docker Compose 올려보기</title>
      <link>https://u-bvm.tistory.com/176</link>
      <description>&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;내가 만져보면서 이해해보고 싶었던 것이기도 했고, 아마존 ECS에 올려보기 위함이기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 현재 서버는 다음과 같은 상태다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Node.js 서버&lt;/li&gt;
&lt;li&gt;MySQL&lt;/li&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;Grafana&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 각각의 것들을 다 컨테이너로 만들어 ``Compose``로 묶어서 빌드 후 배포할 수 있어야 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 노드 서버를 위한 ``Dockerfile``을 다음과 같이 만들었다.&lt;/p&gt;
&lt;pre id=&quot;code_1733226803447&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Node.js 베이스 이미지 사용
FROM node:20-alpine

# 작업 디렉토리 설정
WORKDIR &quot;작업 디렉토리&quot;

# 패키지 파일 복사 및 의존성 설치
COPY package*.json ./
RUN npm install

# 소스 코드 복사
COPY . .

# 포트 노출
EXPOSE 5555

# 애플리케이션 실행
CMD [&quot;npx&quot;, &quot;pm2-runtime&quot;, &quot;start&quot;, &quot;ecosystem.config.cjs&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별 다른 특별한 일은 하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다르다면 ``pm2``를 위한 설정이 있다는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 DB 등의 나머지 것들에 대한 설정은 ``docker-compose.yml``에서 이루어진다.&lt;/p&gt;
&lt;pre id=&quot;code_1733226963676&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  nodejs:
    build: .
    ports:
      - &quot;5555:5555&quot;
    env_file:
      - .env
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
      mysqld_exporter:
        condition: service_healthy
      redis_exporter:
        condition: service_healthy
      node_exporter:
        condition: service_healthy
    restart: on-failure
    networks:
      - app-network
      
// ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 필요한 모든 서비스를 컴포즈 파일에 정의했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 DB와 익스포터들에 대해 헬스 체크도 수행하게 했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``depends_on``이기 때문에 저것들이 다 올라와야 노드 서버가 올라올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하다 보니 익스포터마저 별개 컨테이너로 빠지게 됐는데, 이게 괜찮은지에 대해선 생각을 좀 해봐야 할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너가 8개가 돼버렸기 때문...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1733227075925&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['prometheus:9090']

  - job_name: 'mysql'
    static_configs:
      - targets: ['mysqld_exporter:9104']

  - job_name: 'redis'
    static_configs:
      - targets: ['redis_exporter:9121']

  - job_name: 'node'
    static_configs:
      - targets: ['node_exporter:9100']&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 프로메테우스 설정도 잊지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포즈로 빌드 후 컨테이너가 올라오는 것을 보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1475&quot; data-origin-height=&quot;888&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QtQV0/btsK4zjLxkI/EUiF8vPE8u4JiIgakrOxgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QtQV0/btsK4zjLxkI/EUiF8vPE8u4JiIgakrOxgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QtQV0/btsK4zjLxkI/EUiF8vPE8u4JiIgakrOxgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQtQV0%2FbtsK4zjLxkI%2FEUiF8vPE8u4JiIgakrOxgK%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;1475&quot; height=&quot;888&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1475&quot; data-origin-height=&quot;888&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너들도 잘 올라오고 서버가 정상적으로 동작하는 것도 확인할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1592&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVFwJA/btsK6N1tXpA/XT8TbBNJBcV1tvWikvgiTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVFwJA/btsK6N1tXpA/XT8TbBNJBcV1tvWikvgiTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVFwJA/btsK6N1tXpA/XT8TbBNJBcV1tvWikvgiTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVFwJA%2FbtsK6N1tXpA%2FXT8TbBNJBcV1tvWikvgiTk%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;1592&quot; height=&quot;734&quot; data-origin-width=&quot;1592&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 그라파나와 프로메테우스도 잘 올라온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전엔 도커와 도커 컴포즈를 남이 말아놓은 걸 &quot;사용&quot;만 해봤었다.&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;p data-ke-size=&quot;size16&quot;&gt;혹시 모를 에러가 있을 수 있으니 시간을 두고 지켜보며 개선할 수 있을 것이다.&lt;/p&gt;</description>
      <category>Camp/T.I.L.</category>
      <category>compose</category>
      <category>docker</category>
      <author>BVM</author>
      <guid isPermaLink="true">https://u-bvm.tistory.com/176</guid>
      <comments>https://u-bvm.tistory.com/176#entry176comment</comments>
      <pubDate>Tue, 3 Dec 2024 21:23:10 +0900</pubDate>
    </item>
    <item>
      <title>[TIL #33] 마법의 엘리베이터</title>
      <link>https://u-bvm.tistory.com/167</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/148653&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/148653&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1729650557302&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;프로그래머스&quot; data-og-description=&quot;코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/148653&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dje81r/hyXlK6wdTs/hBftIMXTMwe8UuIUogKRdk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/148653&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/148653&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dje81r/hyXlK6wdTs/hBftIMXTMwe8UuIUogKRdk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈러 뺑뺑이 돌리는 문제.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접근법은 명확한데, 특정 테스트 케이스를 통과하지 못해서 평소보다 많은 시간을 투자했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 첫 접근부터.&lt;/p&gt;
&lt;pre id=&quot;code_1729651128464&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 음수층은 없음
// 하지만 음수층이 있는 것 처럼 생각 가능
// 모듈러 지옥

function solution(storey) {
    let total = 0;
    let carry = 0;

    while (storey &amp;gt; 0) {
        let digit = (storey % 10) + carry; // 현재 자릿수 + 캐리

        if (digit &amp;lt;= 5) {
            total += digit; // -1 버튼을 digit번 누름
            carry = 0; // 캐리 초기화
        } else {
            total += (10 - digit);
            carry = 1; // 다음 자릿수로 캐리
        }

        storey = Math.floor(storey / 10); // 다음 자릿수로 이동
    }

    if (carry === 1) {
        total += 1; // 남아있는 캐리 처리
    }

    return total;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 이거면 문제가 없어야 한다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 문제가 있었고...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당장 질문하기로 가서 유사한 문제를 찾아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;484&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uQHme/btsKgdG1sGU/yieDoqbfQK6tuC5CeHF4Tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uQHme/btsKgdG1sGU/yieDoqbfQK6tuC5CeHF4Tk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uQHme/btsKgdG1sGU/yieDoqbfQK6tuC5CeHF4Tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuQHme%2FbtsKgdG1sGU%2FyieDoqbfQK6tuC5CeHF4Tk%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;484&quot; height=&quot;106&quot; data-origin-width=&quot;484&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아하...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드대로라면 5를 더하고 20을 더하고 500을 빼기 때문에 12가 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 필요한 건 다음 자릿수가 5 이상인지도 고려하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;445였다면 그냥 5를 빼고, 그다음 자릿수도 5 이하이기 때문에 빼는 흐름이 됐을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 8로 5보다 크기 때문에 이 점도 고려가 되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 다음의 설명도 많은 도움이 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;793&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vtfGA/btsKgelxBES/IisGVO2awUBLy77LAIjGf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vtfGA/btsKgelxBES/IisGVO2awUBLy77LAIjGf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vtfGA/btsKgelxBES/IisGVO2awUBLy77LAIjGf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvtfGA%2FbtsKgelxBES%2FIisGVO2awUBLy77LAIjGf0%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;793&quot; height=&quot;654&quot; data-origin-width=&quot;793&quot; data-origin-height=&quot;654&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 같은 말을 하고 있는 것과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 점을 반영하면 다음과 같은 결과가 나온다.&lt;/p&gt;
&lt;pre id=&quot;code_1729651899167&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function solution(storey) {
    let answer = 0;
    
    while (storey !== 0) {
        let current = storey % 10; // 현재 자릿수        
        let next = Math.floor((storey % 100) / 10); // 다음 자릿수
        
        if (current &amp;gt; 5) {
            // 현재 자릿수가 5보다 크면 위로 올라가는 게 이득
            answer += (10 - current);
            storey += (10 - current);
        } else if (current === 5 &amp;amp;&amp;amp; next &amp;gt;= 5) {
            // 현재 자릿수가 5이고 다음 자릿수가 5 이상이면 위로 올라가는 게 이득
            answer += 5;
            storey += 5;
        } else {
            // 그 외의 경우는 아래로 내려가는 게 이득
            answer += current;
        }
        
        // 다음 자릿수로 이동
        storey = Math.floor(storey / 10);
    }
    
    return answer;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;352&quot; data-origin-height=&quot;365&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rsnGc/btsKfUgB0U2/XKfR2Okv0L3aQaNuEHBbok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rsnGc/btsKfUgB0U2/XKfR2Okv0L3aQaNuEHBbok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rsnGc/btsKfUgB0U2/XKfR2Okv0L3aQaNuEHBbok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrsnGc%2FbtsKfUgB0U2%2FXKfR2Okv0L3aQaNuEHBbok%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;352&quot; height=&quot;365&quot; data-origin-width=&quot;352&quot; data-origin-height=&quot;365&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;층수가 1억 이하기 때문에 큰 부하가 걸리진 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무리 문제의 스토리텔링이라도 그렇지 이건 머리에 문제가 있는 게 아닌가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 사람은 어떻게 풀었는지도 함 봐줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 해법은 다음과 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1729652113707&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function solution(storey) {
    // 기저 조건: 5 미만이면 그대로 반환
    if (storey &amp;lt; 5) return storey;
    
    // 현재 자릿수의 값 구하기
    const r = storey % 10;
    // 현재 자릿수를 제외한 나머지 숫자
    const m = (storey - r) / 10;
    
    // 두 가지 선택지 중 최소값 반환:
    // 1. 아래로 내려가기: 현재 자릿수(r)만큼 내려가고 나머지 숫자(m)에 대해 재귀
    // 2. 위로 올라가기: (10-r)만큼 올라가고 다음 숫자(m+1)에 대해 재귀
    return Math.min(r + solution(m), 10 - r + solution(m + 1));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매우 간결한 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제한 사항은 1억이기 때문에 스택이 터질 일도 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;``Math.min()``을 통한 분기도 아까의 설명글에서 설명했던 그대로다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래머스는 가독성이 나빠도 코드가 짧기만 하면 추앙하는 이상한 사람들이 많은데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 진짜 간결하고 흐름이 보이는 코드가 좋은 코드가 아닐까.&lt;/p&gt;</description>
      <category>Camp/T.I.L.</category>
      <author>BVM</author>
      <guid isPermaLink="true">https://u-bvm.tistory.com/167</guid>
      <comments>https://u-bvm.tistory.com/167#entry167comment</comments>
      <pubDate>Wed, 20 Nov 2024 17:55:54 +0900</pubDate>
    </item>
  </channel>
</rss>