<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hc-kang.log</title>
        <link>https://velog.io/</link>
        <description>안녕하세요!</description>
        <lastBuildDate>Mon, 29 Dec 2025 10:04:27 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hc-kang.log</title>
            <url>https://velog.velcdn.com/images/hc-kang/profile/5ed171bb-735f-4ddc-a5c0-14b625863888/image.webp</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hc-kang.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hc-kang" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[2025년 회고]]></title>
            <link>https://velog.io/@hc-kang/2025%EB%85%84-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hc-kang/2025%EB%85%84-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 29 Dec 2025 10:04:27 GMT</pubDate>
            <description><![CDATA[<p>아직 25년이 이틀 정도 남기는 했지만 조금 이른 시점에 올 한해를 돌아보며 회고를 작성하고자 합니다.</p>
<p>크게 업무와 개인, 두 축에서 간략하게 분기별로 정리해보면 다음과 같습니다.</p>
<h2 id="업무">업무</h2>
<h3 id="1분기">1분기</h3>
<p>1분기는 작년과 거의 비슷했는데요. 이때까지가 딱 이직 후, 현 회사에서 1년차가 된 시점이었습니다.  </p>
<p>저희 회사는 웹 리소스(HTML 문서와 이미지, 비디오)에 대한 <code>캐싱</code>과 <code>최적화</code> 솔루션을 주요 제품으로 제공하고있고 저는 그중에서도 문서와 이미지 리소스 최적화 부분을 담당하고 있습니다.  </p>
<p>이러한 이유로 대부분의 시간을 문서와 이미지 최적화 엔진의 <code>성능개선</code> 및 <code>기능추가</code>, <code>버그픽스</code>에 할애했습니다.<br>그러던 중, 사내 검수 시스템과 관련해서 몇가지 이슈를 확인하고 이를 보고했는데요, 이 이슈가 2분기에서 이어질 작업의 계기가 되었습니다.</p>
<h3 id="2분기">2분기</h3>
<p>2분기에는 1분기부터 진행하던 단위 기능에 대한 업무와 더불어, <code>사내 테스트 인프라 이전 작업</code>을 주도했습니다.  </p>
<p>당시 회사의 테스트 인프라는 <a href="https://www.linode.com/ko/">Linode</a>와 <a href="https://www.vultr.com/">Vultr</a> 라는 클라우드 벤더를 사용하고 있었는데요, 회사가 성장함에 따라 아래와 같은 몇가지 문제를 직면하게 되었습니다.</p>
<p>첫째로, 초기 구축 당시 리노드와 벌쳐는 한국, 혹은 동북아시아 리전을 지원하지 않았기에 미국 리전을 사용해야만 했습니다.<br>이렇게 서버와 물리적으로 멀리 떨어져있다보니, 단순한 API요청 조차도 800ms 이상의 시간이 소요되는 경우가 왕왕 발생하고 있었습니다.</p>
<p>둘째로, 두 클라우드 서비스는 직관적이고 단순면서도 저렴하지만, 그만큼 제공하는 기능이 제한적이고 인스턴스에 문제가 발생하는 빈도가 높았습니다.<br>특히나 사내 검수 시스템의 테스트케이스(TC) 규모가 커져가면서 인스턴스에 부하가 증가하면서 이러한 문제가 점점 더 심각해졌는데요.<br><code>Noisy Neighbor</code> 현상으로 인해, 테스트 중 뚜렷한 이유 없이 실패하고 단순 인스턴스 재시동시 성공하는 등 <strong>재현 불가능한 실패 사례</strong>가 점점 더 많아졌습니다.</p>
<p>무엇보다 심각한것은, 위 두 문제점의 시너지로 제품의 <code>데일리 테스트 소요시간도 급격히 증가</code>하고, 간헐적으로 <code>이유없이 데일리 테스트 자체가 실패하는 상황</code>도 발생하기 시작했습니다.<br>이런 상황은 개발자들의 개발 효율성을 떨어뜨릴 뿐더러 무엇보다도 검수 시스템 전체의 신뢰성을 떨어뜨리는 원인이 되었습니다.</p>
<p>이외에도 두 벤더로 흩어진 인프라를 하나로 통합하고, 추후 확장성을 고려해 단순히 가상서버(VPS)만 제공하는 것이 아니라, 전반적인 클라우드 환경 전체를 제공할 수 있는 클라우드 서비스로 이전할 필요가 있었습니다.</p>
<p>이러한 이유로, 비용과 규모, 벤더의 신뢰성 등 여러 요소를 고려하여 회사는 테스트를 포함한 전체 인프라를 <code>OCI(Oracle Cloud Infrastructure)</code>로 이전하게 되었습니다.  </p>
<h3 id="3분기">3분기</h3>
<p>3분기에는 다시 리소스 최적화 엔진의 기능 추가에 집중해서, iOS에서의 AV1 기반 비디오 최적화 작업, PNG 이미지에 대한 최적화 성능 개선 등의 작업을 주로 진행했습니다.  </p>
<p>그리고 9월부터는 이전한 OCI 클라우드 환경을 기반으로 <code>SaaS 서비스</code> 구축을 위한 초기 작업을 시작했는데요,<br>이는 기존에 제공하던 설치형 솔루션인 저희 제품에 대한 고객사/사용자의 접근성을 높이기 위한 초기 작업이었습니다.
처음 다뤄보는 Keycloak을 활용한 인증 서버 구축과, 요구사항 및 사내 구성원들의 기술 스택을 고려한 개발 환경 구축 등의 작업을 진행했습니다.</p>
<h3 id="4분기">4분기</h3>
<p>4분기에 들어 본격적으로 <code>SaaS 서비스</code> 구축을 진행했습니다.  </p>
<p>소규모 팀으로 구성된 회사에서 SaaS 서비스 구축을 진행하는 것은 처음이었는데요, 초기 요구사항 정의부터 개발 환경 구축, 서비스 개발, 테스트 및 배포 등 정말이지 모든 사이클을 직접 경험할 수 있었습니다.  </p>
<p>특히 워터폴이 아닌 짧은 주기의 스프린트 방식으로 RBAC 권한 관리, 모니터링 및 정산/대사 시스템 등의 기능을 개발하는것은 정말 새로운 경험이었습니다.  </p>
<p>마치 군시절 혼란하고 예측불가했던 경험처럼, 매 스프린트마다 예상치 못한 요구사항이 등장하고, 그에 대응하는 개발을 진행하는 것은 정말 쉽지 않은 경험이었고, 그 과정에서 &#39;개발도 중요하지만, 커뮤니케이션이 훨씬 중요하다&#39;라는 생각을 사무치도록 정말 많이 하게 되었습니다.</p>
<h2 id="개인">개인</h2>
<h3 id="1분기-1">1분기</h3>
<p>1분기에는 24년 4분기에 이어서 개인 프로젝트로 <a href="https://www.blue-rabbit.kr/">제 프로필 사이트</a>를 구축하고 개선했습니다.  </p>
<img src="https://velog.velcdn.com/images/hc-kang/post/31f916ae-91db-4f0d-b1f3-eb60e9afe1f3/image.png" alt="프로필 사이트" width="500" height="auto">

<p>아무래도 홈 서버를 사용하고 있기에, 내년 초 이사 기간에는 페이지가 내려가있을지도 모르겠네요.  </p>
<p>여하튼, 초기에는 저라는 사람과 최근 관심사를 효과적으로 전하기 위해 상용 서비스인 링크트리나 리틀리를 활용하려고 했는데요, 생각보다 번거롭기도 하고 제가 원하는 디자인과 기능을 찾기가 쉽지 않았습니다.<br>그리고 뭐.. 서비스 할 게 아니라, 제가 필요한 기능만 작성한다면 그리 어렵지도 않겠다는 생각에 직접 구현해보기로 했습니다.  </p>
<p>그렇게 구현한 프로필 사이트는 초기에는 단순히 제 블로그나 깃허브, 링크드인 바로가기 정도만을 포함했었는데요, 이후에는 저만의 개인 서재나, 업무나 일상생활등에 자주 사용하는 도구 모음을 하나씩 추가하면서, 실무나 일상생활에서 알뜰하게 사용하고 있습니다.</p>
<img src="https://velog.velcdn.com/images/hc-kang/post/c5752b2a-f608-4019-b076-c703d1164fc5/image.png" alt="도구모음 사이트" width="500" height="auto">

<p>무엇보다도 아래 두 가지 장점으로 인해 만족도가 매우 큰데요</p>
<ol>
<li>테스트와 CI/CD 파이프라인이 완벽하게 구축되어있어, 필요한 기능을 수 분 이내에 배포하고 결과를 확인할 수 있습니다. 즉, POC, MVP성 기능 개발에서 초기 장벽이 매우 낮아졌습니다. 언제든지 새로운 아이디어에 대한 검증용 API를 추가해서 배포 할 수 있고, 지금도 아래 예시와 같은 API를 <del>비밀리에</del> 제공하고있습니다.<ol>
<li><code>https://www.blue-rabbit.kr/api/holiday</code><ol>
<li>오늘이 휴일인가?: 자동화 일일 보고서 작성시 봇이 공휴일에도 뻘하게 메시지를 보내는 것을 방지하기 위해 사용 가능합니다<pre><code class="language-json">{
   &quot;today&quot;: {
       &quot;date&quot;: &quot;2025-12-29&quot;,
       &quot;isHoliday&quot;: false,
       &quot;name&quot;: null,
       &quot;dayOfWeek&quot;: &quot;월요일&quot;
   },
   &quot;nextHoliday&quot;: {
       &quot;date&quot;: &quot;2026-01-01&quot;,
       &quot;name&quot;: &quot;1월1일&quot;,
       &quot;daysFromToday&quot;: 3
   },
   &quot;weekRange&quot;: [
       ...
       {
       &quot;date&quot;: &quot;2026-01-01&quot;,
       &quot;isHoliday&quot;: true,
       &quot;name&quot;: &quot;1월1일&quot;,
       &quot;dayOfWeek&quot;: &quot;목요일&quot;
       },
       ...
   ]
}</code></pre>
</li>
</ol>
</li>
</ol>
</li>
<li>홈 서버를 사용하기에 비용 부담 없이 백엔드의 컴퓨팅 파워를 어디서든 활용할 수 있습니다. 이미지나 동영상 분석 등의 꽤나 비싼 작업도 부담없이 처리 가능합니다.</li>
</ol>
<img src="https://velog.velcdn.com/images/hc-kang/post/b5bb66b2-fa17-4cfb-91b7-bdd24b349596/image.png" alt="CI 배포 보고서 디스코드 메시지" width="500" height="auto">

<p>이런 과정을 하나씩 거치면서, 기본적인 네트워크 구성과 보안, 파이프라인의 본질적인 중요성(직접 할 수 있지만 굳이 할 필요 없는 일을 덜어내는 과정)을 깊이 있게 느끼게 되었습니다.</p>
<h3 id="2분기-1">2분기</h3>
<p>2분기에는 <a href="https://www.hellotap.me/">헬로탭</a>이라는 사이드 프로젝트를 시작했습니다.  </p>
<p>저는 이전부터 각종 세미나나 커뮤니티 행사에 가는 걸 즐겼는데요, 명함을 두고 왔거나, 준비한 명함보다 사람이 많은 경우 등 비상사태가 종종 있더라구요. 그리고 간혹 명함이 없거나, 명함이 없다는 사실 자체에 주눅드는 분들도 있다는 걸 알게 되었습니다. 그래서 &#39;회사 명함&#39;이 아닌 &#39;내 명함&#39;을 만들 수 있으면 좋겠다는 생각을 어렴풋이 하고 있었습니다.</p>
<p>그러던 중, 아두이노 키트 안에서 우연히 NFC 카드를 발견했는데요. 카드에 저장된 정보를 읽어서 특정 액션을 취하는 게 가능하더라구요. 신기해서 이것저것 알아보니, 실제로 NFC로 명함을 대체하려는 시도가 꽤 있었습니다. 심지어 제가 사용 중인 <code>hellotap.me</code> 도메인의 전 주인도 이런 사업 목적으로 도메인을 썼던 것 같더라구요.</p>
<p>하지만 기존 서비스들은 대부분 단순한 정보 전달에 그치는 것 같았습니다. 여기에 뭔가 재밌는 인터랙션을 추가하면 어떨까 싶어서, 예전에 봤던 <a href="https://poke-holo.simey.me/">CSS Pokemon</a> 프로젝트를 참고해 홀로그램 효과가 있는 &#39;진짜 명함같은 디지털 명함&#39;을 만들어보기로 했습니다.</p>
<p><a href="https://www.hellotap.me/c/%EC%98%88%EC%A0%88%EB%B0%94%EB%A5%B8-%EA%B2%80%EB%91%A5%EC%98%A4%EB%A6%AC-7940">명함 예제1 - 디자이너 박나고</a>
<a href="https://www.hellotap.me/c/%EC%82%AC%EC%A0%81%EC%9D%B8-%EB%AF%B8%EB%85%B8%ED%83%80%EC%9A%B0%EB%A5%B4%EC%8A%A4-1244">명함 예제2 - 프론트 개발자 신보리</a></p>
<p>그래서 뭐.. 다들 하시듯 무턱대고 도메인부터 하나 사고 Nest + Next 조합으로 서비스를 구축했습니다.<br>혼자 기획부터 디자인, 프론트와 백엔드, 배포까지 하려니 조금 고생하긴 했지만 그만큼 자유롭고 재미있게 개발할 수 있었습니다.</p>
<p>재밌는 점은, 혼자 작업하면 막연히 &#39;하고 싶은 대로 다 할 수 있으니 좋겠다&#39;라고 생각하기 쉬운데, 실제로 그렇게 진행하면 기술부채가 엄청난 속도로 쌓인다는 걸 체감했습니다. 오히려 혼자 일할 때 협업 못지않게 엄격한 규칙을 적용하고 정리하는 게 중요하더라구요.</p>
<img src="https://velog.velcdn.com/images/hc-kang/post/341e868d-82e8-454c-be8b-d3b117d68e1f/image.png" alt="헬로탭 서비스" width="700" height="auto">

<p>이 과정에서 &#39;사람이 읽을 수 있는 ID&#39;를 만들기 위해 간단한 npm 패키지도 만들어봤는데, <del>아무도 쓰진 않지만..</del> 재밌는 경험이었습니다.</p>
<p><a href="https://www.npmjs.com/package/ko-unique-name-generator">ko-unique-name-generator</a></p>
<p>헬로탭은 홈 서버 덕분에 비용 부담 없이 아직도 운영 중이고, 조금씩 개선을 이어가고 있습니다.</p>
<h3 id="3분기-1">3분기</h3>
<p>3분기에는 1, 2분기에 이어서 개인 프로젝트의 서재를 카드 UI로 변경하고, 이런저런 기능을 추가했습니다.  </p>
<img src="https://velog.velcdn.com/images/hc-kang/post/dcaa19f7-9741-4a4e-8035-b6f105031a1b/image.png" alt="서재 사이트" width="700" height="auto">

<p>그리고 여름 휴가로 다녀온 경주 여행에서 본 불국사와 월정교가 너무 멋져서, 이 색감을 <code>VSCode</code>와 <code>Cursor</code> 테마로 만들어보았습니다. 밤에 본 월정교와, 처마 사이로 본 밤하늘이 정말 예뻤거든요.</p>
<img src="https://velog.velcdn.com/images/hc-kang/post/2cda3781-aa59-462f-bd84-4cb1b33fccf3/image.png" alt="단청 테마들" width="700" height="auto">

<p>물론 시작은 색감이었지만, 더 신경 쓴 건 &#39;개발에 얼마나 쓸만한가&#39;였습니다. 이런저런 테마를 써보면서 &#39;예쁜 쓰레기&#39;를 꽤 마주쳤었거든요. 군시절부터 계속 생각해온 건데, 이런 소소한 도구와 환경이 생산성에 꽤 큰 영향을 준다고 봅니다. 즉 누구나 하루에 발휘할 수 있는 주의력에는 한계가 있고, 따라서 그 한계를 시간으로 메우기보다는 주의력을 효율적으로 쓰는 게 낫다고 생각하거든요. 그래서 각 토큰별 색상을 &#39;오방색을 활용하되 가독성을 해치지 않는&#39; 방향으로 잡았습니다.</p>
<p>작업하면서 디자이너분들의 고충을 조금이나마 느껴볼 수 있었고, 꽤 재밌는 경험이었습니다.</p>
<p>결과물은 <a href="https://open-vsx.org/extension/weston0713/korean-dancheong-dark">Korean Dancheong Dark</a>, <a href="https://open-vsx.org/extension/weston0713/korean-dancheong-light">Korean Dancheong Light</a>에서 보실 수 있는데요. 라이트 테마는 솔직히 그저 그렇지만, 다크 테마는 정말 만족스럽고 지금도 가장 애용하는 테마입니다.</p>
<img src="https://velog.velcdn.com/images/hc-kang/post/59689ce0-1b9b-4bbe-9707-694728a95022/image.png" alt="단청 테마 실제 적용 화면" width="700" height="auto">

<h3 id="4분기-1">4분기</h3>
<p>4분기에는 개인 프로젝트보다는 공부와 정리에, 그리고 개인사에 시간을 더 많이 썼습니다.</p>
<p>우선 Java와 Spring Boot를 조금씩 들여다보기 시작했는데요.<br>노드 개발자이긴 하지만, 현실적으로 업계 대다수를 차지하는 Java 개발자들의 사고방식과 Spring Boot라는 거대한 프레임워크에서 배울 점이 분명히 있을 거라는 생각이 들기도 했고, 그 다수의 개발자들과의 원활한 커뮤니케이션을 위해 필요한 지식이 있을 거라는 생각이 들었거든요.  </p>
<p>그리고 무엇보다 내년 초 이사를 앞두고 부동산과 인테리어 탐방에 시간을 많이 쏟고 있는데 예상은 했지만 생각보다 더 힘들고 시간이 많이 드네요.<br>이런저런 준비 때문에 개인 프로젝트는 거의 진행하지 못했습니다.</p>
<p>하지만 그래도 이제 얼추 마무리 단계인 만큼, 빠르게 정리하고 다시 재밌는 아이디어들을 꺼내보려고 합니다.</p>
<p>앞으로도 꾸준히 공부하고 정리하면서, 개발자로서 좀 더 단단해져 보려고 합니다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[내가 쓴 글·발표영상 모아보기]]></title>
            <link>https://velog.io/@hc-kang/%EB%82%B4%EA%B0%80-%EC%93%B4-%EA%B8%80%EB%B0%9C%ED%91%9C%EC%98%81%EC%83%81-%EB%AA%A8%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@hc-kang/%EB%82%B4%EA%B0%80-%EC%93%B4-%EA%B8%80%EB%B0%9C%ED%91%9C%EC%98%81%EC%83%81-%EB%AA%A8%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Fri, 05 Dec 2025 12:58:48 GMT</pubDate>
            <description><![CDATA[<img src="https://velog.velcdn.com/images/hc-kang/post/c2520160-e0ec-4db3-b761-c4feab4fac23/image.png" width="800px" />

<p>공유용 인덱스입니다.
주제별로 묻힌 글들을 정리해두었습니다.
웹·네트워크·자바스크립트·이미지 처리·테스트·홈서버 등 다양한 내용이 있습니다!</p>
<p>⸻</p>
<h3 id="http--웹-기본기">HTTP / 웹 기본기</h3>
<p>웹의 구조·의도·프로토콜 흐름을 정리한 글들입니다.</p>
<ul>
<li><a href="https://velog.io/@hc-kang/HTTP%EB%8A%94-%EC%99%9C-%EC%9D%B4%EB%A0%87%EA%B2%8C-%EC%83%9D%EA%B2%BC%EC%9D%84%EA%B9%8C-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0">HTTP는 왜 이렇게 생겼을까</a></li>
<li><a href="https://velog.io/@hc-kang/http">HTTP 개요 정리</a></li>
<li><a href="https://velog.io/@hc-kang/HTTP2-%EC%A2%80-%EB%8D%94-%EC%9E%90%EC%84%B8%ED%9E%88-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0">HTTP/2 자세히 보기 (1/2)</a></li>
<li><a href="https://velog.io/@hc-kang/HTTP2-%EC%A2%80-%EB%8D%94-%EC%9E%90%EC%84%B8%ED%9E%88-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0">HTTP/2 자세히 보기 (2/2)</a><ul>
<li><a href="https://www.youtube.com/watch?v=oVzpGQB5O6E">HTTP 발표 영상(YouTube, HOCO 오픈 스테이지)</a></li>
</ul>
</li>
<li><a href="https://velog.io/@hc-kang/RESTful%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC">RESTful에 대하여</a></li>
<li><a href="https://velog.io/@hc-kang/CORS%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC">CORS에 대하여</a></li>
<li><a href="https://velog.io/@hc-kang/103-Early-Hints-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%8B%A4%ED%8C%A8">103 Early Hints 테스트 실패 기록</a>
⸻</li>
</ul>
<h3 id="네트워크--보안--인프라">네트워크 / 보안 / 인프라</h3>
<p>프로토콜·TLS·Docker 네트워크 구조를 정리한 글들입니다.</p>
<ul>
<li><a href="https://velog.io/@hc-kang/SSLTLS-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0-%ED%86%A0..%ED%86%B5%EC%8B%A0%EB%B3%B4%EC%95%88">SSL/TLS 톺아보기</a></li>
<li><a href="https://velog.io/@hc-kang/%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C-%EC%95%B1-vs-%EB%8F%84%EC%BB%A4-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88">네이티브 앱 vs Docker 컨테이너</a></li>
<li><a href="https://velog.io/@hc-kang/%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C-%EC%95%B1-vs-%EB%8F%84%EC%BB%A4-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88and-Host-vs-Bridge-part2">네이티브 앱 vs Docker (Host vs Bridge) 파트2</a></li>
</ul>
<p>⸻</p>
<h3 id="js--nodejs--성능">JS / Node.js / 성능</h3>
<p>언어 내부 구조, 최적화, CPU 수준 알고리즘 등.</p>
<ul>
<li><a href="https://velog.io/@hc-kang/%EB%8B%B9%EC%8B%A0%EC%9D%98-JS%EB%A5%BC-%EC%A1%B0%EA%B8%88%EC%9D%B4%EB%9D%BC%EB%8F%84-%EB%8D%94-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%ED%95%B4%EB%B4%85%EC%8B%9C%EB%8B%A4">당신의 JS를 조금이라도 더 빠르게</a></li>
<li><a href="https://velog.io/@hc-kang/bitCount-%ED%95%A8%EC%88%98%EC%9D%98-%EB%82%B4%EB%B6%80-%EA%B5%AC%ED%98%84-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-%EB%B3%91%EB%A0%AC-%EA%B3%84%EC%82%B0">bitCount 내부 구현 (병렬 비트 계산)</a></li>
<li><a href="https://velog.io/@hc-kang/%EB%8B%B9%EC%8B%A0%EC%9D%98-Node.js%EB%A5%BC-%EC%A3%BD%EC%9D%B4%EB%8A%94-n%EA%B0%80%EC%A7%80-%EB%B0%A9%EB%B2%95-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%88%84%EC%88%98">Node.js 메모리 누수 분석</a></li>
</ul>
<p>⸻</p>
<h3 id="테스트--설계">테스트 / 설계</h3>
<p>구조적 관점에서의 테스트 가능성, TDD의 한계.</p>
<ul>
<li><a href="https://velog.io/@hc-kang/TDD%EB%B3%B4%EB%8B%A4%EB%8A%94-Testable-%ED%95%AD%ED%95%B4%ED%94%8C%EB%9F%AC%EC%8A%A4-2%EA%B8%B0">TDD보다는 Testable (항해플러스 2기)</a></li>
</ul>
<p>⸻</p>
<h3 id="이미지-처리--포맷--서비스-운영">이미지 처리 / 포맷 / 서비스 운영</h3>
<p>실서비스 관점에서의 이미지 처리 노하우.</p>
<ul>
<li><a href="https://velog.io/@hc-kang/%EC%9D%B4%EB%AF%B8%EC%A7%80%EB%A5%BC-%EB%8B%A4%EB%A3%A8%EC%96%B4%EC%95%BC%ED%95%98%EB%8A%94-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%93%A4%EC%9D%84-%EC%9C%84%ED%95%9C-%EA%B0%84%EB%8B%A8%ED%95%9C-%EB%A7%A4%EB%89%B4%EC%96%BC">이미지를 다루어야 하는 개발자를 위한 간단 매뉴얼</a><ul>
<li><a href="https://www.youtube.com/watch?v=5iFwcL4tqBU">HTTP 발표 영상(YouTube, HOCO 오픈 스테이지)</a></li>
</ul>
</li>
</ul>
<p>⸻</p>
<h3 id="홈-서버--사이드-프로젝트">홈 서버 / 사이드 프로젝트</h3>
<p>직접 구축하고 운영해본 서버 경험.</p>
<ul>
<li><a href="https://velog.io/@hc-kang/%EA%B0%80%EC%9E%A5-%EC%A0%80%EB%A0%B4%ED%95%98%EA%B2%8C-%EA%B0%9C%EC%9D%B8-%EC%84%9C%EB%B2%84-%EC%9A%B4%EC%98%81%ED%95%98%EA%B8%B0-%ED%99%88-%EC%84%9C%EB%B2%84-%EC%9A%B4%EC%98%81%EA%B8%B0">가장 저렴하게 개인 서버 운영하기 (홈 서버 운영기)</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커/쿠버네티스 스터디 6일차]]></title>
            <link>https://velog.io/@hc-kang/%EB%8F%84%EC%BB%A4%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%8A%A4%ED%84%B0%EB%94%94-6%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@hc-kang/%EB%8F%84%EC%BB%A4%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%8A%A4%ED%84%B0%EB%94%94-6%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Tue, 26 Aug 2025 23:57:46 GMT</pubDate>
            <description><![CDATA[<h2 id="day-6-82---쿠버네티스-설치">Day 6 (8/2) - 쿠버네티스 설치</h2>
<p><strong>📖 진도</strong>: 5장 / 236~259p</p>
<h3 id="51-쿠버네티스-설치-환경의-종류">5.1 쿠버네티스 설치 환경의 종류</h3>
<ul>
<li><p>도커 엔진과 다르게, 쿠버네티스는 사용 환경과 목적에 따라 매우 다양한 설치 방법이 있음</p>
</li>
<li><p>목적에 따른 구분</p>
<table>
<thead>
<tr>
<th>용도</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>개발 용도</td>
<td>Minikube<br>Docker Desktop on Mac</td>
</tr>
<tr>
<td>테스트 및 운영</td>
<td>kops<br>kubespray<br>kubeadm<br>EKS, GKE 등 Managed Service</td>
</tr>
</tbody></table>
</li>
<li><p>환경에 따른 구분</p>
<ul>
<li>자체 서버(on-premise) 환경에 직접 설치하는 경우</li>
<li>클라우드 플랫폼에 직접 설치</li>
<li>관리형 클라우드 서비스 사용</li>
</ul>
</li>
<li><p>요약</p>
<img src="https://velog.velcdn.com/images/hc-kang/post/c08ec680-0b8d-47d1-8423-11e68db2398e/image.png" alt="쿠버네티스 설치 환경의 종류" width="500" />

</li>
</ul>
<h3 id="52-쿠버네티스-버전-선택">5.2 쿠버네티스 버전 선택</h3>
<ul>
<li>쿠버네티스의 핵심 개념은 같겠지만, 버전 업데이트가 매우 빨라 사용 방법이나 기능이 달라질 수 있음.</li>
<li>너무 최신이나, 너무 오래된 버전은 사용하기 어려울 수 있음.</li>
<li>이 책에서는 1.32 버전을 사용함.</li>
</ul>
<h3 id="53-개발-용도의-쿠버네티스-설치">5.3 개발 용도의 쿠버네티스 설치</h3>
<h4 id="531-docker-desktop-on-mac--windows-쿠버네티스-사용">5.3.1 Docker Desktop on Mac / Windows 쿠버네티스 사용</h4>
<ul>
<li>도커 데스크탑이나 <code>orbstack</code> 등의 환경에 내장된 쿠버네티스를 사용할 수 있음</li>
</ul>
<h4 id="532-minikube로-쿠버네티스-설치">5.3.2 Minikube로 쿠버네티스 설치</h4>
<ul>
<li>로컬에서 가상머신이나 도커 엔진을 통해 쿠버네티스를 사용해 볼 수 있음.</li>
<li>간단한 테스트 환경은 구축 가능하나, 실제 운영환경 적용은 힘들고 일부 기능 사용 불가</li>
</ul>
<h3 id="54-여러-서버로-구성된-쿠버네티스-클러스터-설치">5.4 여러 서버로 구성된 쿠버네티스 클러스터 설치</h3>
<ul>
<li>주의사항<ul>
<li>모든 서버의 ntp를 통해 시간을 동기화해야 함</li>
<li>(서버 복제 사용시) 각 서버의 MAC 주소가 모두 달라야 함</li>
<li>모든 서버가 최소 2GB 메모리, 2CPU 이상 필요</li>
<li><code>swapoff -a</code> 명령어로 swap 메모리 사용 중지<ul>
<li>스왑 사용시 성능이 일관되지 못함</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="541-kubeadm으로-쿠버네티스-설치">5.4.1 kubeadm으로 쿠버네티스 설치</h4>
<p>(생략)</p>
<h4 id="542-kops로-aws에서-쿠버네티스-설치">5.4.2 kops로 AWS에서 쿠버네티스 설치</h4>
<p>(생략)</p>
<h4 id="543-구글-클라우드-플랫폼의-gke로-쿠버네티스-사용하기">5.4.3 구글 클라우드 플랫폼의 GKE로 쿠버네티스 사용하기</h4>
<p>(생략)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커/쿠버네티스 스터디 5일차]]></title>
            <link>https://velog.io/@hc-kang/%EB%8F%84%EC%BB%A4%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%8A%A4%ED%84%B0%EB%94%94-5%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@hc-kang/%EB%8F%84%EC%BB%A4%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%8A%A4%ED%84%B0%EB%94%94-5%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Tue, 12 Aug 2025 11:43:36 GMT</pubDate>
            <description><![CDATA[<h2 id="day-5-81---도커-컴포즈">Day 5 (8/1) - 도커 컴포즈</h2>
<p><strong>📖 진도</strong>: 4장 / 211~229p</p>
<h3 id="41-도커-컴포즈를-사용하는-이유">4.1 도커 컴포즈를 사용하는 이유</h3>
<ul>
<li>여러 컨테이너를 하나의 애플리케이션으로 사용시, <code>run</code> 커맨드를 여러 번 사용할수도 있지만, 이는 관리가 어려움</li>
<li>이를 해결하기 위해 <code>docker-compose.yaml</code> 파일을 사용하여 여러 컨테이너를 하나의 애플리케이션으로 관리할 수 있음</li>
<li>여러 컨테이너의 옵션과 환경을 미리 정의해두고 사용할 수 있음</li>
</ul>
<h3 id="42-도커-컴포즈-설치">4.2 도커 컴포즈 설치</h3>
<ul>
<li>생략</li>
</ul>
<h3 id="43-도커-컴포즈-사용">4.3 도커 컴포즈 사용</h3>
<h4 id="431-도커-컴포즈-기본-사용법">4.3.1 도커 컴포즈 기본 사용법</h4>
<ul>
<li>가장 먼저 <code>docker-compose.yaml</code> 파일을 정의해야 함</li>
</ul>
<h5 id="4311-docker-composeyaml-작성과-활용">4.3.1.1 docker-compose.yaml 작성과 활용</h5>
<ul>
<li><p><code>docker-compose.yaml</code>을 사용하지 않는 경우</p>
<pre><code class="language-bash"># docker run -d --name mysql \
alicek106/composetest:mysql \
mysqld

# docker run -d -p 80:80 \
--link mysql:db --name web \
alicek106/composetest:web \
apachectl -DFOREGROUND</code></pre>
</li>
<li><p><code>docker-compose.yaml</code> 파일을 사용하는 경우</p>
<pre><code class="language-yaml">services:
  web:
    image: alicek106/composetest:web
    ports:
      - 80:80
    links:
      - mysql:db
    command: apachectl -DFOREGROUND
  mysql:
    image: alicek106/composetest:mysql
    command: mysqld</code></pre>
</li>
</ul>
<h5 id="4312-도커-컴포즈의-프로젝트-서비스-컨테이너">4.3.1.2 도커 컴포즈의 프로젝트, 서비스, 컨테이너</h5>
<pre><code class="language-mermaid">graph LR
  A[docker-compose.yml] --&gt; B[프로젝트&lt;br/&gt;ubuntu]
  B --&gt; C[서비스&lt;br/&gt;web]
  B --&gt; D[서비스&lt;br/&gt;mysql]
  C --&gt; E[컨테이너&lt;br/&gt;web x2]
  D --&gt; F[컨테이너&lt;br/&gt;mysql]

  style A fill:#fff,stroke:#333,stroke-width:1px
  style B fill:#f0f0f0,stroke:#333,stroke-width:1px
  style C fill:#d0f0ff,stroke:#333,stroke-width:1px
  style D fill:#d0f0ff,stroke:#333,stroke-width:1px
  style E fill:#c0ffc0,stroke:#333,stroke-width:1px
  style F fill:#c0ffc0,stroke:#333,stroke-width:1px</code></pre>
<ul>
<li>필요한 경우 <code>docker compose scale mysql=2</code>와 같은 명령어로 컨테이너 수를 조정할 수 있음</li>
<li>또한 <code>docker compose up -d mysql</code>과 같은 명령어로 컴포즈 파일에 정의된 특정 서비스의 컨테이너만 시작할 수도 있음</li>
<li>프로젝트의 이름은 기본적으로 디렉토리명을 사용하나, <code>docker compose -p &lt;project_name&gt;</code>과 같은 옵션으로 지정할 수 있음</li>
</ul>
<h4 id="432-도커-컴포즈-활용">4.3.2 도커 컴포즈 활용</h4>
<h5 id="4321-yaml-파일-작성">4.3.2.1 YAML 파일 작성</h5>
<ul>
<li><p>도커 컴포즈 파일은 크게 세 부분으로 나뉨</p>
<ul>
<li>서비스 정의(services)</li>
<li>볼륨 정의(volumes)</li>
<li>네트워크 정의(networks)</li>
</ul>
</li>
<li><p>서비스 정의</p>
<pre><code class="language-yaml">services:
  web:
    image: alicek106/composetest:web # 베이스로 사용할 이미지
    links:
      - mysql:db # 다른 서비스와 연결 - deprecated
      - redis:cache
    depends_on: # 의존성 설정
      - mysql
      - redis
    environment:
      - MYSQL_HOST=mysql
      - MYSQL_PORT=3306
      # 혹은
      MYSQL_HOST: mysql
      MYSQL_PORT: 3306
    command: apachectl -DFOREGROUND</code></pre>
<ul>
<li><p>그러나 <code>links</code>와 <code>depends_on</code>은 컨테이너의 가동만을 확인하며, 실제 어플리케이션의 구동은 확인하지 않음. 아래와 같은 방법으로 확인 가능</p>
<ul>
<li><p><code>yaml</code></p>
<pre><code class="language-yaml">...
entrypoint: ./sync_script.sh mysql:3306</code></pre>
</li>
<li><p>검증 스크립트</p>
<pre><code class="language-bash">until (&lt;상태확인 명령어&gt;); do
  echo &quot;depend container is not available yet&quot;
  sleep `
done
echo &quot;depends_on container is ready&quot;</code></pre>
</li>
</ul>
</li>
</ul>
</li>
<li><p>네트워크 정의</p>
<pre><code class="language-yaml">services:
  myservice:
    image: nginx
    networks:
      - mynetwork
networks:
  mynetwork:
    driver: overlay # 도커 스웜에서 overlay 네트워크 사용
  ipam: # IP Address Manager 사용을 위한 옵션. 상세한 설정을 위해 사용
    driver: mydriver
    config:
      sublet: 172.20.0.0/16 # 서브넷 범위
      ip_range: 172.20.5.0/24 # 사용 가능한 IP 범위
      gateway: 172.20.5.1 # 서브넷 내에서 사용할 게이트웨이 주소
  other_network:
    external: true # 외부 네트워크 사용</code></pre>
</li>
<li><p>볼륨 정의</p>
<pre><code class="language-yaml">services:
  mysql:
    volumes:
      - my_test_data:/data

volumes:
  my_test_data:
    driver: local

  other_volume:
    external: true</code></pre>
</li>
<li><p><code>YAML</code> 파일 검증하기</p>
<ul>
<li><code>docker compose config</code> 명령어로 파일 형식 검증 가능</li>
<li><code>-f ../some/other/file.yml</code> 옵션을 통해 다른 파일을 지정할 수 있음</li>
</ul>
</li>
</ul>
<h5 id="4322-도커-컴포즈-네트워크">4.3.2.2 도커 컴포즈 네트워크</h5>
<ul>
<li><code>YAML</code> 파일에 네트워크를 별도로 지정하지 않으면, 도커 컴포즈는 각 프로젝트별로 브리지 타입 네트워크를 생성<ul>
<li><code>docker compose up</code> 뿐 아니라, <code>docker compose scale</code> 명령어로 생성된 컨테이너도 이 네트워크에 속함</li>
</ul>
</li>
<li>이 브리지 네트워크 내의 컨테이너는 서비스의 이름으로 접근 가능(RR; Round Robin)</li>
</ul>
<h5 id="4323-도커-스웜-모드와-함께-사용하기">4.3.2.3 도커 스웜 모드와 함께 사용하기</h5>
<ul>
<li>도커 컴포즈를 위해 정의된 <code>docker-compose.yml</code> 파일은 <code>docker stack</code> 명령어로 도커 스웜 모드에서 사용할 수 있음</li>
</ul>
<h3 id="44-도커-학습을-마치며-도커-컨테이너-생태계">4.4 도커 학습을 마치며: 도커 컨테이너 생태계</h3>
<pre><code class="language-mermaid">graph TD
  A[User] --&gt;|&quot;User interacts with&lt;/br&gt;the Docker Engine&quot;| B[Docker Engine]
  B --&gt;|&quot;Engine communicates&lt;/br&gt;with containerd&quot;| C[containerd]
  C --&gt; D[runC]
  C --&gt;|&quot;containerd spins up runC&lt;/br&gt;or other OCI compliant runtime&lt;/br&gt;to run containers&quot;| D2[runC]
  C --&gt; E[&quot;...&quot;]
  C --&gt; F[&quot;...&quot;]

  style A fill:#f2f2f2,stroke:#333,stroke-width:1px
  style B fill:#d9edf7,stroke:#31708f,stroke-width:1px
  style C fill:#dff0d8,stroke:#3c763d,stroke-width:1px
  style D fill:#fcf8e3,stroke:#8a6d3b,stroke-width:1px
  style D2 fill:#fcf8e3,stroke:#8a6d3b,stroke-width:1px
  style E fill:#fcf8e3,stroke:#8a6d3b,stroke-width:1px
  style F fill:#fcf8e3,stroke:#8a6d3b,stroke-width:1px</code></pre>
<ul>
<li>컨테이너의 표준은 OCI(Open Container Initiative) 표준을 따름</li>
<li>도커 엔진은 컨테이너를 관리하기 위해 <code>containerd</code>를 사용</li>
<li><code>containerd</code>는 컨테이너를 실행하기 위해 <code>runC</code>를 사용</li>
<li>결과적으로 도커 엔진은 컨테이너가 아니며, 단순히 <code>containerd</code>를 사용하기 위한 도구임</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커/쿠버네티스 스터디 4일차]]></title>
            <link>https://velog.io/@hc-kang/%EB%8F%84%EC%BB%A4%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%8A%A4%ED%84%B0%EB%94%94-4%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@hc-kang/%EB%8F%84%EC%BB%A4%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%8A%A4%ED%84%B0%EB%94%94-4%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Wed, 06 Aug 2025 00:03:54 GMT</pubDate>
            <description><![CDATA[<h2 id="day-4-731---도커-스웜">Day 4 (7/31) - 도커 스웜</h2>
<p><strong>📖 진도</strong>: 3장 / 168~210p</p>
<h3 id="핵심-개념"><strong>핵심 개념</strong></h3>
<ul>
<li>도커 스웜: 여러 서버를 클러스터로 구성하여 수평 확장성을 제공하는 기능</li>
<li>서비스 디스커버리: 새로 추가된 서버나 컨테이너가 추가/삭제되었을 때 이를 서비스 목록에 반영하는 기능</li>
<li>NTP(Network Time Protocol): 클러스터 내부 시간 동기화를 위한 프로토콜</li>
<li>Raft Consensus 알고리즘: 리더 선출을 위한 알고리즘</li>
<li>노드: 클러스터를 구성하는 서버</li>
<li>서비스: 같은 이미지에서 생성된 컨테이너의 집합</li>
<li>태스크: 서비스를 구성하는 개별 컨테이너</li>
<li>레플리카: 서비스를 구성하는 컨테이너(태스크)의 수</li>
<li>글로벌 서비스: 모든 노드에 컨테이너를 배포하는 서비스</li>
<li><code>secret</code>, <code>config</code> 기능</li>
</ul>
<h3 id="실습-내용"><strong>실습 내용</strong></h3>
<h4 id="도커-스웜-모드-클러스터-구축"><strong>도커 스웜 모드 클러스터 구축</strong></h4>
<ul>
<li><p>매니저 노드로 사용할 서버에서 스웜 클러스터 시작</p>
<pre><code class="language-bash">docker swarm init --advertise-addr 192.168.0.100
# advertise-addr: 매니저 노드의 IP 주소. 외부에서 접근 가능한 주소이고, 자동으로 선택도 가능하지만 다수의 인터페이스가 있는 경우, 도커는 자체적으로 외부 IP를 선택 할 수 없음.</code></pre>
</li>
<li><p>워커 노드로 사용할 서버에서 스웜 클러스터 참여</p>
<pre><code class="language-bash">docker swarm join --token SWMTKN-1-5cj... 192.168.0.100:2377</code></pre>
<ul>
<li>이때 사용하는 토큰은 매니저 노드에서 <code>docker swarm join-token --rotate</code> 명령어로 새로 생성할 수 있음.</li>
</ul>
</li>
<li><p>추가된 노드의 삭제는 <code>docker swarm leave</code> 명령어로 가능</p>
<ul>
<li>매니저 노드는 <code>docker swarm leave --force</code> 명령어로 삭제 가능</li>
</ul>
</li>
<li><p>워커에서 매니저로, 매니저에서 워커로의 이동은 아래의 명령어로 가능</p>
<pre><code class="language-bash">docker node promote &lt;node_id&gt;
docker node demote &lt;node_id&gt;</code></pre>
</li>
</ul>
<h4 id="서비스-생성"><strong>서비스 생성</strong></h4>
<ul>
<li>서비스 생성은 <code>docker service create</code></li>
<li>서비스 목록은 <code>docker service ls</code></li>
<li>서비스의 자세한 정보는 <code>docker service ps &lt;service_name&gt;</code></li>
<li>서비스 삭제는 <code>docker service rm &lt;service_name&gt;</code></li>
</ul>
<h3 id="배운-내용"><strong>배운 내용</strong></h3>
<h4 id="도커-스웜-모드의-구조"><strong>도커 스웜 모드의 구조</strong></h4>
<ul>
<li>매니저 노드<ul>
<li>클러스터 관리 및 서비스 배포, 실제 작업도 수행 가능</li>
<li>다중화가 권장되며, 네트워크 파티셔닝시에도 정상 동작을 위해 홀수개 구성 권장</li>
<li>다만 매니저 노드 다중화와 클러스터의 성능은 무관함</li>
<li>매니저 중에서도 일반 매니저와 <strong>리더 매니저</strong>가 있음</li>
</ul>
</li>
<li>워커 노드: 실제 작업을 수행하는 노드</li>
</ul>
<h4 id="secret-config-기능"><strong>secret, config 기능</strong></h4>
<ul>
<li><p>도커 스웜과 같은 클러스터 환경에서, 환경변수와 설정 파일을 호스트마다 마련하는 것은 비효율적임.</p>
<ul>
<li>매번 세팅하고, 업데이트 해야 함.</li>
</ul>
</li>
<li><p>또한 비밀번호 등 민감한 정보를 환경변수로 설정하는 것은 보안상으로도 좋지 않다.</p>
</li>
<li><p>이러한 문제를 해결하기 위해, 도커 스웜에서는 <code>secret</code>, <code>config</code> 기능을 제공한다.</p>
<ul>
<li><code>secret</code>은 민감한 정보를 저장하는 기능</li>
<li><code>config</code>는 설정 파일을 저장하는 기능</li>
</ul>
</li>
<li><p><code>secret</code> 생성</p>
<pre><code class="language-bash">docker secret create my-secret my-secret.txt

docker secret ls

docker secret inspect my-secret
# inspect로 조회시에도 실제 secret을 확인할 수는 없음
# 파일 시스템이 아닌 메모리에 저장되므로 휘발성을 띄고 더 안전함.</code></pre>
</li>
<li><p><code>config</code> 생성</p>
<pre><code class="language-bash">docker config create my-config my-config.yaml

docker config ls

docker config inspect my-config</code></pre>
<ul>
<li><code>secret</code>과 다르게 <code>Data</code>라는 필드로 Base64 인코딩된 실제 내용을 확인 가능</li>
</ul>
</li>
</ul>
<h4 id="도커-스웜-네트워크"><strong>도커 스웜 네트워크</strong></h4>
<ul>
<li><p>스웜 네트워크는 일반적인 도커 네트워크와는 다소 다른 방법으로 동작</p>
<ul>
<li>스웜 모드는 다수의 도커 엔진을 통제해야 하기에, 앞서 다룬 네트워크들의 풀이 필요함.</li>
<li>또한 다수의 노드 내의 컨테이너에 접근 할 수 있어야 하므로, 라우팅 기능도 필요함.</li>
<li>이로인해 스웜 모드는 자체적으로 네트워크 드라이버를 가지고 활용함.</li>
</ul>
</li>
<li><p>스웜 모드의 <code>ingress</code> 네트워크</p>
<ul>
<li>스웜 모드 <code>init</code>시 자동으로 생성되는 예약된 오버레이 네트워크</li>
<li>외부 클라이언트가 어떤 노드의 IP:PORT로 접속해도 동일한 iptable/IPVS가 서비스의 VIP(10.0.0.x)로 전달(DNAT)하여 접속 할 수 있게 함<ul>
<li>로컬 태스크가 있다면 바로 전달</li>
<li>없으면 RR로 원격 노드로 전달</li>
</ul>
</li>
</ul>
</li>
<li><p>오버레이 네트워크</p>
<ul>
<li>스웜 모드에서 사용하는 네트워크 드라이버</li>
<li>여러 개의 도커 데몬을 하나의 네트워크로 묶어주는 역할</li>
<li>각 스웜 노드의 컨테이너들이 <code>오버레이 네트워크</code>의 서브넷에 해당하는 IP를 할당받고, 이를 통해 상호 통신이 가능함</li>
<li>앞서 다룬 <code>ingress</code> 네트워크 또한 오버레이 네트워크의 위에서 동작하며, 예약된 특수한 네트워크로 볼 수 있음</li>
</ul>
</li>
<li><p><code>docker_gwbridge</code> 네트워크</p>
<ul>
<li>외부로 나가는 통신을 위한 네트워크</li>
<li>오버레이 네트워크 트래픽의 종단점(VTEP; Virtual Tunnel End Point) 역할을 담당</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커/쿠버네티스 스터디 3일차]]></title>
            <link>https://velog.io/@hc-kang/%EB%8F%84%EC%BB%A4%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%8A%A4%ED%84%B0%EB%94%94-3%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@hc-kang/%EB%8F%84%EC%BB%A4%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%8A%A4%ED%84%B0%EB%94%94-3%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Thu, 31 Jul 2025 00:29:00 GMT</pubDate>
            <description><![CDATA[<h2 id="day-3-730---도커파일과-도커-데몬">Day 3 (7/30) - 도커파일과 도커 데몬</h2>
<p><strong>📖 진도</strong>: 2.4, 2.5 / 110~167p</p>
<h3 id="핵심-개념"><strong>핵심 개념</strong></h3>
<ul>
<li>Dockerfile 작성법과 주요 명령어</li>
<li>Dockerfile 사용시 주의사항</li>
<li>도커의 구조<ul>
<li>도커 데몬과 도커 클라이언트</li>
<li><img src="https://velog.velcdn.com/images/hc-kang/post/72e0b0c1-032b-44fa-aa70-c82f3281562e/image.png" width="500"></li>
</ul>
</li>
<li>도커 데몬 모니터링</li>
</ul>
<h3 id="실습-내용"><strong>실습 내용</strong></h3>
<ul>
<li>Dockerfile 작성<ul>
<li>주요 명령어<ul>
<li><code>FROM</code>, <code>RUN</code>, <code>COPY</code>, <code>ADD</code>, <code>CMD</code>, <code>ENTRYPOINT</code>, <code>EXPOSE</code>, <code>ENV</code>, <code>VOLUME</code>, <code>USER</code>, <code>WORKDIR</code>, <code>STOPSIGNAL</code>, <code>HEALTHCHECK</code>, <code>ONBUILD</code></li>
</ul>
</li>
<li>명령어 단위 레이어 생성 및 캐싱 활용</li>
</ul>
</li>
<li>도커 클라이언트와 데몬 연결<ul>
<li>소켓을 사용한 연결(기본값)</li>
<li>API를 사용한 원격 연결</li>
</ul>
</li>
<li>도커 데몬 모니터링</li>
</ul>
<h3 id="배운-내용"><strong>배운 내용</strong></h3>
<h4 id="dockerfile-명령어의-올바른-사용"><strong>Dockerfile 명령어의 올바른 사용</strong></h4>
<ul>
<li><code>LABEL</code> 명령어를 사용해 이미지에 메타데이터를 추가할 수 있음. 이를 통해 <code>docker images --filter label=SOME_LABEL</code> 와 같은 필터링이 가능하다. 관리가 보다 용이해진다.</li>
<li><code>COPY</code> 와 <code>ADD</code> 명령어의 차이점<ul>
<li><code>COPY</code> 명령어는 파일을 복사한다.</li>
<li><code>ADD</code> 명령어는 1. 파일을 복사하기도 하고, 2. 파일을 다운로드(URL) 받기도 하며, 3. 압축파일을 풀어서 복사하기도 한다.<ul>
<li>따라서 혼란을 유발하거나, 의도치 않은 변경된 리소스(URL)을 복사할 수 있다.</li>
</ul>
</li>
<li>결론적으로, 어지간하면 <code>COPY</code> 명령어를 사용하자.</li>
</ul>
</li>
</ul>
<h4 id="멀티-스테이지-빌드를-통한-이미지-크기-줄이기"><strong>멀티 스테이지 빌드를 통한 이미지 크기 줄이기</strong></h4>
<ul>
<li><p>멀티 스테이지 빌드를 통해, 빌드 환경과 실행 환경을 분리 할 수 있다.</p>
<ul>
<li><p>이를 통해 실행 환경의 이미지를 더 가볍게 만들 수 있다.</p>
<pre><code class="language-dockerfile"># 빌드 환경 1 - 빌드를 위한 golang 이미지(대용량) 사용
FROM golang
ADD main.go /root
WORKDIR /root
RUN go build -o /root/mainApp /root/main.go

# 빌드 환경 2
FROM golang as builder2 # 이름을 지정해서 사용할 수도 있다.
ADD main2.go /root
WORKDIR /root
RUN go build -o /root/mainApp2 /root/main2.go

# 실행 환경 - alpine 이미지에 빌드된 파일만 복사
FROM alpine:latest
WORKDIR /root
COPY --from=0 /root/mainApp . # 빌드 환경 1의 결과물 복사(인덱스 0)
COPY --from=builder2 /root/mainApp2 . # 빌드 환경 2의 결과물 복사(이름 지정)
CMD [&quot;SOME_CMD&quot;]</code></pre>
</li>
</ul>
</li>
</ul>
<h4 id="컨테이너-보안이-중요한-이유"><strong>컨테이너 보안이 중요한 이유</strong></h4>
<ul>
<li>컨테이너의 내부 사용자를 <code>root</code>로 사용하는 것을 피해야 하는 이유는, 볼륨이 공유된 경우 내부 사용자가 <code>root</code>로서 호스트의 파일 시스템에 접근할 수 있기 때문</li>
</ul>
<h4 id="stopsignal-명령어"><strong>STOPSIGNAL 명령어</strong></h4>
<ul>
<li>컨테이너 종료 시 보내는 시그널을 설정한다.<ul>
<li><code>STOPSIGNAL SIGKILL</code> 과 같이 설정할 수 있다.</li>
</ul>
</li>
<li><code>STOPSIGNAL</code> 기본값은 <code>SIGTERM</code>이며, 실제로 종료가 안전하게 되려면 애플리케이션이 해당 시그널을 수신하고 정리 작업을 수행할 수 있어야 한다.<ul>
<li>특히 Node.js 등에서는 <code>process.on(&#39;SIGTERM&#39;)</code> 등의 핸들러 구현이 필요하다.</li>
</ul>
</li>
</ul>
<h4 id="cmd와-entrypoint-명령어"><strong>CMD와 ENTRYPOINT 명령어</strong></h4>
<ul>
<li><p><code>CMD</code>와 <code>ENTRYPOINT</code> 명령어의 차이점</p>
<ul>
<li><code>CMD</code>는 컨테이너 실행 시 실행되는 명령어를 설정한다.</li>
<li><code>ENTRYPOINT</code>는 <code>CMD</code>를 인자로 사용할 수 있는 스크립트의 역할을 할 수 있다.</li>
</ul>
</li>
<li><p>이에 유의하지 않으면 컨테이너의 상태 관측에 문제가 생길 수 있다.</p>
<ul>
<li>컨테이너는 <strong>PID 1</strong> 프로세스를 기준으로 컨테이너의 상태를 판단한다.<ul>
<li>따라서 어떤 프로세스가 <strong>PID 1</strong>을 차지하느냐가 신호 처리 및 종료 처리의 핵심이다.</li>
</ul>
</li>
<li><code>ENTRYPOINT [&quot;/bin/bash&quot;, &quot;/entrypoint.sh&quot;]</code>처럼 쉘을 통해 실제 실행 파일을 감싸면, <strong>bash가 PID 1을 점유</strong>하고 <code>/entrypoint.sh</code>는 그 하위 프로세스가 된다.<ul>
<li>이 경우 시그널이 애플리케이션까지 전달되지 않아 graceful shutdown이 실패할 수 있으며, <code>exec &quot;$@&quot;</code>로 실행을 위임하는 방식이 필요하다.</li>
</ul>
</li>
<li>반면 <code>ENTRYPOINT [&quot;node&quot;, &quot;index.js&quot;]</code>처럼 직접 실행할 경우, <code>node</code>가 PID 1이 되며 <strong>signal, exit code, 상태 모니터링 등에서 문제가 생기지 않는다.</strong></li>
<li>이러한 이유로, 복잡한 초기화 스크립트를 사용해야 하는 경우가 아니라면 <strong>애플리케이션 바이너리나 런타임(node 등)을 직접 ENTRYPOINT로 설정</strong>하는 것이 컨테이너 상태 추적 측면에서 가장 안전하다.</li>
</ul>
</li>
<li><p><code>CMD</code>와 <code>ENTRYPOINT</code>는 조합해서 사용 가능하며, 그 조합은 다음과 같이 동작한다:</p>
<pre><code class="language-dockerfile">ENTRYPOINT [&quot;/bin/bash&quot;]
CMD [&quot;entrypoint.sh&quot;]</code></pre>
<p>→ 실행 결과: <code>/bin/bash entrypoint.sh</code><br>→ 이 경우도 마찬가지로 <code>bash</code>가 PID 1이며 <code>entrypoint.sh</code>는 하위 프로세스로 동작한다.</p>
</li>
</ul>
<h4 id="도커-데몬-제어"><strong>도커 데몬 제어</strong></h4>
<ul>
<li><p>도커 데몬은 기본적으로 소켓을 통해 클라이언트와 통신한다. 따라서 아래의 두 명령어는 같은 의미이다.</p>
<pre><code class="language-bash">dockerd
dockerd -H unix:///var/run/docker.sock</code></pre>
</li>
<li><p>이처럼 IP와 포트를 통해서도 도커 데몬과 통신할 수 있다. 다만 unix 소켓을 병기하지 않으면 도커 CLI를 통한 명령어 실행이 불가능하다.</p>
<pre><code class="language-bash">dockerd -H tcp://0.0.0.0:2375 --tls=false</code></pre>
</li>
<li><p>도커 데몬 모니터링</p>
<ul>
<li>도커 데몬을 모니터링 하거나, 문제가 생겼을 때, 가장 간단한 방법은 <code>dockerd -D</code>로 디버깅 모드를 활성화하는 것이다.</li>
<li>그 외에도 <code>docker events</code>, <code>docker stats</code>, <code>docker system df</code> 등 다양한 명령어를 통해 도커 데몬을 모니터링 할 수 있다.</li>
</ul>
</li>
</ul>
<h3 id="적용해볼-내용"><strong>적용해볼 내용</strong></h3>
<ul>
<li>사용중인 도커 이미지에 <code>LABEL</code> 사용해서 관리 용이성 개선하기</li>
<li>멀티 스테이지 빌드로, 사내 활용중인 이미지의 설치 빌드 분리하여 이미지 크기 줄이기</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커/쿠버네티스 스터디 2일차]]></title>
            <link>https://velog.io/@hc-kang/%EB%8F%84%EC%BB%A4%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%8A%A4%ED%84%B0%EB%94%94-2%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@hc-kang/%EB%8F%84%EC%BB%A4%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%8A%A4%ED%84%B0%EB%94%94-2%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Tue, 29 Jul 2025 23:49:54 GMT</pubDate>
            <description><![CDATA[<h2 id="day-2-729---도커-이미지-관리">Day 2 (7/29) - 도커 이미지 관리</h2>
<p><strong>📖 진도</strong>: 2.2, 2.3 / 20 ~ 109p</p>
<h3 id="핵심-개념"><strong>핵심 개념</strong></h3>
<ul>
<li>도커 이미지와 컨테이너 개념</li>
<li>볼륨과 네트워크</li>
<li>로깅, 자원할당 옵션</li>
<li>이미지의 구조, 레이어 관리</li>
</ul>
<h3 id="실습-내용"><strong>실습 내용</strong></h3>
<ul>
<li>도커 컨테이너 다루기</li>
<li>볼륨, 네트워크의 개념</li>
<li>다양한 도커 컨테이너 명령어<ul>
<li><code>docker run -it ubuntu:24.04</code> 등 기본 커맨드</li>
<li><code>docker logs &lt;container_id&gt;</code> 등 로깅 명령어</li>
</ul>
</li>
<li>이미지의 구조, 레이어 관리</li>
</ul>
<h3 id="배운-내용"><strong>배운 내용</strong></h3>
<h4 id="몰랐던-유용한-커맨드"><strong>몰랐던 유용한 커맨드</strong></h4>
<ul>
<li><code>docker create -it ubuntu:24.04</code><ul>
<li>컨테이너 생성만 하고 실행은 안함</li>
</ul>
</li>
<li><code>cmd + P, Q</code><ul>
<li>컨테이너 끄지 않고 나가기</li>
</ul>
</li>
<li><code>docker attach &lt;container_id&gt;</code><ul>
<li>컨테이너 접속: 맨날 <code>exec -it &lt;container_id&gt; bash</code> 썼었는데.. 이게 있었네..</li>
<li><strong>다만 <code>Ctrl + C</code> 로 컨테이너를 종료하면 메인 프로세스가 종료될 수 있음.</strong></li>
<li>따라서 사실 <code>exec</code> 가 <strong>더 안전</strong>하다.</li>
<li><code>run</code> 과 <code>attach</code> 커맨드의 차이점<ul>
<li><img src="https://velog.velcdn.com/images/hc-kang/post/52434a06-5c57-414e-98fc-d8b256dc24a6/image.png" width="500">

</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="볼륨-컨테이너의-개념"><strong>볼륨 컨테이너의 개념</strong></h4>
<ul>
<li><p>볼륨만 전담하는 컨테이너 사용방식이 존재한다.</p>
<ul>
<li><p>그런데 왜? docker에서 관리하는 단순 볼륨 사용과 차이점은?</p>
<ul>
<li><p>사실 볼륨 컨테이너는 레거시 패턴이다.</p>
<ul>
<li><p>Docker 1.9 이후 <code>named volumes</code> 도입으로 대부분 불필요해졌다.</p>
</li>
<li><p>대체 사용법</p>
<pre><code class="language-bash">  # 이전 방식 (볼륨 컨테이너)
  docker create -v /data --name dbdata busybox
  docker run --volumes-from dbdata postgres

  # 현재 권장 방식
  docker volume create dbdata
  docker run -v dbdata:/var/lib/postgresql/data postgres</code></pre>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="macos에서-네트워크-환경"><strong>MacOS에서 네트워크 환경</strong></h4>
<ul>
<li>MacOS에서는 도커가 VM 환경에서 실행되기 때문에 네트워크 환경이 다르다.</li>
<li>도서에서는 호스트의 <code>veth0</code>을 통해 컨테이너와 브리지 네트워크를 연결한다고 하였으나, MacOS에서는 이보다 좀 더 복잡한 과정을 거친다.<ul>
<li>실제 구성<pre><code>macOS 호스트
    ↓
bridge102 (192.168.215.0/24) ← Docker VM이 사용하는 브릿지
    ↓
vmenet2 ← VM과 통신하는 가상 네트워크 인터페이스
    ↓
Docker Desktop VM (Linux)
    ↓
docker0 bridge
    ↓
veth pairs (여기에 실제로 존재!)
    ↓
컨테이너 (192.168.215.2)</code></pre></li>
</ul>
</li>
<li>도커의 브리지 네트워크<ul>
<li><code>subnet</code>, <code>gateway</code>, <code>ip-range</code> 등 세부 설정 가능<ul>
<li>&#39;공유기&#39;를 가상으로 만들 수 있다.</li>
</ul>
</li>
</ul>
</li>
<li><code>--net-alias</code> 옵션<ul>
<li>도커 내부 DNS 서버에 별칭을 추가로 부여할 수 있다.</li>
<li>RR(Round-Robin) 방식만 가능</li>
<li>그러나 헬스체크 등 주요 기능이 없으므로, 간단한 서비스에 사용하는 것이 좋다.</li>
<li>고급 기능 사용시에는 <code>haproxy</code>, <code>traefik</code> 등 외부 네트워크 프로그램을 사용하는 것이 좋다.</li>
</ul>
</li>
</ul>
<h4 id="도커의-로깅"><strong>도커의 로깅</strong></h4>
<pre><code class="language-bash">  docker run -it ubuntu:24.04
    --log-opt max-size=10m # 로그 파일 최대 크기
    --log-opt max-file=3 # 로그 파일 최대 개수
    --log-opt compress=true # 로그 파일 압축</code></pre>
<h4 id="컨테이너-자원-할당-제한"><strong>컨테이너 자원 할당 제한</strong></h4>
<pre><code class="language-bash">  docker run -d
    --memory=1g # 메모리 제한
    --memory-swap=2g # 메모리 + 스왑 제한
    --cpu-shares=1024 # 상대적 CPU 가중치, 1024이 최대
    --cpuset-cpus=0,1 # CPU 셋, 0번과 1번 코어만 사용
    --cpu-period=100000 # CPU 주기
    --cpu-quota=50000 # CPU 할당량, 주기 중 50% 사용
    --cpus=2 # CPU 제한, 2개 코어 사용</code></pre>
<ul>
<li><code>--cpus</code> 옵션이 간단하기는 하지만, <code>--cpuset-cpus</code> <strong>옵션이 더 유리</strong>하다.<ul>
<li>이는 CPU를 직접 지정할 수 있으므로 CPU 친화성이 높고, 불필요한 캐시미스, 컨텍스트 스위칭을 방지하여 성능상 이점이 있다.</li>
</ul>
</li>
</ul>
<h4 id="도커-이미지-관리"><strong>도커 이미지 관리</strong></h4>
<ul>
<li><code>docker save</code> 명령어로 이미지를 하나의 파일로 내보낼 수 있다.<ul>
<li>간단한 이미지를 바로 OCI 저장소에 올릴 수 있을듯.</li>
</ul>
</li>
<li>반대로 <code>docker load</code> 명령어로 파일을 이미지로 불러올 수 있다.</li>
<li><code>docker export</code>, <code>docker import</code> 명령어로 컨테이너 &lt;-&gt; 이미지로 변환할 수 있다.</li>
</ul>
<h3 id="적용해볼-내용"><strong>적용해볼 내용</strong></h3>
<ul>
<li><code>docker save</code> 명령어로, 사내에서 테스트에 사용할 골든 이미지 업로드 가능한지 적용해보기</li>
<li>홈 서버용 사설 레지스트리 구축 및 적용 해보기</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[토스 메이커스 컨퍼런스 25]]></title>
            <link>https://velog.io/@hc-kang/%ED%86%A0%EC%8A%A4-%EB%A9%94%EC%9D%B4%EC%BB%A4%EC%8A%A4-%EC%BB%A8%ED%8D%BC%EB%9F%B0%EC%8A%A4-25</link>
            <guid>https://velog.io/@hc-kang/%ED%86%A0%EC%8A%A4-%EB%A9%94%EC%9D%B4%EC%BB%A4%EC%8A%A4-%EC%BB%A8%ED%8D%BC%EB%9F%B0%EC%8A%A4-25</guid>
            <pubDate>Mon, 28 Jul 2025 00:14:12 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hc-kang/post/b89a5b42-c2f5-48d4-8514-9643793da769/image.png" alt=""></p>
<h2 id="20년-레거시를-넘어-미래를-준비하는-시스템-만들기">20년 레거시를 넘어 미래를 준비하는 시스템 만들기</h2>
<p><strong>하태호</strong> | 토스페이먼츠 Head of Technology</p>
<blockquote>
<p>결제 산업 혁신을 목표로 출범한 토스페이먼츠는, 20년 넘게 운영된 레거시 시스템을 인수한 뒤 복잡한 구조를 개선하며 안정성과 확장성을 동시에 확보해왔습니다.<br>이번 세션에서는 국내 결제 시장의 패러다임을 바꾸기 위한 기술적 진화 과정과, 고객·가맹점 경험을 높이기 위한 새로운 결제 제품 개발 사례를 공유합니다.</p>
</blockquote>
<ul>
<li>처음에 <code>git</code>도 없었다고 함.</li>
<li>최신 코드 찾아서 <code>git</code> 등록하는 과정은 굉장히 힘들다. (근데 이건 대부분의 현대 개발자들에게 상관 없을듯)</li>
<li>온프레미스에서 AWS로 마이그레이션<ul>
<li>물리 장비의 상태에 의존하는 시스템에서 탈피하자.</li>
<li>MSA 전환 및 Docker/K8S 도입으로 고가용성 확보</li>
</ul>
</li>
<li><code>Java</code>도 최신 LTS 항상 따라가고 있음.</li>
<li>모든 로컬, 빌드머신 및 배포 환경 맞추기<ul>
<li>모두 동일한 환경을 갖추는 것의 중요성</li>
</ul>
</li>
<li>골든 이미지 작성 및 활용 하는듯<ul>
<li>골든 이미지란? 표준화되고 최적화된 상태의 시스템 이미지.</li>
<li>우리 회사도 적용했으면 좋겠다.</li>
</ul>
</li>
<li>전사 내에 추상화된 <code>DevOps</code> 파이프라인 구축: 모든 부서가 동일하게, 커밋 해시를 통해 배포 할수 있음<ul>
<li>팀 이동, 다른 업무시에도 적응 시간 최소화: 효율적인 업무 환경 조성</li>
</ul>
</li>
<li>표준화와 자동화의 중요성</li>
<li>1% 단위로 네트워크 흐름을 조정 할 수 있음<ul>
<li>카나리 배포 등 안정적이고 신뢰할 수 있는 배포 가능</li>
</ul>
</li>
</ul>
<h2 id="현장-결제-서비스의-분산-트랜잭션-관리학-개론">현장 결제 서비스의 분산 트랜잭션 관리학 개론</h2>
<p><strong>김학현</strong> | 토스 현장결제 서비스</p>
<blockquote>
<p>토스 현장결제 서비스에서 어떻게 분산 트랜잭션을 관리하는지 소개헤요.<br>결제 도메인에 국한되지 않고, 다양한 상황에서도 적용 가능한 보편적이고 직관적인 접근 방식을 중심으로 실전 사례를 공유합니다.</p>
</blockquote>
<ul>
<li><p>&quot;개론&quot;에 포인트</p>
</li>
<li><p>가맹점 -&gt; 결제서버 -&gt; 송금, 포인트, 프로모션 서버로 트랜잭션</p>
</li>
<li><p>API 호출 결과를 단정 할 수 없는 경우가 있음</p>
<ul>
<li><code>Unknown</code> 상태(알 수 없는 경우) 예시<ul>
<li><code>Read Timout</code></li>
<li>네트워크 유실</li>
<li>정의되지 않은 에러</li>
</ul>
</li>
<li>즉, 실행 결과에는 당연히 &#39;모름&#39;도 포함되어야 함.<ul>
<li>그래야 <code>MECE</code>(Mutually Exclusive, Collectively Exhaustive) 하게 모든 경우를 표현 할 수 있음.</li>
<li>이 <code>Unknown</code> 상태는 또다시 성공, 실패로 나뉘어짐.</li>
</ul>
</li>
</ul>
</li>
<li><p><code>Task1</code>이 각각 또다른 2차 서브태스크를 가진 <code>SubTask1</code>, <code>SubTask2</code>로 나뉜 경우.</p>
<ul>
<li>이 때, <code>SubTask1</code>과 <code>SubTask2</code>는 독립적으로 실행되며, 결과를 반환함.</li>
</ul>
</li>
<li><p>예시) 토스결제 등..</p>
</li>
<li><p>문제 해결</p>
<ul>
<li><p>복잡한 상황에 대한 표현 모델</p>
<ul>
<li><p>트리 구조를 통해 복잡한 상황도 표현 가능, 유연한 확장도 가능</p>
<ul>
<li>리프 노드에 상태를 표현 할 수 있음!</li>
<li>이를 각각 상위로 집계하며 표현해 나갈 수 있음.<ul>
<li>자식이 하나라면 그대로 승계</li>
<li>둘 이상이라면 자식들의 상태에 따라 상태 결정</li>
</ul>
</li>
<li>이를 통해 전체 상태에 대한 관심사를 오직 자식 노드로 한정 할 수 있음.<ul>
<li>하위 상태가 <code>Unknown</code> -&gt; <code>Success</code> 로 전이 시 상태를 상위로 리졸브 할 수 있음.<ul>
<li>다만 이것만으로는 문제를 해결할수 없음</li>
</ul>
</li>
<li>방법 1. 실패한 상태를 재시도하여 성공으로 맞춘다</li>
<li>방법 2. 성공한 상태를 롤백하여 실패시킨다.</li>
</ul>
</li>
</ul>
</li>
<li><p>실제 구현 예시</p>
<pre><code>// 트리 노드 구조
Task(결제)
├── Transaction(송금서버): Success
├── Transaction(포인트서버): Unknown → Success/Failure
└── Transaction(프로모션서버): Failure

// 상태 집계 로직
- 모든 자식이 Success → 부모도 Success
- 하나라도 Failure → 부모는 Unknown (보정 대기)
- Unknown 포함 시 → 부모도 Unknown</code></pre></li>
</ul>
</li>
<li><p>일관성을 누가!</p>
<ul>
<li>이처럼 방법을 선택할 책임을 누가? 각 노드가?<ul>
<li>각 노드는 단순 보상이 아닌, 재시도와 보상이라는 두 가지 전략을 가짐 - 기존 <code>Saga Orchestrator</code>와 다름.</li>
</ul>
</li>
</ul>
</li>
<li><p>언제까지 보장할지</p>
<ul>
<li>ASAP 하지만 결국은 &#39;최종적 일관성&#39;</li>
<li>Unknown 상태 처리 방식<ul>
<li>타임아웃 정책: 각 서비스별로 다른 타임아웃 설정</li>
<li>재시도 정책: exponential backoff로 부하 분산</li>
<li>최종 전이: Unknown → Success/Failure 확정 후 상위 노드에 전파</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p>실제 구현</p>
<ul>
<li>노드의 레벨별로 테이블을 나눈다??</li>
<li>분단위 배치를 통해 정합성 보정?<ul>
<li>하지만 부족해서 API 내에서 자체 보정도 시행</li>
</ul>
</li>
</ul>
</li>
<li><p>정책</p>
<ul>
<li>정책 수립 시 고려<ul>
<li>결제수단별 심리적 가치차이(현금, 카드, 등)</li>
<li>보상 트랜잭션의 난이도 (입금 &lt; 출금)</li>
<li>결제수단별 처리 난이도(내부 &lt; 외부)</li>
</ul>
</li>
<li>결제수단 승인/환불순서</li>
<li>각 결제수단의 필수 성공여부</li>
<li><code>Unknown</code> 상태의 전파 바운더리 설정<ul>
<li>정책의 위임</li>
</ul>
</li>
<li>금원이동 서비스와 결제서비스의 분리</li>
<li>API 트랜잭션 내에서의 최소한의 보상 트랜잭션 시행</li>
</ul>
</li>
<li><p>트랜잭션은 사실 원자적 요소가 아님</p>
<ul>
<li>결국 각각은 내부적으로 묶이는 또다른 트랜잭션이 있음. 사실상 이미 트리구조를 형성하고 있음.</li>
</ul>
</li>
</ul>
<h2 id="수천-개의-apibatch-서버를-하나의-설정-체계로-관리하기">수천 개의 API/BATCH 서버를 하나의 설정 체계로 관리하기</h2>
<p><strong>나재은</strong> | 토스페이먼츠 Server Platform Team Leader</p>
<blockquote>
<p>서버 설정도 소프트웨어일까요? 개발자의 인프라 접근성을 높이면서도 설정 실수를 줄여야 하는 미션은 얼핏 모순처럼 느껴질 수 있어요.<br>단순한 복사·붙여넣기에서 시작해, 수천 대의 서버를 한 번의 클릭으로 바꾸기까지.<br>이번 세션에서는 플랫폼 엔지니어링의 핵심인 설정 체계의 진화 과정을 소개하고자 해요.</p>
</blockquote>
<ul>
<li><p>일종의 <code>setting.json</code> 같은걸까? =&gt; 맞음.</p>
</li>
<li><p>수백, 수천개의 배치 중 잘못된(버그) 정산 배치를 찾아야 함.</p>
<ul>
<li>일을 하다보면, 실수와 오타는 필연적</li>
<li>대부분 비슷한 설정이지만 &#39;조금씩만&#39; 다르다면?: <strong>버그의 온상</strong></li>
<li>코드는 버그를 만들지만, 인프라 설정 실수는 <strong>장애로</strong> 이어짐</li>
</ul>
</li>
<li><p><strong>중복 없이</strong>, <strong>테스트 가능</strong>한 설정 체계가 필요함.</p>
</li>
<li><p>API 설정에 가해지는 외부 압력</p>
<ul>
<li>개발자의 요구사항</li>
<li>모든 서버에 공통 변수 넣기</li>
<li>특정 서버 그룹에만 힙 메모리 늘리기</li>
<li>특정 빌드에서만 특정 테스트 실행하기</li>
<li>해결을 위한 두가지 축<ul>
<li>오버레이 아키텍처<ul>
<li>조합 가능한 계층형 레이어 설정 구조<ul>
<li>Global, CLuster, Phase, App-type, APp-group, App...</li>
</ul>
</li>
<li>설정 파일을 여러 파일의 디렉토리 구조로 구성함</li>
<li>하지만 특정 환경변수의 중복이 생기는 경우가 있음<ul>
<li>kv yaml의 한계. 리스트형 표현에 한계가 있음.</li>
<li>예시: DB_URL이 여러 레이어에서 정의되어 충돌</li>
</ul>
</li>
</ul>
</li>
<li>템플릿 패턴<ul>
<li><code>{{ }}</code> 와 같은, 동적으로 삽입 가능한 템플릿으로 활용<ul>
<li>그러면 이걸 누가넣어주지? 실제 사용 직전에 후처리를 위한 프로세서가 관리되어야 할텐데</li>
</ul>
</li>
<li>실제 템플릿 예시:<pre><code class="language-yaml">server:
  port: {{ SERVER_PORT | default(8080) }}
  heap: {{ if eq .ENV &quot;prod&quot; }}4g{{ else }}2g{{ end }}
database:
  url: jdbc:mysql://{{ DB_HOST }}:{{ DB_PORT }}/{{ DB_NAME }}</code></pre>
</li>
</ul>
</li>
<li>오버레이를 빌드한 뒤, 마지막 완성된 설정의 템플릿을 채우기?</li>
<li>설정에 코드를 넣기?<ul>
<li>이건 제어 못하면 안티패턴이 될듯</li>
</ul>
</li>
<li>조건부 설정 기입<ul>
<li>이것도 좀.. 나중에 복잡도가 폭주할 것 같은데..</li>
</ul>
</li>
</ul>
</li>
<li>결국은 계속 변화, 진화 해야함<ul>
<li>복붙 =&gt; 오버레이 아키텍처 =&gt; 템플릿 패턴 =&gt; ???<ul>
<li>중복제거, 실수 방지 / 필요한 값 동적 기입</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p>배치 설정</p>
<ul>
<li>토스페이먼츠, 한달에 수 조원 정산</li>
<li>일, 월단위 대사 작업</li>
<li>KISS: 단순하게 가자.</li>
<li>Jenkins 사용: 단순하고 강력함. 그동안 증명된 신뢰성</li>
<li>해결해야 하는 문제<ul>
<li>수백개의 젠킨스 java 경로 업데이트</li>
<li>수백개 잡의 환경변수 일괄 수정..</li>
</ul>
</li>
<li>JOB DSL 플러그인 어댑터?<ul>
<li>플러그인을 페이먼츠에서 자체 구현?<ul>
<li>개발자에게 정말 필요한 부분만 노출토록 래핑</li>
<li>빌더 패턴으로 Job Building을 할 수 있음<pre><code class="language-groovy">job(&#39;payment-batch&#39;) {
    schedule(&#39;0 2 * * *&#39;)
    memory(4096)
    environment {
        add(&#39;SPRING_PROFILES_ACTIVE&#39;, &#39;production&#39;)
        add(&#39;BATCH_TYPE&#39;, &#39;daily-settlement&#39;)
    }
    steps {
        shell(&#39;java -jar payment-batch.jar&#39;)
    }
}</code></pre>
<ul>
<li>근데, 그러면 어디까지 열어줄건지?<ul>
<li>&#39;이런 기능 없나요?&#39;</li>
</ul>
</li>
<li>그만큼 <code>Jenkins와</code> <code>JobDSL</code>이 성숙해서 대부분의 기능이 지원 가능한걸까?</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>Dynamic Provisioning 인프라<ul>
<li>배치의 메모리 부족문제 해결</li>
<li>노드 하나에 프로세스 몇개가 적절할까.<ul>
<li>전용 노드 활용</li>
</ul>
</li>
</ul>
</li>
<li>진화과정<ul>
<li>복붙 =&gt; Job DSL =&gt; Dynamic Provisioning =&gt; ???</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="100번-실패하고-살려-낸-문서-시스템">100번 실패하고 살려 낸 문서 시스템</h2>
<blockquote>
<p>⚠️ 다듬어진 글은 <a href="https://velog.io/@hc-kang/%ED%86%A0%EC%8A%A4%EC%9D%98-%EA%B8%B0%EA%B0%80%EB%A7%89%ED%9E%8C-%EB%AC%B8%EC%84%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%8B%A4%ED%8C%A8%EC%97%90%EC%84%9C-%EB%B0%B0%EC%9A%B4-%EA%B5%90%ED%9B%88%EB%93%A4">이 링크</a>를 참고해 주세요!</p>
</blockquote>
<p><strong>박서진, 한주연</strong> | 토스 Head of Frontend / Technical Writer</p>
<blockquote>
<p>믿을 만한 문서 하나 없어서 옆 사람에게 모르는 것을 물어보던 토스 프론트엔드.<br>1년 사이 3천개 넘는 고품질 문서가 생기고, 하루 천 번 넘게 문서를 읽고 있어요.<br>하지만, 문서만 쓰는 것은 답이 아니었어요. 왜 문서를 쓰기만 해서는 안 되는지, 대규모 문서 시스템을 운영하는 데 중요한 것은 무엇인지, 토스가 99번 실패하고 고민하며 얻어온 노하우를 공유해요.</p>
</blockquote>
<ul>
<li><p>Notion, Logseq, Confluence...</p>
<ul>
<li>이런 여러 도구를 사용했으나.. 잘 되지 않았음.<ul>
<li>계속되는 질문<ul>
<li><code>이 문서 있나요</code></li>
<li><code>이거 최신 맞나요</code></li>
<li><code>이거 더 잘 아시는분</code></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p>현재는 &#39;박씨&#39; 봇</p>
<ul>
<li>문서를 기반으로 답변</li>
<li>참조된 문서 링크</li>
<li>외부 데이터를 학습도 바로 시킬 수 있음</li>
</ul>
</li>
<li><p>문서화 길드? 라는게 있다는 듯: 개발자들이 능동적으로 참여하는 문서화 그룹(?)</p>
</li>
<li><p>프론트, 온보딩 각 파트별 문서 플랫폼 관리중 - 약 2,000개 이상</p>
</li>
<li><p>테크니컬 라이팅 가이드 존재</p>
</li>
<li><p>오래되거나 잘못된 문서는 바로 알림을 보낼 수 있는 기능이 통합되어있음.</p>
<ul>
<li>메트릭, 피드백 평가 등을 함께 노출: 직관적인 피드백</li>
</ul>
</li>
<li><p>문서.. 좋은건 알겠는데, 왜 그렇게까지?</p>
<ul>
<li>신규입사자 온보딩<ul>
<li>이걸 누구한테 물어봐야?</li>
<li>이건 어떻게 동작하지?</li>
<li>이거 왜 이렇게 짰지? 등등</li>
</ul>
</li>
<li>누군가는 분명 알지만, 이게 암시적으로만 있음. =&gt; <strong>이걸 명시적으로 끄집어 내자.</strong><ul>
<li>힘들여서 아는사람부터 찾고, 이것도 단발성으로 동작</li>
<li>휴가, 퇴사 시 정보의 부재</li>
</ul>
</li>
<li>게다가 규모가 커지면서 이게 길을 잃게됨</li>
<li>슬랙은 공지가 되고, 서로 아는사람도 잘 없고, 질문하기도 좀 눈치보임.</li>
</ul>
</li>
<li><p>좋은 문서 시스템의 두가지 조건</p>
<ul>
<li><strong>신뢰할 수 있는 정보</strong><ul>
<li>낡지 않은, 최신의</li>
<li>틀리지 않고 정확한</li>
<li>스스로 완결되어있는</li>
<li>필요한 것은 대부분 있는: 그래도 원하는것의 85% 정도는 있어야 나머지를 내가 채우지</li>
</ul>
</li>
<li><strong>누구나 접근 가능해야한다.</strong><ul>
<li>검색하기 쉽다.</li>
<li>익숙하고 맥락을 이어갈수 있어야한다.<ul>
<li>어려운 툴이나, 다른 매체로 변경(context switching 최소화)</li>
</ul>
</li>
</ul>
</li>
<li>즉, 신뢰성과 접근성<ul>
<li>&#39;박씨&#39; 에서 느낀점: 접근성이 생각보다 중요하다.</li>
</ul>
</li>
<li>문서의 라이프사이클: 중요하다.<ul>
<li>문서 작성</li>
<li>읽기 =&gt; 중점. 읽어야 뭐라도 한다.</li>
<li>피드백</li>
<li>더많은 문서</li>
</ul>
</li>
<li>당연히 검색이 되어야 한다.<ul>
<li>수많은 주제, 수많은 형태의 문서들이 있는데, 이게 보통 개별로 검색된다</li>
<li>&#39;통합 검색&#39;의 중요성!</li>
<li><strong>알골리아? SaaS</strong><ul>
<li>통합 인덱스 활용</li>
</ul>
</li>
</ul>
</li>
<li>워크플로우에 통합되어야 한다.<ul>
<li>이게 바로 슬랙 봇 박씨?</li>
</ul>
</li>
</ul>
</li>
<li><p>근데 <strong>검색만으로도 부족</strong>, 접근성에 대한 갈증</p>
<ul>
<li>옆사람에 질문이 더 편함</li>
<li>메신저로 물어보는게 더편함</li>
<li>IDE 밖으로 나가고싶지 않음</li>
<li>이게 결국은 &#39;물어보기&#39;의 흐름을 유지해야 함.</li>
</ul>
</li>
<li><p>그리고 박씨라는 봇이 생기니, 사람이 아니므로 질문의 허들이 매우 낮아짐.</p>
</li>
<li><p>게다가 박씨가 몰라도 다른 사용자가 대답해줌.</p>
</li>
<li><p>통합된 문서에 대한 MCP를 활용하여 문서 참조</p>
<ul>
<li>MCP(Model Context Protocol) 활용 방식:<pre><code>// MCP를 통한 문서 시스템 연동
- IDE에서 직접 문서 검색/참조 가능
- 코드 작성 중 관련 문서 자동 추천
- 문서 업데이트 시 실시간 반영</code></pre></li>
</ul>
</li>
<li><p>다시 신뢰성 이야기</p>
<ul>
<li>시스템 만들기: Docflow</li>
<li>문서의 종류부터 파악<ul>
<li>레퍼런스 문서: 특정 소스코드에 대한 설명 문서. 사전과 같은 속성 <strong>Docflow</strong>, JSDoc을 사용한 문서 자동화<ul>
<li>코드를 바탕으로 문서 자동화<pre><code class="language-javascript">/**
 * @description 결제 요청을 처리하는 함수
 * @param {PaymentRequest} request - 결제 요청 정보
 * @returns {PaymentResponse} 결제 처리 결과
 * @example
 * const result = await processPayment({
 *   amount: 10000,
 *   method: &#39;CARD&#39;
 * });
 */
function processPayment(request) { ... }</code></pre>
</li>
<li>CI에 통합하여, 문서가 없으면 통합이 안되게!</li>
<li>신선하지 않은 문서에 대해서는 관리자에게 알림</li>
</ul>
</li>
<li>가이드 문서: 튜토리얼, 줄글 및 설명</li>
</ul>
</li>
<li>문서가 &#39;정확한&#39; 정보를 담게 하기 위한 시스템 구축<ul>
<li>문서 생성</li>
<li>개발자 리뷰(수석 개발자)<ul>
<li>기술 리뷰</li>
</ul>
</li>
<li><strong>Technical Writer 리뷰</strong><ul>
<li>읽기 좋은 문서</li>
<li>이해하기 좋은 문서</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p>왜 이렇게까지 하나?</p>
<ul>
<li>고품질 문서 비율을 <code>80%</code> 이상으로 유지해야 한다.</li>
</ul>
</li>
<li><p>필요하고, 쓸모있고 유용한 문서가 많아지게 하는 방법?</p>
<ul>
<li>문서를 만들기 위한 리더들의 마중물<ul>
<li>분위기 만들기: 우선 열심히 초안 들이붓기<ul>
<li>일종의 가이드라인</li>
</ul>
</li>
</ul>
</li>
<li>정복단? 스터디를 통해 획득한 산출물 문서화</li>
<li>문서화 세션: 글 쓰는 방법에 대한 세션</li>
</ul>
</li>
<li><p>박씨 학습시키기</p>
<ul>
<li>근데 그래서 이건 누가 만들어..? 일단 박씨가 있어야...</li>
</ul>
</li>
<li><p>중간정리</p>
<ul>
<li>좋은 문서 시스템<ul>
<li>신뢰 할 수 있는 문서<ul>
<li>Docflow</li>
<li>문서 리뷰 시스템</li>
<li>정복자, 문서화 세션</li>
</ul>
</li>
<li>누구나 접근 할 수 있는 문서<ul>
<li>통합검색</li>
<li>익숙한 플랫폼</li>
<li>맥락전환 최소화(MCP등)</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p>수평확장</p>
</li>
<li><p>Technical Writer must die??</p>
<ul>
<li>2000개 이상의 문서 관리.. 자동화</li>
</ul>
</li>
<li><p>문서는 조직의 인프라다.</p>
</li>
<li><p>좋은, 양질의 정보를 누구나 쉽게 접근 할 수 있게 해야한다.</p>
</li>
<li><p>이게 팀이 성장하는 동력</p>
</li>
</ul>
<h2 id="당신은-이미-팀을-최적화하고있다">당신은 이미 팀을 최적화하고있다</h2>
<p><strong>이재연, 유희열</strong> | Server Developer / L&amp;D Coach</p>
<blockquote>
<p>코드를 분석해 문제를 발견하고 가장 효과적인 수정 방법을 통해 최적화하는 것처럼 팀과 팀원이 일하는 방식에서도 효과적인 해결 방법을 어떻게 만들 수 있는지 공유해요.<br>개발을 직접 하지 않더라도 팀의 성장을 이끄는 방법. 그 감을 익힐 수 있도록 실전 사례들을 소개드려요.</p>
</blockquote>
<ul>
<li><p>위임, 분배, 책임감</p>
<ul>
<li>위임, 기대와 현실의 거리, 책임감 과잉<ul>
<li>위임<ul>
<li>내가 하고말지.. 하는거 있지.</li>
<li>주변인의 도움이 필요할테니 위임을 해라<ul>
<li>자신이 하던 일을 그냥 쪼개서 주는게 위임이 아님.</li>
<li>주고 잊을 수 있어야 진짜 위임. 근데 그게 어려움.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p>믿음을 쌓아가는 방법</p>
<ul>
<li>팀원의 프로젝트 싱크를 맞출 수 있어야 함<ul>
<li>계획을 구체적으로 세우되... ?????</li>
</ul>
</li>
<li>전체 볼륨을 먼저 합의하자<ul>
<li>이걸 합의 안하면 대체로 결국 충돌이 날 수 밖에 없음.</li>
<li>당일, 혹은 하루, 혹은 중간중간 서로 확인하는 것도 필요: 부담되지 않게 요청하기?<ul>
<li>이것도 그냥 수시로 물어보는것도 스트레스 요인임. <ul>
<li>이런 작은것도 <strong>퍼미션</strong>을 하나씩 쌓아가자.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>의견이 다른 부분에 대해 맞춰가는 과정</li>
<li>신뢰의 3요소(배반의 3요소...)<ul>
<li>역량 신뢰: 이사람이 이 일을 잘 해낼거라는 믿음<ul>
<li>개발자의 흔한 실수: <strong>남들도 나만큼 할 수 있을거라는 착각</strong></li>
</ul>
</li>
<li>소통 신뢰: 이 사람이 어떤 상황에서도 진심으로, 솔직하게, 맥락있게 소통할거라는 믿음</li>
<li>계약 신뢰: 약속할 것을 지킬 사람이라는 믿음</li>
</ul>
</li>
<li>개발능력 외에도 위의 요소를 갖춰야 할 듯.</li>
<li>자율성을 존중하면서 퍼미션 얻기??<ul>
<li>팀원의 자율성이 존중되고 있는지</li>
<li>팀원이 스스로 뭔가를 결정할수 있는지</li>
<li>일정도 지켜야하는데?</li>
<li>&#39;퍼미션&#39;이 뭐지?<ul>
<li>무턱대는 조언 말고, &#39;<strong>의견을 말해줘서 좋고, 그러면 내가 의견을 말해도 될까</strong>&#39; 라는 허락을 통해 청자의 듣기 효율을 매우 높일 수 있다.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Managing Up</strong></p>
<ul>
<li>변화, 한 사람, 버드뷰, 알아주기</li>
<li>리더십 코칭 경험<ul>
<li>잘 변하는, 혹은 변하지 않는 조직</li>
<li>코칭 대상자의 작은 변화를 읽어 줄 수 있는 매니저가 있어야 함.<ul>
<li>회사의 방향과의 정렬이 잘 되어있는지 확인해 줄 사람이 필요함 - 유형석 CTO?<ul>
<li>결국은 상하동욕자승이라고 했지.</li>
</ul>
</li>
</ul>
</li>
<li>&#39;나를 알아봐주는 리더&#39;를 만나는 것도 복이지</li>
</ul>
</li>
</ul>
</li>
<li><p>사람을 &#39;대체 불가능한 인력&#39;, &#39;대체 가능한 인력&#39; 으로 나누는 습관.</p>
<ul>
<li>대체 불가능한 사람들의 풀을 늘리자</li>
</ul>
</li>
<li><p>하위 팀 멤버의 성과를 끌어내고, 보일 수 있게 하는 방법</p>
<ul>
<li>이력서를 실제로 같이 써보는 경험도 좋다?<ul>
<li>회사 사람, 더군다가 상급자와 이력서를 같이 써보는 경험이라..</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="확장성과-회복-탄력성을-갖춘-결제-시스템-만들기">확장성과 회복 탄력성을 갖춘 결제 시스템 만들기</h2>
<p><strong>박순현, 양권성</strong> | 토스페이먼츠 Server Developer</p>
<blockquote>
<p>토스페이먼츠의 20년 된 레거시 시스템, 어떻게 개선했을까? 코드 뿐 아니라 DB까지 노후화된 레거시 시스템은 강결합된 구조로 인해 유지보수는 물론, 비즈니스 측면에서도 수많은 제약을 만들어냈어요.<br>이번 세션에서는 이런 레거시 시스템을 어떻게 신규 DB로 이관하고, 점진적으로 구조를 개선해 나갔는지 그 과정에서 마주한 이슈들과 해결 경험을 공유하고자 해요.</p>
</blockquote>
<ul>
<li><p>원장</p>
<ul>
<li>기업의 모든 거래내역을 기록하는 공식 장부</li>
</ul>
</li>
<li><p>결제 원장</p>
<ul>
<li>고객이 결제하거나 취소한 내역 저장</li>
</ul>
</li>
<li><p>레거시 원장 시스템의 문제점</p>
<ul>
<li>일관성 없는 구조<ul>
<li>카드 취소/부분취소는 분리된 테이블</li>
<li>Refund라는 이름 테이블 이름이지만, 계좌이체 부분취소</li>
</ul>
</li>
<li>다양한 도메인 사이의 강한 의존<ul>
<li>동일한 용어도 팀별로 사용 용어가 다름. 혹은 중복</li>
</ul>
</li>
<li>비즈니스 확장을 막는 구조<ul>
<li>1:1 구조로, 분할 결제 등을 구현 할 수 없음.</li>
</ul>
</li>
</ul>
</li>
<li><p>Oracle =&gt; MySQL 로 전환</p>
<ul>
<li>시스템 안정성: 결제팀 전용의 DB 인프라</li>
<li>유지보수<ul>
<li>오라클 자체 함수, 디버깅 힘든 환경 등</li>
</ul>
</li>
<li>유연함과 확장성, 실험환경 구성<ul>
<li>오픈소스 기반, 다양한 도구와 쉽게 연동 가능</li>
</ul>
</li>
</ul>
</li>
<li><p>해결</p>
<ul>
<li>일관된 구조 잡기<ul>
<li>승인과 취소를 각각의 테이블로 쌓음. 기존 row 조작이 아니라 무조건 insert. 이러면 락도 필요없어짐</li>
</ul>
</li>
<li>도메인 과 결합도 낮추기<ul>
<li>카프카를 활용한 결합도 분리</li>
<li>근데 이건 좀 다른 이야기같은데? 용어 문제라며???</li>
</ul>
</li>
<li>확장성<ul>
<li>결제와 승인의 개념적 분리</li>
<li>더치페이, 복합결제 등 요소를 구현 할 수 있음.</li>
</ul>
</li>
</ul>
</li>
<li><p>신규 원장으로 전환시 동기/비동기 동시 처리: 섀도우?</p>
</li>
<li><p>비동기 처리를 위한 스레드풀 설정</p>
<ul>
<li>일부 데이터 버려도 되도록</li>
</ul>
</li>
<li><p>Graceful shutdown</p>
</li>
<li><p>레거시, 신규 결제원장 사이의 검증배치.</p>
<ul>
<li>매시 정각이 아닌 5분 텀</li>
</ul>
</li>
<li><p>신규원장 전환시 점진적 트래픽 투입</p>
<ul>
<li>트래픽 관리, 버그 확인도 있지만 스펙도 확인해야 함.</li>
</ul>
</li>
<li><p>마이그레이션 작업을 라이브에서 하면 문제가 생길 수 있음.</p>
<ul>
<li>마이그레이션용 서버를 분리해서 작업</li>
</ul>
</li>
<li><p>초기에는 하나씩 기존 로직으로 insert를 하려했으나... 느리고 트래픽 터짐</p>
</li>
<li><p>결국 배치 인서트</p>
</li>
<li><p>로컬 캐시 사용?? 어디다가?? 왜??</p>
</li>
<li><p>마이그레이션 시 네트워크 대역폭 문제가 있음</p>
<ul>
<li>다른 서비스가 눌려버릴 수 있음..</li>
</ul>
</li>
<li><p>그래서 회복력과 탄력성은?</p>
<ul>
<li>결제서버 장애 문제<ul>
<li>결제 DB에 부하가 배포 이후 확 침</li>
<li>특정 Select 쿼리가 문제<ul>
<li>전날 배포된 신규 로직. 롤백하여 우선 장애 해소</li>
<li>단순한 셀렉트인데 왜?<ul>
<li>적재량이 많아짐에 따라, 옵티마이저가 인덱스를 활용을 안했다?? 왜??<ul>
<li>쿼리 힌트 추가하여 해결</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>DB 적재 실패하여 신, 구 원장 불일치<ul>
<li>아직 메인이 구 원장이므로 배치로 동기화 </li>
</ul>
</li>
<li>원천사 원장 불일치<ul>
<li>타임아웃으로 실패 판단한 거래에 대해서는 원천사에 결제취소를 자동으로 요청</li>
</ul>
</li>
<li>원장 가맹점 불일치<ul>
<li>각 홉 간에 타임아웃이 따라야 할 규칙을 세우고, 이를 조사한 뒤 새로 적용해야 함.</li>
</ul>
</li>
<li>이벤트 발행도 누락됨<ul>
<li>아웃박스 패턴</li>
<li>아웃박스에도 저장이 안된 이벤트??</li>
<li>이벤트 재발생시 중복 가능성<ul>
<li>헤더에 멱등키 활용</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="레거시-정산-개편기-신규-시스템-투입-여정부터-대규모-배치-운영-노하우까지">레거시 정산 개편기: 신규 시스템 투입 여정부터 대규모 배치 운영 노하우까지</h2>
<blockquote>
<p>20년간 운영되어 온 토스페이먼츠의 레거시 정산 시스템에 ‘대대적인 리모델링’을 감행한 여정을 소개해요. 고고학 발굴처럼 복잡한 쿼리 속에서 핵심 로직을 명쾌한 코드로 재구성하고, 수억 건의 데이터를 빠르게 처리하기 위한 최적화 과정을 수차례의 검증을 반복했어요. 1,700개가 넘는 배치를 운영하며 얻은 실전 경험까지 빠짐없이 공유합니다.</p>
</blockquote>
<ul>
<li><p>정산이란?</p>
<ul>
<li>결국 돈이 흐르게 하는것</li>
</ul>
</li>
<li><p>왜 개편하는가?</p>
<ul>
<li>세 가지 한계점<ul>
<li><ol>
<li>비즈니스 로직이 쿼리에 종속적임</li>
</ol>
<ul>
<li>수수료 계산 쿼리, 조인, 서브쿼리, 유니언.... 읽기 힘듦.</li>
<li>작은 수정시에도 쿼리를 하나 하나 모두 열어봐야 함.</li>
<li>이 한방쿼리를 작고 명확한 단위가 될때까지 분해</li>
<li>이를 통해, 전체 일괄 변경이 아니라, 분해된 작은 기능단위로 새로운 기능 도입 가능</li>
<li>이런 작업의 비즈니스 로직을 하나하나 객체로 분리하여 코드 재사용성 높임</li>
</ul>
</li>
<li><ol start="2">
<li>데이터 모델링 한계</li>
</ol>
<ul>
<li>각 거래의 결과를 개별적으로 저장하는게 아니라 집계 후 저장하고있었음.<ul>
<li>원인 추적도 힘들고, 가시성 떨어짐</li>
</ul>
</li>
<li>이미 특정 단위로 집계되어, 다른 요구사항을 위한 데이터 제공도 어려움.</li>
<li>데이터를 집계가 아닌, 독립적인 단위로 저장<ul>
<li>원인 추적이 용이</li>
<li>최소 단위의 데이터에 대한 재사용성 확보</li>
<li>계산 당시 파라미터를 모두 스냅샷 저장</li>
<li>실패건에 대한 지엽적인 조치 가능</li>
<li>데이터가 고해상도가 되면서, 사이즈가 매우 커짐</li>
</ul>
</li>
<li>날짜 기반 레인지 파티셔닝, 인덱스</li>
<li>조회 전용 테이블 설계<ul>
<li>개별 데이터 조회시, 기존 인터페이스를 맞추려면 매번 집계해야 함.</li>
<li>따라서 조회용 집계 테이블을 별도로 설계</li>
</ul>
</li>
</ul>
</li>
<li><ol start="3">
<li>배치 시스템 성능이슈 </li>
</ol>
<ul>
<li>거래량 증가할수록 처리 실행시간이 매우 길어짐.. 배치간격보다 길어질지도</li>
<li>스프링 배치를 사용해도, 여전히 IO 많음</li>
<li>싱글 스레드 기반이라 처리량 한계있음</li>
<li>IO 처리횟수를 매번 DB 조회 =&gt; 인메모리 캐시 활용</li>
<li>itemProcessor가 하나씩만 입력받음(IO 많음) =&gt; Wrapper 클래스 사용하여 묶음으로 입력</li>
<li>insert도 하나씩 하던것 =&gt; 배치 인서트로</li>
<li>다수 API 호출 필요시 순차처리 =&gt; 병렬처리</li>
<li>배치처리시 스레드 안전 지키도록</li>
<li>배치 병렬처리 시 ID 모듈러 연산 사용</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p>그래서 개발 이후 마이그레이션은?</p>
</li>
<li><p>어떻게 새 시스템이 기존 시스템과 동일한 처리를 한다고 확신 할 수 있지?</p>
</li>
<li><p>테스트 자동화 플랫폼 활용</p>
<ul>
<li>테스트 방식을 표준화하여 제공</li>
<li>수만 개 이상의 테이스케이스 관리 용이</li>
</ul>
</li>
<li><p>신규 시스템 투입 방법</p>
<ul>
<li>시스템 변화를 원자적으로 적용?</li>
<li>최소 단위를 구분하여 카나리 배포</li>
<li>트래픽을 조정할 수 있다는 것의 장점 / 필요성</li>
</ul>
</li>
<li><p>정산 배치 관리</p>
<ul>
<li>젠킨스 잡만 1781개가 있음..</li>
</ul>
</li>
<li><p>과거 정산배치 구조</p>
<ul>
<li>매우 많은 배치가 다수의 IDC에 분산되어 있음</li>
<li>즉, 관리되지 않고 있었음.</li>
</ul>
</li>
<li><p>일단 하드웨어 장애에 취약한 환경에서 벗어나자.</p>
<ul>
<li>AWS k8s로 이전<ul>
<li>RTT 늘어남</li>
</ul>
</li>
</ul>
</li>
<li><p>배치는 결국 젠킨스</p>
<ul>
<li>안정적이어야함.</li>
<li>정산 도메인은 매우 stateful함</li>
<li>파이프라인 선언을 통해 워크플로우 정의 가능</li>
<li>플러그인 생태계가 풍부함</li>
</ul>
</li>
<li><p>젠킨스 잡 실행 장비를 고정해두면 비용 낭비 혹은 성능이슈가 생김.</p>
</li>
<li><p>dynamic provisioning 으로 필요할때만 사용</p>
</li>
<li><p>Job DSL</p>
</li>
<li><p>배치 모니터링 도구 강화</p>
</li>
</ul>
<h2 id="조직의-생산성을-높이는-process-mining">조직의 생산성을 높이는 Process Mining</h2>
<p><strong>김병묵</strong> | 토스플레이스 Node.js Developer</p>
<blockquote>
<p>반복되는 운영 업무를 자동화했는데도 조직이 성장하지 않는다면, 과연 문제는 어디에 있을까요? Process Mining을 활용해 데이터 기반으로 핵심 병목을 찾아내고, 업무 프로세스를 재설계한 사례를 소개하고자 해요. 개발자가 단순한 자동화를 넘어, 조직의 생산성 향상에 직접 기여할 수 있는 전략과 접근 방식을 함께 나눕니다.</p>
</blockquote>
<ul>
<li><p>Process Mining?</p>
<ul>
<li>우리가 어떻게 일하는지</li>
<li>업무 자동화와 생산성</li>
</ul>
</li>
<li><p>성장을 위해 필요한 OOO: 모두 필요함</p>
<ul>
<li>제품</li>
<li>일하는 방식</li>
<li>손님이 많아지는 가게가 마냥 좋은것은 아님.<ul>
<li>준비가 되어있어야 하는데, 그렇지 못하다면 컴플레인이나 실수가 많아지는 문제가 있음.</li>
<li>고객이 많아지기 위해서는 그걸 받아줄 인프라가 뒷받침되어야 함.</li>
<li>개발자 생활중에도 이런 점을 느낌</li>
</ul>
</li>
</ul>
</li>
<li><p>단순 반복 업무나 백오피스에 대한 자동화</p>
</li>
<li><p>이런 경험이 쌓일수록, 우선 &#39;우리가 어떻게 일하고 있는지&#39; 를 데이터 기반으로 명확하게 알아야 함.</p>
<ul>
<li>에러 감지 =&gt; 담당자 할당 =&gt; 개발 =&gt; 코드리뷰 =&gt; 배포</li>
<li>이 싸이클이 대략 몇시간, 얼마나 걸리는지, 그리고 각 비율이 얼마나 되는지.</li>
<li>보통 수집하는 데이터도 없고,비율도 명확한 수치로 알 수 없다.</li>
<li>반복 업무에 건당 약간이라도 자동화를 통해 시간을 세이브 할 수 있다면, 그 효과는 커질 수 있다.</li>
</ul>
</li>
<li><p>일반적인 팀의 업무 프로세스에서...</p>
<ul>
<li>이 업무들이 한달아 몇 건 정도 있는지, 얼마나 걸리는지, 각 단계를 어떻게 개선할 수 있는지<ul>
<li>동기부여도 된다.</li>
<li>팀에 기여한 성과도 알 수 있다.</li>
<li>새로운 아이디어를 주도할 수 있다.</li>
</ul>
</li>
</ul>
</li>
<li><p>자 다시 Process Mining: 우리가 어떻게 일하고 있는지 이해하기 위한 도구</p>
<ul>
<li><ol>
<li>데이터를 수집</li>
</ol>
</li>
<li><ol start="2">
<li>분석을 위한 가공</li>
</ol>
</li>
<li><ol start="3">
<li>이용을 위한 실제 업무 흐름일 도식화한 &#39;프로세스 모델&#39; 작성</li>
</ol>
<ul>
<li>단계별 소요시간 등..</li>
</ul>
</li>
</ul>
</li>
<li><p>어떤 업무를, 누가 어느정도 빈도로 하고 얼마나 시간을 쓰는지</p>
</li>
</ul>
<ol>
<li>데이터 수집<ul>
<li>모든 일은 시스템 위에서 일어난다<ul>
<li>팀마다 다양한 툴을 사용할수는 있다.</li>
<li>그러나 결국 기록은 하나의 시스템, 플랫폼 안에서 기록된다.</li>
<li>즉, 모든 흔적을 데이터로 쌓을 수 있다. =&gt; 외부 툴이어도 가능, 센트리, 깃, 슬랙, 마케팅 툴 등 대부분.</li>
<li>데이터를 수입할 기반부터 만들자.<ul>
<li>어떠한 데이터를 어떠한 포멧으로 남길까? =&gt; &quot;이벤트 로그&quot;</li>
</ul>
</li>
<li>PM4PY<ul>
<li>CaseID Activity Timestamp</li>
</ul>
</li>
</ul>
</li>
<li>그럼 어디까지 데이터를 수집해야 하는가?: Scoping</li>
<li>서로 다른 시스템을 어떻게 연결할것인가?: Entity Resolution<ul>
<li>결국 독립된 플랫폼이랑 GUID 가 필요한거 아닌가??</li>
<li>{{ 이거 JIRA 같은데에 배치로 뽑을 수 있는거 없나? 있을것같은데 }}</li>
</ul>
</li>
</ul>
</li>
<li>수집된 데이터로 일의 흐름과 병목을 분석하고 개선한다<ul>
<li>수집된 데이터를 바탕으로, 실제 흐름의 경향을 볼 수 있음</li>
<li>이 과정에서 노이즈가 생기면 그냥 안듣게됨..</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[토스의 기가막힌 문서 시스템: 실패에서 배운 교훈들]]></title>
            <link>https://velog.io/@hc-kang/%ED%86%A0%EC%8A%A4%EC%9D%98-%EA%B8%B0%EA%B0%80%EB%A7%89%ED%9E%8C-%EB%AC%B8%EC%84%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%8B%A4%ED%8C%A8%EC%97%90%EC%84%9C-%EB%B0%B0%EC%9A%B4-%EA%B5%90%ED%9B%88%EB%93%A4</link>
            <guid>https://velog.io/@hc-kang/%ED%86%A0%EC%8A%A4%EC%9D%98-%EA%B8%B0%EA%B0%80%EB%A7%89%ED%9E%8C-%EB%AC%B8%EC%84%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%8B%A4%ED%8C%A8%EC%97%90%EC%84%9C-%EB%B0%B0%EC%9A%B4-%EA%B5%90%ED%9B%88%EB%93%A4</guid>
            <pubDate>Fri, 25 Jul 2025 11:43:04 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>[ ⚠️ ]
이 문서는 토스 메이커스 컨퍼런스 2025 세션 중 토스의 기가막힌 문서 시스템: 실패에서 배운 교훈들 세션을 정리한 것입니다.</p>
</blockquote>
<h2 id="문제의-시작점-이거-누구한테-물어봐야-하나요">문제의 시작점: &quot;이거 누구한테 물어봐야 하나요?&quot;</h2>
<p>토스 프론트엔드팀이 직면했던 가장 큰 문제는 단순했고, 사실 직장인이라면 누구나 겪어보았을 문제였습니다.<br>새로운 개발자가 입사하면 항상 같은 질문들이 반복되었죠:</p>
<blockquote>
<ul>
<li>&quot;이 기능은 어떻게 동작하나요?&quot;</li>
<li>&quot;이 코드는 왜 이렇게 작성했나요?&quot;</li>
<li>&quot;이 문서가 최신 버전 맞나요?&quot;</li>
<li>&quot;이건 누가 잘 아시나요?&quot;</li>
</ul>
</blockquote>
<p><a href="https://www.notion.com/ko">Notion</a>, <a href="https://logseq.com/">Logseq</a>, <a href="https://www.atlassian.com/ko/software/confluence">Confluence</a>...<br>여러 도구를 써봤지만 결국 개발자들이 가장 선호하는 방법은 결국 아래와 같았죠.</p>
<blockquote>
<p>(옆자리 동료에게) &quot;저기 혹시.. 이거 아세요?&quot;
(슬랙으로) &quot;OO님 이거 써보셨어요?&quot;</p>
</blockquote>
<p>네, 결국 수많은 개발자들은 그냥 옆자리 동료에게 물어보는 걸 선호했습니다.</p>
<h2 id="암묵지-아는-사람만-아는-정보">암묵지: 아는 사람만 아는 정보</h2>
<pre><code class="language-js">setTimeout(() =&gt; {
    someUpdateUI();
}, 0);</code></pre>
<p>이런 코드를 보고 <code>&quot;왜 setTimeout을 0으로 설정한거지?&quot;</code>라고 물으면</p>
<p><code>&quot;아, 그거요? 예전에 IE에서 렌더링 이슈가 있어서...&quot;</code></p>
<p>이런 대답은 보통 문서에 잘 나와있지 않은 경우가 많죠.<br>특히 이슈로 인해 급하게 수정된 코드가 문서화에서 누락되는건 흔하게 발생하는 휴먼 에러니까요.</p>
<p>조직이 커질수록 이런 암묵지는 점점 더 큰 문제가 됩니다:</p>
<ul>
<li>담당자가 휴가나 퇴사를 하면 정보가 사라짐<ul>
<li><code>이거 할줄 아는거 OO님인데 지금 장기휴가 중이라서...</code></li>
</ul>
</li>
<li>슬랙은 너무 빠르게 흘러가서 정보를 찾기 어려움 (<del>게다가 검색은 또 왜 이모양이야</del>)</li>
<li>질문하기 눈치 보이는 문화가 생김<ul>
<li><code>이런거 물어봐도 되나..? 나 너무 멍청해보이는거 아니야..?</code></li>
</ul>
</li>
</ul>
<h2 id="이전의-문서화-시도들이-실패했던-이유">이전의 문서화 시도들이 실패했던 이유</h2>
<p>해당 세션 내에서 정확히 언급해 주시지는 않았지만, 추정컨데 아무래도 아래와 같은 이유가 아니었을까 싶습니다.</p>
<ul>
<li>문서가 다양한 매체에 분산되어 있어서, 찾기 어려움</li>
<li>문서의 <code>Ownership</code>이 명확하지 않아서, 누가 문서를 관리하는지 모호함</li>
<li>이로인해 방치되고, 최신화되지 않는 문서가 쌓임</li>
<li>결국 찾기 힘든 정보, 불확실한 정보가 된 문서더미..</li>
</ul>
<p>이러한 현실은 사실 수많은 개발자들이 겪어본, 그리고 겪고있는 문제죠.<br>이러한 이유로 개발자들은 여전히 옆자리 동료에게 물어보는 걸 선호한게 아닐까 싶습니다.</p>
<h2 id="해결을-위한-두-가지-원칙">해결을 위한 두 가지 원칙</h2>
<p>토스는 문서관리를 위한 두 가지 핵심 원칙을 세웠다고 합니다. 세션 중에서도 특히나 공감되는 내용이었어요.</p>
<h3 id="1-신뢰할-수-있는-정보">1. 신뢰할 수 있는 정보</h3>
<ul>
<li><strong>최신 상태 유지</strong></li>
<li><strong>정확성 보장</strong></li>
<li><strong>자기 완결성</strong> (다른 문서 참조 최소화)</li>
<li><strong>충분한 커버리지</strong> (최소 85% 이상 궁금증이 해소되어야 한다!)</li>
</ul>
<h3 id="2-누구나-접근-가능">2. 누구나 접근 가능</h3>
<ul>
<li><strong>통합 검색</strong>: <a href="https://www.algolia.com/">알골리아</a> 활용, 누구나 쉽게 도달 가능</li>
<li><strong>워크플로우 통합</strong>: <code>박씨</code>(토스 내부 슬랙 봇) 도입으로, 기존 업무환경(슬랙)을 벗어나지 않고 문서에 접근 가능</li>
<li><strong>컨텍스트 스위칭 최소화</strong>: MCP 활용 (<del>IDE를 벗어나지 않겠다는 의지</del>)</li>
</ul>
<h2 id="실질적-해결책-박씨-봇과-통합-문서-시스템">실질적 해결책: &#39;박씨&#39; 봇과 통합 문서 시스템</h2>
<h3 id="박씨-봇">박씨 봇</h3>
<ul>
<li>토스 내부 슬랙 봇, <a href="https://toss.tech/article/31643">아티클 참조</a></li>
<li>문서 검색 기능 제공</li>
<li>인사이트<ul>
<li><strong>접근성</strong>이 생각보다 훨씬 중요하다.</li>
<li>개발자들이 문서를 두고 결국 옆사람에게 물어보는것도, 본질적으로는 접근성 때문이다. 고품질의 문서라도 보려면 일일이 플랫폼에 들어가서 찾고, 검색해야 한다.</li>
</ul>
</li>
</ul>
<h4 id="박씨-봇의-예상치-못한-효과">박씨 봇의 예상치 못한 효과</h4>
<ul>
<li><p><strong>질문의 허들이 극적으로 낮아짐</strong></p>
<ul>
<li>사람이 아니니까 &quot;이런 걸 물어봐도 되나?&quot; 고민이 사라짐</li>
<li>새벽 3시에도, 점심시간에도 눈치 볼 필요 없음</li>
<li>&quot;멍청한 질문&quot;이라는 개념 자체가 없어짐</li>
</ul>
</li>
<li><p><strong>커뮤니티 지식의 활성화</strong></p>
<ul>
<li>박씨가 모르는 내용은 답을 아는 누군가가 등장해서 답변해주는 경우가 생김</li>
<li>다른 개발자들의 답변이나 외부자료가 있다면 그것도 박씨의 학습 데이터로 즉시 활용 가능함(<code>!학습</code> 커맨드)</li>
<li>질문하는 사람 + 답하는 사람 + 미래에 같은 궁금증을 가질 사람 모두에게 도움이 됨!</li>
</ul>
</li>
</ul>
<h3 id="통합-문서-시스템">통합 문서 시스템</h3>
<h4 id="문서-시스템의-핵심-요소">문서 시스템의 핵심 요소</h4>
<ul>
<li><strong>통합 검색</strong><ul>
<li><strong>전체 플랫폼에 대한 통합 검색</strong>의 중요성 또한 생각보다 크다</li>
</ul>
</li>
<li>익숙한 플랫폼<ul>
<li>문서를 찾기위해 새로운 플랫폼으로 전환하거나, 새로운 도구를 사용하는 것은 부담스럽다.</li>
</ul>
</li>
<li><strong>컨텍스트 스위칭 최소화</strong><ul>
<li>문서를 찾기위해 IDE 밖으로 벗어나는 것 조차 일종의 벽이 된다.</li>
</ul>
</li>
<li>(+@) 피드백 시스템<ul>
<li>단순한 &#39;좋아요&#39; 등 피드백도 양질의 문서 수렴을 위해 도움이 된다.</li>
</ul>
</li>
</ul>
<h4 id="문서-생성-파이프라인">문서 생성 파이프라인</h4>
<ul>
<li><strong>코드 기반 자동 문서 생성</strong> (<code>JSDoc</code>)</li>
<li><strong>CI 통합</strong> (문서 없으면 머지 불가)</li>
<li><strong>기술 리뷰</strong> (수석 개발자)</li>
<li><strong>Technical Writer 리뷰</strong> (가독성)</li>
<li><strong>자동 신선도 체크</strong> (오래된 문서 알림)</li>
</ul>
<h4 id="실제-구현-예시-docflow">실제 구현 예시: <a href="https://github.com/toss/docflow">Docflow</a></h4>
<pre><code class="language-typescript">// 예시: JSDoc을 활용한 자동 문서화
/**
 * @description 사용자 주문 내역을 조회합니다
 * @param {string} userId - 사용자 ID
 * @returns {Promise&lt;Order[]&gt;} 주문 목록
 * @throws {UserNotFoundError} 사용자를 찾을 수 없을 때
 * @example
 * const orders = await getUserOrders(&#39;user123&#39;);
 */
export async function getUserOrders(userId: string): Promise&lt;Order[]&gt; {
    // 구현...
}</code></pre>
<h2 id="문화-만들기-문서화는-인프라다">문화 만들기: 문서화는 인프라다</h2>
<p>세션의 결론으로 말씀하신 내용이 바로, 문서야 말로 회사의 인프라라는 것이었습니다.<br>매우 중요한 내용이라고 생각합니다.</p>
<h3 id="문서화-활성화-전략-선순환-구조-만들기">문서화 활성화 전략: 선순환 구조 만들기!</h3>
<p><img src="https://velog.velcdn.com/images/hc-kang/post/7f007bd7-db2a-4da7-bbc1-090949514e33/image.png" alt=""></p>
<p>가장 중요한 것은 &quot;읽기&quot; 단계라고 합니다. 아무리 좋은 문서도 읽히지 않으면 의미가 없으니까요.<br>&quot;읽어야 뭐라도 한다&quot; - 토스는 이 단계에 집중했습니다.</p>
<h3 id="문서화-활성화-구체적인-전략들">문서화 활성화 구체적인 전략들</h3>
<ol>
<li>리더들의 마중물 역할</li>
</ol>
<ul>
<li>팀 리더들이 먼저 초안을 대량으로 작성</li>
<li>&quot;완벽하지 않아도 괜찮다, 일단 쓰자&quot;는 분위기 조성</li>
<li>주 1회 &quot;문서화 데이&quot; 운영</li>
</ul>
<ol start="2">
<li>정복단 활동</li>
</ol>
<ul>
<li>특정 기술이나 도메인을 깊게 파고드는 스터디 그룹</li>
<li>스터디 결과물을 반드시 문서화하는 규칙(?)</li>
</ul>
<ol start="3">
<li>문서화 세션</li>
</ol>
<ul>
<li><code>Technical Writer</code>가 진행하는 글쓰기 교육</li>
<li>개발자를 위한 기술 문서 작성법</li>
<li>좋은 문서/나쁜 문서 사례 분석</li>
</ul>
<h3 id="문서의-종류별-관리-전략">문서의 종류별 관리 전략</h3>
<ul>
<li><p><strong>레퍼런스 문서</strong></p>
<ul>
<li>특징: 특정 소스코드에 대한 설명, 사전과 같은 속성</li>
<li>관리 방법: <code>JSDoc</code> + <code>Docflow</code>를 통한 자동화</li>
<li>품질 보증: <code>CI</code> 통합으로 문서 없으면 코드도 머지 불가</li>
</ul>
</li>
<li><p><strong>가이드 문서</strong></p>
<ul>
<li>특징: 튜토리얼, How-to, 개념 설명</li>
<li>관리 방법: 기술 리뷰 + Technical Writer 리뷰</li>
<li>품질 보증: 정기적인 신선도 체크, 피드백 반영</li>
</ul>
</li>
</ul>
<h2 id="핵심-교훈-문서-시스템-구축의-정석">핵심 교훈: 문서 시스템 구축의 정석</h2>
<h3 id="1-문서는-조직의-인프라다">1. 문서는 조직의 인프라다</h3>
<ul>
<li>문서를 단순한 &#39;기록&#39;이 아닌 &#39;인프라&#39;로 인식하는 관점의 전환이 필요합니다.</li>
<li>도로나 전기처럼, 문서도 조직이 원활하게 돌아가기 위한 필수 인프라입니다.</li>
</ul>
<h3 id="2-접근성이-신뢰성만큼-중요하다">2. 접근성이 신뢰성만큼 중요하다</h3>
<ul>
<li>아무리 정확하고 최신의 문서라도, 찾기 어렵거나 접근이 불편하면 무용지물입니다.</li>
<li>&quot;물어보기&quot;만큼 쉬운 접근성을 제공해야 합니다.</li>
</ul>
<h3 id="3-시스템과-문화-둘-다-필요하다">3. 시스템과 문화, 둘 다 필요하다</h3>
<ul>
<li>시스템: 자동화, 통합 검색, 봇</li>
<li>문화: 문서화 습관, 피드백 문화, 지속적 개선</li>
</ul>
<h3 id="4-8020-법칙을-기억하라">4. 80/20 법칙을 기억하라</h3>
<ul>
<li>모든 것을 100% 문서화할 필요는 없습니다.</li>
<li>하지만 자주 필요한 정보의 80% 이상은 반드시 고품질로 유지해야 합니다.</li>
</ul>
<h2 id="마무리">마무리</h2>
<p>사실, 개인적으로 이번 컨퍼런스에서 기술적인 내용들보다도 더 공감되고, 앞으로 도움이 될 내용들이 많았던 것 같습니다.<br>지금 회사에서도 문서화와 정보 검색 등에 대한 문제를 해결하기 위해 노력하고 있는데, 이번 세션을 통해 많은 것을 배워 적용 해 볼 수 있을 것 같습니다.  </p>
<p>읽어주셔서 감사하고, 도달할 수 있을지는 모르겠지만, 오늘 발표해주신 연사분들께 다시 한 번 감사드립니다!  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[왜 비오는날 인터넷이 느려지지?]]></title>
            <link>https://velog.io/@hc-kang/%EC%99%9C-%EB%B9%84%EC%98%A4%EB%8A%94%EB%82%A0-%EC%9D%B8%ED%84%B0%EB%84%B7%EC%9D%B4-%EB%8A%90%EB%A0%A4%EC%A7%80%EC%A7%80</link>
            <guid>https://velog.io/@hc-kang/%EC%99%9C-%EB%B9%84%EC%98%A4%EB%8A%94%EB%82%A0-%EC%9D%B8%ED%84%B0%EB%84%B7%EC%9D%B4-%EB%8A%90%EB%A0%A4%EC%A7%80%EC%A7%80</guid>
            <pubDate>Wed, 23 Jul 2025 23:54:37 GMT</pubDate>
            <description><![CDATA[<p>요즘 날씨가 참 이상하죠?<br>정말 말그대로 40도를 찍어버리더니 며칠 전까지는 정말 미친듯이 비를 또 쏟아내렸죠.  </p>
<p>그리고 지난 주 일요일, 천둥번개치고 비오는 날 밤 디스코드로 밋업을 했는데요, 유난히 사람들이 인터넷이 자꾸 끊긴다는 이야기들을 많이 하시더라구요.  </p>
<p>그런데 생각해보면 우리 모두 한번쯤은 이런 경험을 하지 않았을까? 하는 생각이 들었어요.  </p>
<p>특히 저는 장교로 복무하면서, 비가 심하게 오는 날엔 정말 인트라넷이 거의 마비되는 경우도 많았죠.(<del>일은 안해서 좋았지만, 통신장교들 RIP</del>)  
그래서 한번 찾아보기로 했죠. 과연 이게 정말일까? 아니면 다들 그냥 나빴던 기억만 나는걸까?  </p>
<p>제 몇가지 가설은 이렇습니다.</p>
<blockquote>
<ol>
<li>비오는 날에는 사람들이 다들 외출을 안하고 집에서 인터넷을 쓴다. 이로인해 트래픽이 증가하여 인터넷이 느려진다.  </li>
<li>비(낙뢰)로 인해 물리적인 네트워크 장비의 성능이 저하(혹은 손상)되어 인터넷이 느려진다.  </li>
<li>사실 느려지는게 아니라 그냥 사람들이 부정성 편향으로 인해(비오는 날씨 + 순간 느린 인터넷) 착각하는 것이다.  </li>
</ol>
</blockquote>
<p>그래서 저는 이 가설들을 검증하기 위해 관련 자료들을 찾아보기로 했어요.</p>
<h2 id="가설-1-비오는-날에는-사람들이-다들-외출을-안하고-집에서-인터넷을-쓴다-이로인해-트래픽이-증가하여-인터넷이-느려진다">가설 1. 비오는 날에는 사람들이 다들 외출을 안하고 집에서 인터넷을 쓴다. 이로인해 트래픽이 증가하여 인터넷이 느려진다.</h2>
<p>이 가설은 직관적으로는 말이 되는 것 같은데, 좀 더 생각해보니 허점이 꽤 있더라구요.  </p>
<p>우선 검색해보니 비오는날 실제로 트래픽이 평소보다 약 3~5% 정도 늘어난다고 합니다. 하지만 그렇게 큰 차이는 아니죠?<br>그리고 우리나라의 인터넷 인프라는 그렇게 나약하지 않아서 그 정도 부하 증가는 티도 나지 않을거에요.  </p>
<p>그리고 보통 사람들이 &#39;인터넷이 느려진다&#39;는걸 느끼는건 퇴근 후 저녁시간대인데, 이 시간대에 날씨가 좋다고 더 많은 사람들이 나가서 논다..? 는 생각하기 어렵죠.</p>
<h2 id="가설-2-비낙뢰로-인해-물리적인-네트워크-장비의-성능이-저하혹은-손상되어-인터넷이-느려진다">가설 2. 비(낙뢰)로 인해 물리적인 네트워크 장비의 성능이 저하(혹은 손상)되어 인터넷이 느려진다.</h2>
<p>아마 이 가설이 정답에 가장 가깝지 않을까 싶어요.  </p>
<p>우선 여기서는 사람들이 집에서 흔히 사용하는 유선 인터넷에 대해서만 이야기 해볼게요.  </p>
<img src="https://velog.velcdn.com/images/hc-kang/post/8dbc45e7-15e9-4297-a764-ae5cfd5ccc19/image.png" alt="한국의 광섬유 보급률 현황">

<p>위 이미지처럼, 우리나라의 광섬유 보급률은 꽤나 높은 편이라고 해요. 하지만 남은 10%는 아직도 동축 케이블(구리선)을 사용하고 있어요.  </p>
<p><img src="https://velog.velcdn.com/images/hc-kang/post/ca4724df-9b9e-4ef3-8827-85c8cfeb5485/image.png
" alt="광섬유와 동축 케이블 비교"></p>
<p>이런 구리선은, 손상된 부분이나 접점에 물이 닿게되어 전도율이 꽤나 떨어진다고 해요.<br>따라서 전송속도도 떨어지고 에러율도 높아지죠.  </p>
<p>여기부터는 이제 제 <strong>뇌피셜</strong>인데요. 이전에 공부했던 내용을 바탕으로 약간의 추측을 해 봤어요.  </p>
<p>앞서 이야기 한 것 처럼 물리 레이어에 문제가 생긴다면 비트 에러의 발생빈도가 증가할 것 같아요. 물론 작은 오류들이라면 체크섬으로 잡아내거나 직접 복구하는 것도 가능하겠지만, 그 빈도가 평소보다 높아진다면 직접 복구할 수 없는 오류의 빈도도 그만큼 높아질거라고 생각해요. 그렇다면 자연스럽게 패킷의 재전송 요청도 증가할 겁니다.  </p>
<img src="https://velog.velcdn.com/images/hc-kang/post/4f81d5de-9392-4408-bcdf-2fef1f7c5a31/image.png" alt="TCP 혼잡 제어 알고리즘">

<p>여기서 TCP의 속도 제어 기능이 작동하게 되면 더 문제가 생길거에요.  </p>
<p>TCP는 기본적으로, 처음부터 최고 속도로 동작하지 않습니다. 처음엔 느리게 시작해서 요청이 성공하면 속도를 지수적으로 증가시키는 방식이죠.<br>그런데 한가지 문제는, TCP에서는 전송 중 실패를 감지하면 전송속도를 절반씩 줄여버립니다.(TCP 혼잡 제어)<br>하지만 이러한 상황에서는 속도를 줄인들 혼잡이 해결되지 않을겁니다. 물리적인 장비가 오동작 하고있는 상태이니까요. - 그사이에 물기가 말랐다면 몰라도요 -</p>
<p>정리해 보면 이렇게 되겠네요.</p>
<blockquote>
<p><strong>1. 비트 에러율 증가</strong></p>
<ul>
<li>신호 품질 저하 -&gt; 비트 에러 발생</li>
<li>CRC(Cyclic Redundancy Check) 체크 실패 -&gt; 패킷 재전송 요청</li>
</ul>
<p><strong>2. 혼잡 제어의 오인 동작</strong></p>
<ul>
<li>(정상 케이스): 패킷 손실 -&gt; 네트워크 혼잡 -&gt; 전송 속도 감소 -&gt; 혼잡 해소</li>
<li>(문제 케이스): 패킷 손실 -&gt; (그냥 느림..) -&gt; 전송 속도 감소 -&gt; 여전히 느림 -&gt; 계속 감소</li>
</ul>
<p><strong>3. 지연 시간 증가</strong></p>
<ul>
<li>재전송 타임아웃(RTO; Retransmission Timeout) 증가</li>
<li>지수적 백오프(Exponential Backoff) 작동</li>
<li>Round Trip Time(RTT) 증가</li>
</ul>
</blockquote>
<p>이처럼 아직 국내 회선의 약 10%는 구리선을 사용하고 있으니, 사용자 10명 중 한명은 문제가 생길 수 있겠네요.<br>그런데 이런 사용자가 화상회의나 게임에서 한명씩만 있더라도 해당 그룹에서는 전체적인 지연을 체감 할 수 있겠구나 싶지 않나요?</p>
<h2 id="가설-3-사실-느려지는게-아니라-그냥-사람들이-부정성-편향으로-인해비오는-날씨--순간-느린-인터넷-착각하는-것이다">가설 3. 사실 느려지는게 아니라 그냥 사람들이 부정성 편향으로 인해(비오는 날씨 + 순간 느린 인터넷) 착각하는 것이다.</h2>
<p>이것도 꽤나 그럴싸한 가설이라고 생각해요.  </p>
<p>결국 비오는 날에도 인터넷 속도는 전혀 느려지지 않지만, 단지 사람들이 그렇게 착각할 뿐이다! 라는 내용이에요.  </p>
<p>사람들은 부정적인 상황을 더 잘 기억한다고 하잖아요?<br>그래서 똑같이 답답했던 경험이라도 날씨까지 안좋다면 더 심하게 느끼지 않을까 싶어요.<br>이로인해 더 부정적인 기억이 강화되고 결국은 이걸 많은 사람들이 공유까지 하면서 이러한 인식을 만들지 않았을까 싶습니다</p>
<h2 id="결론">결론</h2>
<p>결론적으로 가설 2가 가장 그럴싸하고, 과거의 이야기이지만 직접 경험에서 비롯한 꽤나 신뢰할 수 있는 가설이라고 생각해요.<br>특히 인프라가 노후한 주택가나 오래된 아파트 단지라면, 마치 제가 군대에서 겪었던 것처럼 더 큰 체감을 하실지도 몰라요.  </p>
<p>오늘은 이렇게, 개발과는 조금 멀어진 그냥 궁금한 이야기를 정리해 봤습니다.
사실 여러 방면으로 검색해봐도 정말 명확한 결론이나 참고 자료가 없다는 점에서 정말 아쉽네요.</p>
<p>추측 과정에서 제가 놓치거나 헛다리를 짚은 부분이 있을 수도 있어요.<br>혹시 전문가분이 있으시다면 틀린 부분을 댓글로 공유해주시면 정말 감사하겠습니다!  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿠키 스터핑과 CSRF]]></title>
            <link>https://velog.io/@hc-kang/%EC%BF%A0%ED%82%A4-%EC%8A%A4%ED%84%B0%ED%95%91%EA%B3%BC-CSRF</link>
            <guid>https://velog.io/@hc-kang/%EC%BF%A0%ED%82%A4-%EC%8A%A4%ED%84%B0%ED%95%91%EA%B3%BC-CSRF</guid>
            <pubDate>Sun, 22 Jun 2025 04:32:14 GMT</pubDate>
            <description><![CDATA[<p>올해 초 쯤, 커뮤니티에서 &#39;허니&#39;라는 크롬 익스텐션이 논란이 되었던 적이 있었습니다.<br>잘 모르시는 분들을 위해 간략하게만 설명드리면, 이 허니라는 익스텐션은 사용자에게 특정 쇼핑몰 등의 할인 쿠폰을 자동으로 찾아서 적용해주는 서비스였는데요, 설명만 들으면 안쓰는 사람만 바보가 아닌가 싶을 정도로 좋은 도구죠.  </p>
<image src="https://velog.velcdn.com/images/hc-kang/post/d7ba8665-aa44-4f91-9968-738de4b4f82f/image.png" width="500px" />

<p>그런데 여기서, 몇가지 문제점이 알려지게 됩니다.<br>자세한 내용이 궁금하신 분들은 <a href="https://openads.co.kr/content/contentDetail?contsId=15574">관련 기사</a>나 코딩애플(<del>어둠의 생활코딩</del>)님의 <a href="https://www.youtube.com/watch?v=EsqBuwOuXB0">유튜브 영상</a>도 많이 있으니, 한번쯤 찾아보시는것도 좋을 것 같습니다.  </p>
<p>자, 서론은 여기까지 하고, 결국 이 익스텐션의 문제점을 다시 말해보자면 &#39;사용자 몰래 쿠키를 조작해서 이익을 챙겼다&#39;는 점입니다.<br>물론 정확히 추산 할 수는 없겠지만 원래 그 쿠폰, 추천인 등으로 이익을 받았어야 할 인플루언서들의 피해도 상당할 겁니다.  </p>
<p>이런 사건을 그냥 쿠키를 사용한 단순한 어뷰징으로 치부하고 넘어갈 수도 있겠지만, 저는 이 사건을 보고 웹 공격 기법 중 하나인 <code>CSRF</code>(Cross-Site Request Forgery)가 떠오르더라구요. 물론 엄밀한 정의는 다르지만, &quot;이런 쿠키 스터핑도 CSRF의 한 형태로 볼 수 있지 않을까?&quot; 하는 질문에서부터 오늘의 글을 시작해보려 합니다.</p>
<h2 id="쿠키-스터핑-문제점은-무엇일까">쿠키 스터핑: 문제점은 무엇일까?</h2>
<image src="https://velog.velcdn.com/images/hc-kang/post/75987b2c-2bf4-4d46-b067-d4bb2e959bae/image.png" width="500px" />

<p>먼저 &#39;쿠키 스터핑(Cookie Stuffing)&#39;이 어떻게 동작하는지 간단히 짚고 넘어가겠습니다.</p>
<p>웹 브라우저와 쿠키에 대해 잘 모르시는분들을 위해 간단히 첨언하자면, 쿠키는 간단한 데이터를 클라이언트, 즉 사용자의 브라우저에 일시적으로 저장해두기 위해 사용되는 기술입니다.  </p>
<p>이를 통해 <strong>사용자를 증명(로그인 관리)</strong> 하거나, <strong>특정 상태값(다크모드)을 유지</strong>하거나, <strong>특정 정보(이 사용자는 언제 방문했었음)를 저장</strong>해두는 등의 용도로 사용됩니다.  </p>
<p>주요한 특징은 <strong>브라우저가 매 요청에 자동으로 쿠키를 첨부한다는 점</strong>입니다.<br>이 부분이 바로 오늘의 포인트이자 우리가 항상 주의하고, 해결해야할 문제점이기도 합니다.</p>
<h3 id="정상적인-경우">정상적인 경우</h3>
<p>자 그러면 이제, 정상적인 경우를 먼저 살펴보겠습니다.  </p>
<image src="https://velog.velcdn.com/images/hc-kang/post/a9257fa4-6a4c-438a-9816-f24a41990f9e/image.png" width="600px" />

<blockquote>
<p>[정상적인 경우]</p>
<ol>
<li>사용자가 인플루언서 등의 소개로 얻은 정보를 바탕으로 제품 구매 페이지로 이동합니다. 이 과정에서 사용자의 브라우저에는 추천인 쿠키가 심어집니다.</li>
<li>사용자가 제품을 결제합니다. 이때 <strong>쿠키는 자동으로 서버에 전송</strong>됩니다.</li>
<li>서버는 쿠키를 확인하고, 추천인에게 보상을 지급합니다.</li>
</ol>
</blockquote>
<p>이 경우에는 쿠키를 통해 &#39;이 사용자는 누구의 추천을 통해 여기에 방문했다&#39;라는 정보를 저장해두는 것이 목적이죠.<br>그래야 이를 통해 추천인에게 보상을 지급할 수 있기 때문입니다.  </p>
<h3 id="문제가-되는-경우">문제가 되는 경우</h3>
<p>그러면 이제 문제가 되는 경우를 살펴보겠습니다.  </p>
<image src="https://velog.velcdn.com/images/hc-kang/post/73bcbdf3-e7ee-4d73-8da0-2ac4e550adae/image.png" width="600px" />

<blockquote>
<p>[쿠키 스터핑 사례]</p>
<ol>
<li>(위와 동일)사용자가 인플루언서 등의 소개로 얻은 정보를 바탕으로 제품 구매 페이지로 이동합니다. 이 과정에서 사용자의 브라우저에는 추천인 쿠키가 심어집니다.</li>
<li>쿠키 추천 익스텐션이 쿠폰을 찾으면서 <strong>사용자 몰래 자신의 추천인 쿠키를 심습니다.</strong> 이 과정에서 사용자에게 제품을 소개해 준 <strong>인플루언서의 추천인 쿠키가 덮어씌워</strong>집니다.</li>
<li>(위와 동일)사용자가 제품을 결제합니다. 이때 쿠키는 자동으로 서버에 전송됩니다.</li>
<li>(위와 동일)서버는 쿠키를 확인하고, 추천인에게 보상을 지급합니다.</li>
</ol>
</blockquote>
<h2 id="csrf-많이-들어는-봤는데">CSRF: 많이 들어는 봤는데..</h2>
<p>그렇다면 <code>CSRF(Cross-Site Request Forgery)</code>는 무엇일까요?<br>CSRF의 핵심 구성요소는 다음과 같습니다.  </p>
<blockquote>
<p>[CSRF의 핵심 구성요소]</p>
<ol>
<li>사이트 간 요청(Cross-Site): 일반적으로 공격자는 실제 운영중인 서비스 내의 코드를 직접 조작할수 없습니다. 따라서 <strong>자신이 작성한, 혹은 자신이 조작할 수 있는 다른 사이트</strong>를 통해 피해 대상 사이트로 요청을 보내는 방식으로 공격합니다.</li>
<li>사용자 의사와 무관한 요청(Request Forgery): 그중에서도 특히, 사용자가 의도하지 않은 요청을 공격자가 원하는 대로 전송하는 것을 의미합니다.</li>
<li>이때 꼭 필요한 특성이 있는데, 이것이 바로 <strong>사용자의 브라우저가 자동으로 쿠키를 첨부하는 특성</strong>을 이용하는 것입니다.</li>
<li>(+@) 추가적으로, HTTP 요청은 모두 독립적이기에, 누구나 마치 &#39;나&#39;처럼 요청을 보낼 수 있다는 점도 중요합니다.</li>
</ol>
</blockquote>
<p>이제 이걸 시나리오로 좀 더 이해하기 쉽게 풀어보자면, 아래와 같습니다</p>
<h2 id="csrf-시나리오로-이해해보기">CSRF 시나리오로 이해해보기</h2>
<p>여기부터 CSRF가 실제로 어떻게 동작하는지 시나리오로 살펴보겠습니다.</p>
<h3 id="일반적인-csrf-공격-시나리오">일반적인 CSRF 공격 시나리오</h3>
<image src="https://velog.velcdn.com/images/hc-kang/post/db542c73-acea-4ade-9738-4ff828cb9042/image.png" width="600px" />

<blockquote>
<p>[CSRF 공격 예시]</p>
<ol>
<li>사용자가 은행 사이트에 로그인합니다. 브라우저에 인증 쿠키가 저장되죠.</li>
<li>사용자가 로그아웃하지 않고 다른 탭에서 공격자의 사이트를 방문합니다.</li>
<li>공격자의 사이트에는 은행으로 송금 요청을 보내는 숨겨진 코드가 있습니다.</li>
<li>사용자 모르게 브라우저가 은행으로 송금 요청을 보냅니다. <strong>이때 인증 쿠키가 자동으로 포함됩니다.</strong></li>
<li>은행은 정상적인 요청으로 판단하고 송금을 처리합니다.  </li>
</ol>
</blockquote>
<p>핵심은 &quot;브라우저가 쿠키를 자동으로 보낸다&quot;는 점입니다. 공격자는 이 특성을 악용해 사용자가 의도하지 않은 요청을 보내게 만들죠.  </p>
<h2 id="쿠키-스터핑과-csrf의-공통점">쿠키 스터핑과 CSRF의 공통점</h2>
<p>사실 두개의 개념은 본질적으로는 꽤나 다릅니다.<br>하나는 단순히 <del>얌체같은</del> 쿠키 조작이고, 다른 하나는 정말 큰 위협이 될수도 있는 웹 공격 기법 중 하나니까요.<br>하지만 본질적으로는 둘 다 브라우저의 편의 기능을 악용해서 사용자를 기만한다는 점에서 공통점이 있습니다.  </p>
<h3 id="1-사용자의-의도와-무관한-동작">1. 사용자의 의도와 무관한 동작</h3>
<ul>
<li><strong>CSRF</strong>: &quot;아니, 나는 송금하려고 한 적이 없는데?&quot;</li>
<li><strong>쿠키 스터핑</strong>: &quot;어? 나는 추천인을 바꾸려고 한 적이 없는데?&quot;</li>
</ul>
<h3 id="2-브라우저의-자동-동작-악용">2. 브라우저의 자동 동작 악용</h3>
<ul>
<li><strong>CSRF</strong>: 쿠키 자동 전송을 악용</li>
<li><strong>쿠키 스터핑</strong>: 쿠키 자동 저장/덮어쓰기를 악용</li>
</ul>
<h2 id="개발자로서-할-수-있는-일들">개발자로서 할 수 있는 일들</h2>
<p>그래서 우리가 실제로 뭘 할 수 있을까요? 몇 가지 실용적인 방법들을 소개해 드리겠습니다.</p>
<h3 id="1-쿠키-설정-시-보안-옵션-활용하기">1. 쿠키 설정 시 보안 옵션 활용하기</h3>
<pre><code class="language-javascript">// 민감한 정보를 쿠키에 저장할때는 보안 옵션을 추가해주세요.
res.cookie(&#39;referral&#39;, referralId, {
    httpOnly: true,              // JavaScript로 접근 불가
    secure: isProduction,        // HTTPS에서만 전송
    sameSite: &#39;lax&#39;,             // 크로스 사이트 요청 제한
    maxAge: 24 * 60 * 60 * 1000, // 24시간 유효 시간
    path: &#39;/&#39;,                   // 쿠키 적용 범위
    domain: &#39;example.com&#39;        // 쿠키 적용 도메인
});</code></pre>
<p>이 방법은 쿠키를 발급하는 시점에서 최대한 공격 표현을 줄이는 방법인데요.
자세한 내용은 아래와 같습니다.</p>
<h4 id="1-httponly">1. httpOnly</h4>
<p><code>httpOnly</code> 옵션은 해당 쿠키를 JS로 접근 할 수 없도록 합니다. 따라서, 악의적인 스크립트 등으로 쿠키를 탈취하는 것을 방지할 수 있습니다.<br>무조건 켜면 좋은 옵션은 아니니, JS를 통한 클라이언트측 조작이 필요한가에 따라 결정해야 합니다.</p>
<p>일반적인 가이드를 한번 정리해보자면 아래와 같이 정리 할 수 있을 것 같네요.</p>
<ul>
<li>세션, 사용자 정보 등 민감 정보는 반드시 <code>httpOnly: true</code></li>
<li>다크모드, 언어설정 등 <strong>클라이언트단의 조작이 필요한 정보</strong>는 <code>httpOnly: false</code></li>
</ul>
<h4 id="2-secure">2. secure</h4>
<p>이 옵션은 쿠키를 HTTPS 프로토콜에서만 전송할 수 있도록 합니다.<br>현대에 서비스하는 대부분의 서비스는 실질적으로 HTTPS가 강제되고 있기 때문에, 이 옵션은 꼭 켜두는게 좋습니다.  </p>
<p>다만 개발환경에서는 몹시 귀찮기 때문에 비활성화 하는 경우가 많습니다.  </p>
<pre><code class="language-js">const isProduction = process.env.NODE_ENV === &#39;production&#39;;

res.cookie(&#39;referral&#39;, referralId, {
    secure: isProduction,  // 프로덕션에서만 HTTPS 강제
    httpOnly: true
});</code></pre>
<h4 id="3-samesite">3. sameSite</h4>
<p>이 옵션이 아마 가장 많이 헷갈리고, CSRF 공격을 방지하는데 가장 중요한 옵션이기도 합니다.  </p>
<p>이 옵션은 기본적으로 <code>lax</code>로 설정되어 있는데, 이는 대부분의 경우 적절한 설정이기는 합니다.<br>흔히 해당 옵션을 켜면 크로스 사이트 요청, 그러니까 다른 도메인에서 오는 요청을 <strong>무조건 거절</strong>하는 것으로 오해하시는 경우가 많은데요. 이 옵션은 어디까지나 &#39;<strong>사용자의 의도와 무관하게 발생하는 요청</strong>&#39;에 대한 쿠키를 제한하는 옵션입니다.<br>다시말해, 크로스 사이트 요청인 경우에도 사용자가 의도적으로 발생시킨 요청이라면 쿠키를 전송하게 됩니다. 더 구체적이 예를 들자면 아래의 두 경우가 있습니다.</p>
<ul>
<li>사용자가 주소창에 직접 입력한 주소로 이동하는 경우</li>
<li>사용자가 다른 도메인의 링크를 클릭하여 이동하는 경우</li>
</ul>
<p>즉, 위에서 말했듯 &#39;사용자가 의도하지 않은 백그라운드 리소스 및 JS 요청&#39;에 대해서는 쿠키를 전송하지 않는 방식으로 동작하여, 사용성과 보안을 모두 최대한 보장하는 방식으로 동작합니다.  </p>
<p>이외의 다른 값들은 아래와 같습니다.</p>
<ul>
<li><code>strict</code>: 같은 도메인에서만 쿠키를 보낼 수 있습니다.</li>
<li><code>none</code>: 모든 도메인에서 쿠키를 보낼 수 있습니다.</li>
</ul>
<p>여기서도 한번 가이드라인을 제공하자면, 이렇게 정리 할 수 있겠네요.</p>
<ul>
<li>세션, 사용자 정보 등 민감 정보는 반드시 <code>sameSite: &#39;strict&#39;</code><ul>
<li>피치못하게 필요한 경우에만 <code>sameSite: &#39;lax&#39;</code></li>
</ul>
</li>
<li>이외 대부분의 경우 <code>sameSite: &#39;lax&#39;</code></li>
<li>광고나 사용자 추적, 추천등에 대해서는 <code>sameSite: &#39;none&#39;</code></li>
</ul>
<h4 id="4-maxage">4. maxAge</h4>
<p><code>maxAge</code>는 이름에서 알 수 있듯 쿠키의 유효 시간을 설정합니다.<br>이 옵션은 쿠키의 유효 시간을 설정함으로서 유출이나 악용 가능성이 있는 쿠키를 지정된 시간동안만 유지할 수 있게 합니다.<br>이또한 최대한 명시적으로 지정해 두는 것이 좋습니다.</p>
<h4 id="5-path-domain">5. path, domain</h4>
<p>이 옵션은 쿠키의 적용 범위를 설정합니다.<br>기본적으로 쿠키는 현재 도메인에서만 적용되며, 이를 통해 쿠키의 범위를 제한할 수 있습니다.<br>하지만 필요에 따라 다른 도메인에서도 쿠키를 적용해야 하는 경우가 있습니다.<br>이때 이 옵션을 통해 쿠키의 적용 범위를 제한할 수 있습니다.</p>
<p><code>path</code>의 경우 한 도메인 내에서 특정 경로에 대해서만 쿠키를 적용할 수 있고, <code>domain</code>의 경우 다른 도메인에서도 쿠키를 적용할 수 있습니다.<br>하지만 당연히 이때도 아무 도메인이나 허용할 수 있는것은 아니고, 자신 혹은 자신의 상위 도메인에 대해서만 허용할 수 있습니다.  </p>
<ul>
<li><code>path</code>: 특정 경로(<code>/</code>, <code>/admin</code>, <code>/api</code> 등)에 대해서만 쿠키를 적용할 수 있습니다.</li>
<li><code>domain</code>: 특정 도메인(<code>example.com</code>, <code>.example.com</code>, <code>www.example.com</code> 등)에 대해서만 쿠키를 적용할 수 있습니다.</li>
</ul>
<h3 id="2-csrf-토큰-사용하기">2. CSRF 토큰 사용하기</h3>
<p>앞서 계속 다루었듯, 결국 CSRF 공격이 이루어지는 이유는 브라우저가 쿠키를 자동으로 첨부하여 전송하기 때문입니다.<br>이를 정면으로 맞서는 방식이 바로 CSRF 토큰을 사용하는 방식이죠.<br>즉, 이제 개발자가 구현을 통해 &#39;의도적으로&#39; 토큰을 첨부함으로서 &#39;이 요청은 사용자(개발자) 몰래 자동적으로 발생한 요청이 아니야.&#39; 라고 증명하는 것이죠.</p>
<pre><code class="language-javascript">// 서버와 클라이언트가 모두 알고있는 토큰을 매번 &#39;직접 첨부&#39;하는 방식으로 사용합니다.
const csrfToken = &quot;l23d4ktif..&quot;;

// 정상적인 사용자 요청
// → 쿠키(자동) + CSRF토큰(수동) 모두 포함. 정상 처리 ✅
fetch(&#39;/transfer&#39;, {
    method: &#39;POST&#39;,
    headers: {
        &#39;X-CSRF-Token&#39;: csrfToken  // 개발자가 의도적으로 포함
    },
    body: JSON.stringify({ to: &#39;friend&#39;, amount: 100 })
});

// CSRF 공격 시도 (공격자 사이트에서)
// → 쿠키(자동) 포함, CSRF토큰(없음) → 서버에서 거부
fetch(&#39;https://bank.com/transfer&#39;, {
    method: &#39;POST&#39;,
    credentials: &#39;include&#39;,  // 쿠키는 자동으로 포함됨
    body: JSON.stringify({ to: &#39;attacker&#39;, amount: 1000000 })
    // 공격자는 Same-Origin Policy에 의해 CSRF 토큰 값을 직접 읽을 수 없음! ❌
});</code></pre>
<p>그러면 이런 CSRF 토큰은 어떻게 생성할까요? 그리고 이 토큰은 결국 서버와 클라이언트가 모두 알고 있어야 하는데, 어떻게 관리해야하는 걸까요?  </p>
<p>바로 위 사례에서 알 수 있듯, 만약에 CSRF 토큰이 고정된 값이라거나 &#39;뻔한&#39; 값이라면 여전히 공격자는 공격에 성공할 수 있습니다. 뭐 예를 들어 CSRF 토큰이 단순히 <code>csrftoken123</code> 이라면 그냥 이 값을 넣어서 요청을 보내면 되는 거니까요.  </p>
<p>때문에 이를 방지하기 위해 CSRF 토큰은 랜덤하고 예측 불가능하게 생성되어야 합니다. 또한 주기적으로 새로운 값을 발급받아(Refresh) 사용해야 하죠.</p>
<p>이를 가장 편리하게 해결 할 수 있는 방식이 바로 <code>Double Submit Cookie</code> 방식입니다.  </p>
<pre><code class="language-js">// 1. 서버: 토큰을 쿠키와 응답 모두에 포함
app.get(&#39;/api/csrf-token&#39;, (req, res) =&gt; {
    const token = crypto.randomBytes(32).toString(&#39;hex&#39;);

    res.cookie(&#39;csrf-token&#39;, token, {
        httpOnly: false,  // 클라이언트에서 읽을 수 있어야 함
        secure: true,
        sameSite: &#39;strict&#39;
    });

    res.json({ csrfToken: token });
});

// 2. 클라이언트: 쿠키 값을 읽어서 헤더에 복사
function getCSRFToken() {
    const cookies = document.cookie.split(&#39;;&#39;);
    const csrfCookie = cookies.find(c =&gt; c.trim().startsWith(&#39;csrf-token=&#39;));
    return csrfCookie ? csrfCookie.split(&#39;=&#39;)[1] : null;
}

fetch(&#39;/api/transfer&#39;, {
    method: &#39;POST&#39;,
    headers: {
        &#39;X-CSRF-Token&#39;: getCSRFToken()  // 쿠키 값을 헤더에 복사
    },
    body: JSON.stringify({ to: &#39;friend&#39;, amount: 100 })
});

// 3. 서버: 쿠키 값과 헤더 값이 일치하는지 확인
app.post(&#39;/api/transfer&#39;, (req, res) =&gt; {
    const headerToken = req.headers[&#39;x-csrf-token&#39;];
    const cookieToken = req.cookies[&#39;csrf-token&#39;];

    if (!headerToken || headerToken !== cookieToken) {
        return res.status(403).json({ error: &#39;CSRF 검증 실패&#39; });
    }

    // 검증 성공, 작업 수행
    performTransfer(req.body.to, req.body.amount);
});</code></pre>
<p>이 방식은 CSRF 토큰을 특별히 발급하고 유지/관리하는 데에 큰 부담이 없다는 장점이 있습니다.<br>현대 브라우저의 구현 덕분에, 단순히 현재 요청에 쿠키와 CSRF 토큰을 모두 담고있음에도 해당 페이지의 개발자만이 토큰과 인증 쿠키를 동시에 사용 할 수 있기 때문입니다.  </p>
<h2 id="마치며">마치며</h2>
<p>쿠키 스터핑 사건을 보면서 CSRF를 떠올린 건, 단순한 호기심에서 출발한 다소 뜬금없는 발단이기는 했습니다.<br>하지만 이를 통해 조금이나마 사용자와 개발자가 모두 더 경각심을 가지고, 어떻게 하면 더 사용자 친화적이면서도 보안상 안전한 구조를 만들 수 있을까 한번 더 생각해 볼 수 있었습니다.  </p>
<p>사실 완벽한 보안은 없겠죠. 하지만 원리를 이해하고, 조금씩 계속 개선해나가는 자세가 중요하지 않을까요?  </p>
<p>이 글이 조금이나마 도움이 되었으면 좋겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Verdaccio로 사내 프록시 레지스트리 구축하기 - 커스텀 패키지 사용하기]]></title>
            <link>https://velog.io/@hc-kang/Verdaccio%EB%A1%9C-%EC%82%AC%EB%82%B4-%ED%94%84%EB%A1%9D%EC%8B%9C-%EB%A0%88%EC%A7%80%EC%8A%A4%ED%8A%B8%EB%A6%AC-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-%EC%BB%A4%EC%8A%A4%ED%85%80-%ED%8C%A8%ED%82%A4%EC%A7%80-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hc-kang/Verdaccio%EB%A1%9C-%EC%82%AC%EB%82%B4-%ED%94%84%EB%A1%9D%EC%8B%9C-%EB%A0%88%EC%A7%80%EC%8A%A4%ED%8A%B8%EB%A6%AC-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-%EC%BB%A4%EC%8A%A4%ED%85%80-%ED%8C%A8%ED%82%A4%EC%A7%80-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 30 Mar 2025 06:05:20 GMT</pubDate>
            <description><![CDATA[<h2 id="0-발단">0. 발단</h2>
<p>현재 업무에서 사용하는 라이브러리 중, 몇몇 라이브러리는 도메인에 맞도록 혹은 성능 개선을 위해 커스텀해서 사용중인데요.<br>이러한 모듈이 점차 많아지면서 전체 node_modules를 관리하기가 어려워졌습니다.  </p>
<p>이 문제를 해결하기 위해 아래와 같은 방법들을 고려해 보았습니다.</p>
<h3 id="고려한-방법들">고려한 방법들</h3>
<ol>
<li>압축파일로 관리한다.</li>
<li>GitHub에 올려 Git Dependencies로 관리한다.</li>
<li>프라이빗 프록시 레지스트리를 구축한다.</li>
</ol>
<p>이 중 마지막 세번째 방법을 선택했습니다.<br>이유는 아래와 같습니다.</p>
<ol>
<li>압축파일로 관리하는 경우<ol>
<li>모듈을 추가하거나 삭제할 때마다 압축파일을 수정해야 하는 번거로움이 있습니다.  </li>
<li>특히 수정이 가해질수록 버전 관리가 어렵고, &#39;내가 설치한 모듈이 정확히 어느 버전인지&#39; 파악하기 어렵습니다.  </li>
</ol>
</li>
<li>GitHub에 올려 Git Dependencies로 관리하는 경우<ol>
<li>버전 관리가 상대적으로 명확하지만, 지속적으로 레포지토리와 브랜치를 관리해야 하는 추가 작업이 발생합니다.</li>
<li>무엇보다 프라이빗 레포에서 패키지를 설치하기 위해 인증 및 권한 관리 등 별도의 설정이 필요합니다.  </li>
</ol>
</li>
<li>프라이빗 프록시 레지스트리를 구축하는 경우<ol>
<li>프록시 서버에 대한 관리소요가 발생하긴 하지만, 한번 구축된 후에는 복잡한 관리 없이 패키지를 간편하게 배포할 수 있습니다.</li>
<li>기존에 사용하던 npm과 완전히 동일한 방식으로 사용하기에, 다른 개발자들이 쉽게 적응할 수 있습니다.</li>
<li>또한 사내망을 사용하므로, 프록시 서버에 대한 별도의 인증 및 권한 관리가 필요하지 않습니다.</li>
<li>더불어 각종 모듈에 대한 캐싱 효과도 있습니다.</li>
</ol>
</li>
</ol>
<h3 id="최종-선택-및-기대-효과">최종 선택 및 기대 효과</h3>
<p>이러한 이유로 세 번째 방법인 <strong>프라이빗 프록시 레지스트리 구축(Verdaccio)</strong>을 선택했습니다.  </p>
<p>이 선택을 통해 다음과 같은 기대 효과를 얻고자 합니다.  </p>
<ol>
<li>모듈 관리를 단순화하여 개발자의 생산성 향상</li>
<li>명확한 버전 관리와 손쉬운 배포 환경 구축</li>
<li>npm 생태계와 동일한 방식으로 운영하여 협업 과정에서의 혼란 최소화</li>
<li>모듈 캐싱을 통한 개발자 작업 효율 향상</li>
</ol>
<p>이 글에서는 Verdaccio를 사용하여 사내 프록시 레지스트리를 구축한 과정과 경험을 공유하고자 합니다.  </p>
<h2 id="1-프록시-레지스트리">1. 프록시 레지스트리</h2>
<h3 id="11-프록시-레지스트리란">1.1 프록시 레지스트리란?</h3>
<img src="https://velog.velcdn.com/images/hc-kang/post/510f3741-fcca-4ebb-b161-307f4c1a79ba/image.png" alt="npm 프록시 레지스트리" width="500">


<p>npm의 프록시 레지스트리는 우리가 흔히 사용하는 npm의 공식 레포와 개발자 사이에 위치하는 프록시입니다.<br>개발자의 패키지의 다운로드 요청을 먼저 받아 특정한 처리를 한 후, 필요시 공식 레포에 요청을 전달하는 방식으로 동작합니다.  </p>
<p>이러한 프록시는 보통 아래와 같은 목적으로 사용됩니다.</p>
<ol>
<li>프라이빗 패키지: 사내에서만 비공개로 사용하는 패키지를 관리하기 위해 사용됩니다.</li>
<li>캐싱: 자주 사용되는 패키지를 로컬에 저장하여 빠른 다운로드를 위해 사용됩니다.</li>
<li>가용성: 공식 레포가 다운되거나 네트워크 장애가 발생할 경우, 프록시 레지스트리를 통해 다운로드를 받을 수 있습니다.</li>
</ol>
<h2 id="2-verdaccio">2. Verdaccio</h2>
<h3 id="21-verdaccio란">2.1 Verdaccio란?</h3>
<p>Verdaccio는 Node.js로 작성된 경량 프록시 레지스트리입니다.<br>물론 오픈소스이고, 몇 개의 파일만으로도 간단히 설정하고 빠른 배포가 가능합니다.  </p>
<h3 id="22-verdaccio의-주요-특징">2.2 Verdaccio의 주요 특징</h3>
<ol>
<li>오픈소스: 무료로 사용 가능</li>
<li>간단한 설정: config.yaml 파일 하나로 쉽게 설정 가능</li>
<li>웹 UI 제공: 배포 후 웹 브라우저에서 쉽게 사용 가능</li>
<li>도커 이미지 제공: 도커 이미지를 통해 쉽게 배포 가능</li>
</ol>
<h3 id="23-설치-및-기본설정">2.3 설치 및 기본설정</h3>
<p>저는 개인적으로 로컬에 뭔가를 설치하는 것을 굉장히 꺼리는데요.<br>그래서 어지간한 것들은 도커 이미지를 통해 배포하는 것을 좋아합니다. 이래야 나중에 탈도 없고 제거도 깨끗하게 되니까요.<br>그중에서도 특히 도커 컴포즈를 통해 쉽게 배포하는 것을 선호합니다. 컴포즈 yaml만으로도 충분한 문서화가 가능하기 때문입니다.  </p>
<pre><code class="language-yaml">version: &#39;3.8&#39;

services:
  verdaccio:
    image: verdaccio/verdaccio
    container_name: verdaccio
    ports:
      - &quot;4873:4873&quot;
    volumes:
      - ./storage:/verdaccio/storage
      - ./conf:/verdaccio/conf
      - ./plugins:/verdaccio/plugins
    environment:
      - VERDACCIO_PORT=4873
    restart: unless-stopped</code></pre>
<p>위 설정은 도커 컴포즈를 통해 verdaccio 컨테이너를 배포하기 위한 설정입니다.<br>보시다시피 3개의 볼륨을 사용하는데요, 이를 위해 아래와 같은 디렉토리 구조가 필요합니다.</p>
<pre><code class="language-bash">.
├── conf/
│   ├── config.yaml
│   └── htpasswd
├── docker-compose.yaml
├── storage/
└── plugins/</code></pre>
<p>여기서 우리가 작성해줄 파일은 오직 <code>conf/config.yaml</code> 파일 뿐입니다.<br>(<del>당연히 docker-compose.yaml 파일은 있어야겠죠..?</del>)  
이 파일은 <code>verdaccio</code> 컨테이너의 설정 파일이며, 아래와 같은 내용을 포함합니다.</p>
<pre><code class="language-yaml"># 저장소 경로
storage: /verdaccio/storage

# 플러그인 경로
plugins:
  - /verdaccio/plugins/

# 인증 설정
auth:
  htpasswd:
    file: /verdaccio/conf/htpasswd
    max_users: 1000

# 업스트림 레지스트리 설정
uplinks:
  npmjs:
    url: https://registry.npmjs.org/
    timeout: 30s # 요청 타임아웃
    max_fails: 5 # 최대 실패 횟수
    cache: true # 캐시 사용 여부

# 웹 UI 관련 설정
web:
  title: &quot;사내 패키지 레지스트리&quot;
  logo: logo.png
  primary_color: &quot;#4b5e40&quot;
  darkMode: true
  enable_theme_selector: true

# 기본값은 10mb. 필요시 좀 더 큰 모듈 업로드가 필요한 경우 사용
max_body_size: 30mb

# 패키지 설정
packages:
  &#39;@SOME_PRIVATE/*&#39;:
    access: $all
    publish: $authenticated
    proxy: false # npm에 등록되지 않은 경우 false로 설정

  &#39;**&#39;:
    access: $all
    publish: $authenticated
    proxy: npmjs</code></pre>
<p>위 파일들을 생성하고 나면 아래와 같은 명령어로 배포할 수 있습니다.</p>
<pre><code class="language-bash">docker-compose up -d</code></pre>
<p>이렇게 하면 도커 컴포즈를 통해 verdaccio 컨테이너가 배포됩니다.<br>이제 웹 브라우저에서 <code>http://localhost:4873</code>으로 접속하면 아래와 같이 우리의 프록시 레지스트리를 확인할 수 있습니다.  </p>
<img src="https://velog.velcdn.com/images/hc-kang/post/c47532a0-47b3-424b-ac51-f789b66fbbc7/image.png" alt="verdaccio ui" width="600">

<p>이제 우리는 이 프록시 레지스트리를 사용하여 패키지를 관리할 수 있게 되었습니다.  </p>
<p>이후에는 사용자를 등록하고, 패키지를 배포하면 됩니다.  </p>
<h2 id="3-사용자-관리">3. 사용자 관리</h2>
<p>사용자 관리는 아래와 같은 명령어로 진행할 수 있습니다.</p>
<pre><code class="language-bash">$ npm adduser --registry http://localhost:4873

&gt; npm notice Log in on http://localhost:4873/
&gt; Username: test
&gt; Password: test
&gt; Email: test@test.com

&gt; Logged in on http://localhost:4873/.
&gt; ...</code></pre>
<p>이렇게 하면 사용자를 등록할 수 있습니다.<br>이후에는 아래와 같이 로그인을 진행할 수 있습니다.</p>
<pre><code class="language-bash">$ npm login --registry http://localhost:4873

&gt; Username: test
&gt; Password: test

&gt; Logged in on http://localhost:4873/.
&gt; ...</code></pre>
<h2 id="4-패키지-배포">4. 패키지 배포</h2>
<p>패키지 배포는 아래와 같은 명령어로 진행할 수 있습니다.</p>
<pre><code class="language-bash">npm publish --registry http://localhost:4873</code></pre>
<p>당연하게도 배포시에는 로그인이 필요한데요, 이게 귀찮으시다면 배포할 패키지의 루트에 <code>.npmrc</code> 파일을 통해 아래와 같이 토큰을 미리 설정해 둘 수 있습니다.</p>
<pre><code class="language-bash"># .npmrc
registry=http://localhost:4873
//localhost:4873/:_authToken=ZDJkOGFjMGYwZDE5ZDE0ZmE4YjQ4NTYwYmFjNTU0NDQ6M2UxYjI5OWVhOTQ5ZGY0MzA0</code></pre>
<p>이렇게 하면 패키지 배포시에 로그인 없이 배포가 가능합니다. </p>
<img src="https://velog.velcdn.com/images/hc-kang/post/2207692f-15f4-4cc3-93ff-d94516fad3c8/image.png" alt="published-package" width="500">

<h2 id="5-패키지-설치">5. 패키지 설치</h2>
<p>패키지 설치 방식은 기존 npm 방식과 완전히 동일합니다. 다만, 바라볼 레지스트리만 지정해주면 되는데요.  </p>
<pre><code class="language-bash">npm install --registry http://localhost:4873</code></pre>
<p>역시 이런 방법은 귀찮고 실수가 발생할 수 있기 때문에, 아래와 같이 프로젝트 루트에 <code>.npmrc</code> 파일을 통해 미리 지정해 둘 수 있습니다.</p>
<pre><code class="language-bash"># .npmrc
registry=http://localhost:4873</code></pre>
<p>이렇게 하면 패키지 설치시에 바라볼 레지스트리를 지정해주지 않아도 됩니다.  </p>
<h2 id="마치며">마치며</h2>
<p>오늘은 사내 프라이빗 패키지 사용을 위한 프록시 레지스트리를 구축하는 과정을 공유드렸습니다.<br>이 글을 통해 조금이나마 각자의 서비스 운영에 도움이 되셨으면 좋겠네요.<br>읽어주셔서 감사합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[객체 저장소를 알아보자!]]></title>
            <link>https://velog.io/@hc-kang/%EA%B0%9D%EC%B2%B4-%EC%A0%80%EC%9E%A5%EC%86%8C%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@hc-kang/%EA%B0%9D%EC%B2%B4-%EC%A0%80%EC%9E%A5%EC%86%8C%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sat, 15 Mar 2025 12:45:36 GMT</pubDate>
            <description><![CDATA[<p>요즘 회사 업무중 하나로, 기존에 사용하던 객체 저장소를 새로운 플랫폼의 저장소로 이전하는 작업을 하고있는데요.</p>
<p>이번 기회에 얼마전에 책너두를 진행하며 읽었던 <code>가상 면접 사례로 배우는 대규모 시스템 설계 기초 2</code>를 복습하며, 정리한 내용을 다시 한 번 곱씹어보고자 합니다.</p>
<p>이 책은 여러 시스템에 대해 다루고 있지만, 그중에서 9장에서는 S3와 같은 객체 저장소를 설계하는 과정을 다루고 있습니다.<br>사실 개인적으로는 이 책의 다른 부분은 기존 1권에 비해 다소 아쉬운 부분이 있었는데요, 이 부분 만큼은 이전 책에서 만큼이나 훌륭하다고 생각합니다.<br>(그렇다고 책이 별로인것은 아니니, 한번쯤 관심있으신 부분을 찾아보시면 좋을 것 같습니다.)</p>
<h1 id="저장소란-정확히-무엇일까">저장소란 정확히 무엇일까?</h1>
<p>저와 같은 개발자들이 말하는 저장소란, 말 그대로 데이터를 보관할 수 있는 장소입니다.<br>이 뜻에서 우리는 저장소의 목적과 특징을 유추 할 수 있는데요, 이는 아래와 같습니다.</p>
<ol>
<li>우리가 가진 정보를 일정한 기간동안 유실되거나 손상되지 않도록 보관할 수 있어야 한다.</li>
<li>필요할 때는 언제든지, 내 정보에 저장해서 이를 조회하고 활용 할 수 있어야 한다.</li>
<li>(일반적으로)새로운 정보를 지속적으로 추가할 수 있어야 한다.</li>
</ol>
<p>이러한 기본적인 저장소의 특징을 충족시키기 위해 다양한 유형, 형태의 저장소들이 개발되었는데요,<br>이들은 각각의 목적에 맞게 다양한 형태로 최적화되어 서비스를 하고 있습니다.  </p>
<h1 id="저장소의-종류">저장소의 종류</h1>
<p>여기까지 이야기하면 일반적인 경우에 개발자들이 생각하는 개념은 보통 <code>DB</code>인데요,<br>오늘은 아래와 같이, 이보다 조금 더 낮은 수준의 저장소들에 대해 이야기하고자 합니다.</p>
<ul>
<li>블록(block) 저장소</li>
<li>파일(file) 저장소</li>
<li>객체(object) 저장소</li>
</ul>
<blockquote>
<h3 id="❗dbmsdatabase-management-system">❗DBMS(Database Management System)</h3>
<p>우리가 일상적으로 사용하는 <code>DBMS</code>도 사실은 이러한 저장소 시스템의 한 형태라고 볼 수 있습니다.  </p>
<p>다만 <code>DBMS</code>의 경우 사용자가 물리적인 데이터를 직접 다루지 않는다는 점이 차이점이라고 할 수 있습니다.<br>사용자는 SQL 쿼리를 통해 직접 데이터를 조작한다고 느끼지만, 실제로는 <code>DBMS</code>가 제공하는 논리적 추상화 계층과 상호작용하고 있을 뿐이니까요.  </p>
<p><code>DBMS</code>는 테이블, 행, 열, 관계 등의 개념을 통해 복잡한 데이터의 저장 메커니즘을 숨기고, 사용자가 데이터의 물리적 저장 구조에 대해 이해할 필요 없이 비즈니스 관점에서 데이터를 쉽게 다룰 수 있도록 해줍니다.<br>덕분에 개발자는 저장소의 복잡한 내부 구현 대신 데이터 모델링과 비즈니스 로직에 집중할 수 있게 됩니다.  </p>
</blockquote>
<h2 id="블록-저장소">블록 저장소</h2>
<p>혹시 블록(block)이라는 개념에 대해 들어보신 적이 있으신가요?<br>아마 CS 공부를 하셨던 분들은 어렴풋하게나마 기억하실텐데요, 모든 데이터는 결국 비트(bit)의 집합으로 이루어져 있고 이는 메모리나 디스크에 저장된다는 것은 다들 알고 계실겁니다.
이처럼 저장되는 데이터들의 최소 단위가 블록(block)이라고 할 수 있습니다.  </p>
<blockquote>
<p>❗Block</p>
<p>블록은 데이터를 저장하는 최소 단위이고, 운영체제별로 차이가 있지만 일반적으로 윈도우나 맥, 리눅스 등에서는 4096바이트(4KB) 크기를 가집니다.  </p>
<img src="https://velog.velcdn.com/images/hc-kang/post/f0a074d8-b897-4b33-87bd-27fabe18a2ef/image.png" alt="block-test" width="300" />

<p>이로인해, 위와 같이 아주 작은 파일을 만들더라도 실제 물리적인 크기는 4KB를 가지는 것을 확인 할 수 있습니다.</p>
</blockquote>
<p>블록 저장소는 말 그대로 이 블록을 저장 할 수 있는 저장소를 의미합니다.<br>즉, 쉽게 말하자면 서버에 물리적으로 연결되어있는 디스크도 일종의 블록 저장소라고 할 수 있습니다.<br>우리가 말하는 저장소의 근본적인 형태라고 할 수 있겠네요.</p>
<p>만약 저장소 서비스로 이러한 블록 저장소 서비스를 사용한다면, 아래와 같은 특징들에 대해 충분히 고려가 필요합니다.</p>
<h3 id="블록-저장소의-특징">블록 저장소의 특징</h3>
<ul>
<li>빠른 속도와 높은 유연성<ul>
<li>데이터를 원시 블록(raw block)으로 다루기 때문에, 매우 빠르고 유연합니다.</li>
</ul>
</li>
<li>세밀한 제어가 가능함<ul>
<li>매우 저수준에서 데이터를 다루는만큼, 최적화의 여지가 많고 세밀한 제어가 가능합니다.</li>
</ul>
</li>
<li>높은 관리부담<ul>
<li>그만큼 사용자가 실제로 저장소를 관리하는 부담이 높아집니다.</li>
<li>데이터의 관리와 복구 등 여러 작업을 사용자가 직접 처리해야 합니다.</li>
</ul>
</li>
</ul>
<h2 id="파일-저장소">파일 저장소</h2>
<p>파일 저장소는 앞서 다룬 블록 저장소에 파일과 디렉토리라는 추상화 계층을 추가한 형태입니다.<br>이로인해 사용자는 블록 저장소보다 더 쉽게 데이터를 저장하고 관리할 수 있습니다.<br>우리가 일상적으로 사용하는 파일 시스템을 생각하시면 쉽게 이해가 되실 것입니다.  </p>
<h3 id="파일-저장소의-특징">파일 저장소의 특징</h3>
<ul>
<li>계층적 구조(hierarchical structure)<ul>
<li>파일과 폴더(디렉토리)라는 개념을 통해 데이터를 쉽게 구조화할 수 있습니다.</li>
<li>이를 통해 사용자는 좀 더 친숙하게 정보를 다룰 수 있습니다.</li>
</ul>
</li>
<li>관리의 용이성<ul>
<li>SMB(Server Message Block), NFS(Network File System) 등의 프로토콜을 통해 쉽게 파일 시스템을 마운트할 수 있습니다.</li>
<li>또한 개별 블록에 대한 포멧과 관리 등의 작업을 사용자가 고려 하지 않아도 됩니다.</li>
</ul>
</li>
</ul>
<h2 id="객체-저장소">객체 저장소</h2>
<p>객체 저장소는 조금 더 특별한 형태의 저장소입니다.<br>이 저장소는 데이터를 객체(object)라는 단위로 다루는데요, 이 객체는 실제 데이터와 메타데이터로 이루어져 있습니다.<br>현재 대부분의 클라우드에서 주력으로 제공하고, 수많은 개발자들이 사용하고 있는 저장소 형태입니다.<br>흔히 앞서 말한 <code>DBMS</code>이외에 다른 저장소라고 하면 가장 먼저 떠올리는 형태이죠.  </p>
<p>이러한 객체 저장소가 특별한 이유는, 아마 다들 어렴풋이 알고 계실텐데요, 혹시 객체 저장소에 저장된 데이터를 수정해보신 적 있으신가요?  </p>
<p>아마 없으실 겁니다. 객체 저장소는 일반적으로 데이터 수정 기능을 제공하지 않기 때문입니다.<br>이는 객체 저장소의 특징중 하나인데요, 이어서 계속 정리 해 보겠습니다.<br>가장 중요한점을 먼저 간단하게 말하자면, 객체 저장소는 일부 기능을 포기함으로서 확장성과 내구성에 더욱 초점을 두었다고 할 수 있습니다.  </p>
<h3 id="객체-저장소의-특징">객체 저장소의 특징</h3>
<ul>
<li>불변성<ul>
<li>객체 저장소는 데이터를 수정할 수 없습니다.</li>
<li>대신 새로운 객체를 생성하거나 덮어쓰는 버저닝(versioning) 기능을 제공합니다.</li>
</ul>
</li>
<li>확장성<ul>
<li>객체 저장소는 데이터를 비 계층적으로 저장합니다.</li>
<li>또한 데이터와 메타데이터를 분리하여 저장하기 때문에, 데이터의 확장성이 높습니다.</li>
</ul>
</li>
<li>내구성<ul>
<li>객체 저장소는 데이터를 여러 노드에 분산하여 저장/관리하기 쉽습니다.</li>
<li>이로인해 높은 내구성을 유지할 수 있습니다.</li>
</ul>
</li>
<li>비용 효율성<ul>
<li>위와 같은 특징들로 인해, 더 저렴한 비용으로 더 많은 데이터를 저장할 수 있습니다.</li>
</ul>
</li>
</ul>
<blockquote>
<p>❗ 어? 제 저장소는 폴더 구조를 가지고있는데 왜 비계층적이라고 하는거죠?</p>
<p>객체 저장소는 사용자의 편의를 위해 디렉토리 구조를 제공하지만, 실제로는 데이터를 비 계층적으로 저장합니다.<br>이로인해 실제로 저장소의 데이터 목록을 조회하면 이름이 <code>directory/</code> 와 같이 <code>/</code>로 끝나는 빈 객체들이 존재하고,<br>디렉토리 내부의 객체들은 <code>directory/object</code> 와 같이 이름이 붙게 됩니다.  </p>
<p>즉, 이는 일종의 <code>prefix</code> 형태로 저장되는 것이며, 이로인해 계층적인 구조를 가지는 것처럼 보이는 것입니다.  </p>
</blockquote>
<h1 id="각-저장소의-비교">각 저장소의 비교</h1>
<p>각 저장소를 그림으로 비교해보자면 아래와 같습니다.</p>
<img src="https://velog.velcdn.com/images/hc-kang/post/3cfd4de3-99c9-4a83-8007-8214a6f7488e/image.png" alt="9-1-traits-of-storages" width="600"/>

<img src="https://velog.velcdn.com/images/hc-kang/post/f8d606fd-1343-4712-9676-dbc36cde08f9/image.png" alt="9-1-type-of-storages" width="600"/>

<h1 id="왜-객체-저장소가-이렇게까지-주목받는걸까">왜 객체 저장소가 이렇게까지 주목받는걸까?</h1>
<p>앞서 말씀드렸듯, 대부분의 클라우드 서비스에서 주력으로 제공하는 저장소의 형태가 객체 저장소입니다.<br>실제로 많은 개발자분들의 사용 경험에서도 객체 저장소가 압도적이죠.<br>그렇다면 어떤 요소가 이렇게까지 객체 저장소의 인기를 높이는 데 기여하는 것일까요?  </p>
<h2 id="1-대규모-확장성의-필요성">1. 대규모 확장성의 필요성</h2>
<p>블록 저장소나 파일 저장소는 기본적으로 시스템에 데이터가 더 긴밀하게 연결되어있습니다.<br>각 블록이나 파일에 대한 정보를 직접 가지고 관리하는 방식은 물론, 계층적 구조로 인해 데이터의 확장성이 떨어지는 문제가 있습니다.<br>반면에 객체 저장소는 데이터와 메타데이터를 분리하여 저장하고, 데이터를 비 계층적/평면적으로 저장하므로 필요시 확장이 쉽습니다.  </p>
<h2 id="2-클라우드-네이티브의-등장">2. 클라우드 네이티브의 등장</h2>
<p>혹시 클라우드 네이티브라는 개념을 들어보신 적 있으신가요?<br>최근들어 MSA, 서버리스 등의 트렌드가 뜨겁게 떠오르고 있는데, 이러한 트렌드로 인해 많은 서비스들이 클라우드 플랫폼에 의존하고 있습니다.<br>그리고 이러한 클라우드 플랫폼에서는 아래와 같은 특성을 가진 컴포넌트가 사용에 유리한데요, 이러한 특성들을 갖춘 저장소가 바로 객체 저장소입니다.  </p>
<ul>
<li>HTTP, REST API를 통해 쉽게 접근 가능</li>
<li>무상태(stateless) 특성</li>
<li>쉬운 확장성</li>
</ul>
<h2 id="3-비용-효율성">3. 비용 효율성</h2>
<p>앞서 다루었듯, 객체 저장소는 몇 가지 기능을 포기함으로서 더 저렴한 비용으로 더 많은 데이터를 저장할 수 있습니다.<br>또한 이를 통해 사용자는 관리의 부담 없이도 저렴하게 더 많은 데이터를 저장할 수 있게 되었죠.  </p>
<h2 id="4-높은-데이터-내구성과-가용성">4. 높은 데이터 내구성과 가용성</h2>
<p>이또한 앞서 다룬 내용과 긴밀하게 연결되는데요,<br>불변성이라는 특성 덕분에 객체 저장소는 데이터를 여러 노드에 분산하여 저장/관리하기 쉽습니다. 정합성이나 락에 대해 고민할 필요가 없기 때문이죠.<br>이로인해 높은 내구성을 유지할 수 있습니다.  </p>
<h1 id="객체-저장소의-주요-개념과-용어">객체 저장소의 주요 개념과 용어</h1>
<p>객체 저장소의 주요 개념과 용어에 대해 알아보자면, 아래와 같습니다.</p>
<h2 id="1-객체object">1. 객체(object)</h2>
<p>객체는 저장의 기본 단위로, 두 가지 부분으로 구분됩니다.</p>
<h3 id="1-데이터페이로드">1. 데이터(페이로드)</h3>
<p>실제로 저장되는 콘텐츠. 파일이나 이미지, 동영상일 수도 있고, 문자열이나 바이너리 데이터일 수도 있습니다.</p>
<h3 id="2-메타데이터">2. 메타데이터</h3>
<p>객체의 속성을 설명하는 데이터. 이름, 타입, 크기, 생성일, 수정일 등의 정보를 포함합니다.<br>이외에도 사용자가 정의한 별도의 태그, 카테고리 등의 정보도 포함될 수 있습니다.</p>
<h2 id="2-버킷bucket">2. 버킷(bucket)</h2>
<p>버킷은 객체를 저장하는 물리적 공간입니다.<br>객체 저장소에서는 버킷을 생성하고, 이 버킷 내에 객체를 저장합니다.  </p>
<p>일반적으로 DNS 주소처럼 사용자가 쉽게 이해할 수 있는 이름을 가지고 있습니다.<br>또한 객체 저장소에서는 버킷을 생성하고, 이 버킷 내에 객체를 저장합니다.  </p>
<p>AWS의 S3에서는 전역적으로 고유한 이름을 가져야 하는데, 이는 아마 평면적인 구조에서 비롯된 특성이 아닐까 생각합니다 (근거는 없습니다.)</p>
<p>하지만 오라클 등의 다른 객체 스토리지에서는 네임스페이스를 통해 이런 번거로움(?)을 해결하고 있기는 하더라구요.</p>
<h2 id="3-uriuniform-resource-identifier-키key">3. URI(Uniform Resource Identifier), 키(key)</h2>
<p>URI, 혹은 키는 객체를 식별하는 고유한 식별자입니다.<br>일반적으로 아래와 같은 형태를 띕니다.</p>
<pre><code>https://bucket-name.domain.com/object-key</code></pre><p>이는 전세계에서 유일한, 우리가 지정한 객체 하나를 지칭하는 고유한 식별자입니다.  </p>
<h2 id="4-버전version">4. 버전(version)</h2>
<p>앞서 말했듯, 객체 저장소는 데이터의 수정 기능을 제공하지 않습니다. 오직 덮어쓰기 기능만을 제공하죠.<br>이로인해 사실 불편함이 큰데요, 누군가 데이터에 조작을 가했을때 이를 추적하기가 어렵습니다.<br>이런 문제를 해결하기 위해 객체 저장소에서는 버저닝 기능을 제공합니다.  </p>
<p>덕분에 데이터를 실수로 삭제하거나, 덮어썼을 때 복구도 가능하고, 이전 버전으로의 롤백도 가능합니다.  </p>
<h2 id="5-체크섬checksum">5. 체크섬(checksum)</h2>
<p>체크섬은 데이터의 무결성을 확인하기 위해 사용되는 값입니다.<br>객체 저장소에서는 데이터를 업로드할 때 체크섬을 계산하고, 이를 메타데이터에 저장합니다.<br>이후 데이터를 다운로드할 때 체크섬을 다시 계산하여 원본 데이터와 비교하여 무결성을 확인합니다.</p>
<h1 id="객체-저장소-시스템의-내부-아키텍처는-어떻게-생겼을까">객체 저장소 시스템의 내부 아키텍처는 어떻게 생겼을까?</h1>
<p>이러한 객체 저장소는 어떻게 구성되어 있을까요?<br>일반적으로 실제 서비스중인 객체 저장소에서 구조가 공개된 경우는 거의 없지만, 일반적인 구성 패턴을 알아보자면 아래와 같습니다.</p>
<h2 id="주요-컴포넌트">주요 컴포넌트</h2>
<img src="https://velog.velcdn.com/images/hc-kang/post/965961b6-573d-4243-9eab-df18d0c5445b/image.png" alt="9-4-blueprint" width="600"/>

<h3 id="1-api-서비스">1. API 서비스</h3>
<p>클라이언트의 요청을 직접 받아서 처리하는 서비스입니다.<br>주요 작업은 아래와 같고, 무상태로 설계되어 확장성이 높습니다.</p>
<ul>
<li>버킷 생성/삭제</li>
<li>객체 업로드/다운로드</li>
<li>객체 목록 조회</li>
<li>액세스 권한 수정</li>
</ul>
<h3 id="2-iam-서비스">2. IAM 서비스</h3>
<p>사용자의 인증과 권한 관리를 담당합니다.  </p>
<ul>
<li>인증(authentication): 사용자의 신원 확인</li>
<li>권한 관리(authorization): 사용자의 권한 확인</li>
<li>접근 제어(access control): 세밀한 접근 제어 관리</li>
</ul>
<h3 id="3-메타데이터-저장소">3. 메타데이터 저장소</h3>
<p>객체들의 메타데이터를 관리하는 분산 데이터베이스입니다.<br>보통 메타데이터는 아래와 같은 정보들이 저장됩니다.</p>
<ul>
<li>객체의 이름, 크기, 생성 시간</li>
<li>체크섬(checksum)</li>
<li>실제 데이터의 물리적 위치</li>
<li>버전 정보</li>
<li>기타 사용자 정의 메타데이터</li>
</ul>
<p>이러한 정보들을 바탕으로, 사용자의 요청에 따라 빠르게 객체의 데이터 목록을 조회할 수 있습니다.<br>또한 실제 데이터의 물리적인 위치와 분리되어있기 때문에, 데이터의 확장에 훨씬 유리한 구조를 가지고 있습니다.  </p>
<h3 id="4-데이터-저장소">4. 데이터 저장소</h3>
<p>객체의 실제 데이터(바이너리)를 저장하는 분산 시스템입니다.<br>이는 아래와 같은 하위 서비스들로 구성되어 있는데요,  </p>
<ul>
<li>데이터 라우팅 서비스<ul>
<li>데이터 노드 클러스터에 접근하기 위한 진입점을 제공합니다.</li>
<li>데이터 업로드시 배치 서비스를 호출하여 데이터가 저장될 노드를 선정합니다.</li>
</ul>
</li>
<li>배치 서비스: 노드들의 가상 클러스터 지도를 유지하고, 데이터가 실제로 어디에 저장되어야 하는지를 결정합니다.</li>
<li>데이터 노드: 데이터를 실제로 저장하는 물리적 서버와 디스크</li>
</ul>
<h1 id="마치며">마치며</h1>
<p>오늘은 저장소에 대한 개념과, 그중에서도 특히 많이 사용되는 객체 저장소에 대해 도서 <code>가상 면접 사례로 배우는 대규모 시스템 설계 기초 2</code>의 내용에 기반하여 정리를 해 보았습니다.  </p>
<p>이외에도 이 책에서는 더 풍성한 내용을 더 깊이있게 다루고 있어서, 매번 단순하게 사용하기만 해왔던 객체 저장소의 특징과, 왜 이렇게 구성되어 있는지, 그리고 실제 내부적으로 어떻게 동작하는지에 대해 이해 할 수 있었는데요.</p>
<p>당연하게도 여기서 이 챕터의 모든 내용을 다루는것은 상도덕에 맞지 않으니, 오늘의 글은 여기서 마치도록 하겠습니다.
더 깊은 내용이 궁금하시다면 이 책을 한 번 읽어보시는것도 큰 도움이 될 것 같습니다!</p>
<p>그러면 여기까지 읽어주셔서 감사하고, 다음에 다른 주제로 찾아뵙겠습니다~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[메시지 큐와 전달 보장 수준]]></title>
            <link>https://velog.io/@hc-kang/%EB%A9%94%EC%8B%9C%EC%A7%80-%ED%81%90%EC%99%80-%EC%A0%84%EB%8B%AC-%EB%B3%B4%EC%9E%A5-%EC%88%98%EC%A4%80</link>
            <guid>https://velog.io/@hc-kang/%EB%A9%94%EC%8B%9C%EC%A7%80-%ED%81%90%EC%99%80-%EC%A0%84%EB%8B%AC-%EB%B3%B4%EC%9E%A5-%EC%88%98%EC%A4%80</guid>
            <pubDate>Sun, 02 Mar 2025 07:01:53 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! 오늘은 메시지 큐에 대해 알아보고자 합니다.<br>최근 많은 기업들, 혹은 프로젝트들이 MSA를 도입하고 있고, 이로인해 메시지 큐의 사용을 피할 수 없는 상황이 많아지고 있는데요.  </p>
<p>이번 포스팅에서는 메시지 큐의 개념과, 이를 사용함에 있어 가장 많은 문제(?)를 일으키는 전달 보장에 대해 이야기해 보겠습니다.</p>
<h2 id="메시지-큐란-왜-필요하지">메시지 큐란? 왜 필요하지?</h2>
<p>메시지 큐를 사용하는데에는 여러 가지 이유가 있지만, 그중에서 가장 중요한(위대한) 이유는 바로 결합도를 끊어준다는 점이라고 생각합니다.  </p>
<p>뭐 당연한 소리라고 받아들이시는분들도 계시겠지만, 이를 처음 접하시는 분들을 위해 약간의 비유를 해보자면 우리 실생활에서의 택배나 배달을 생각하시면 쉽게 이해하실 수 있습니다.  </p>
<p>우리가 배민이나 쿠팡이츠에서 배달을 시켜 드실 때, 최근에는 전문 배달 기사분들이 배달을 해주고 계신데요.<br>하지만 불과 얼마 전만 해도 전문 배달기사라는 직종은 생각조차 못했고, 동네의 중국집들만 음식배달을 하고는 했잖아요?</p>
<img src="https://velog.velcdn.com/images/hc-kang/post/1cd33e07-c942-4e29-bd9c-e7c603c22c7a/image.png" alt="delivery" width="500" />

<p>다시 말해 당시에는 식당 주인이 배달을 위한 모든 준비와 운영을 직접 해야했어요.<br>필요에 따라 오토바이도 구매해야 하고, 그 오토바이를 운전하고 길도 잘 찾아다니는 배달원도 고용해야 했죠.  </p>
<p>이게 말이야 간단하지, 가끔씩 교통사고가 나기도 하고 종종 배달원이 도망가는 경우도 있는데다가 오토바이에 대한 유지관리 비용은 물론, 고객에게 들어온 불평불만까지도 식당에서 모두 처리해야했어요.<br>단 하나의 &#39;배달&#39; 이라는 기능을 위해 수많은 부수적인 업무들이 생겼던 거죠.</p>
<p>그런데 요즘은?<br>식당에서는(물론 많은 비용을 지불하고 있지만) 음식을 준비해서 문쪽 테이블에 올려두면 식당과는 전혀 관련이 없는 배달 기사분들이 음식을 가져다가 고객의 주소로 정확하고 빠르게 배달해 주시죠.</p>
<p>자, 이렇게 이야기 하고 보니 아마 메시지 큐를 써보지 않으신 분들이라도 이게 왜 중요한지 이해하셨을 것 같습니다.  </p>
<p>여기서 조금만 더 극단적인 예를 들어보자면, 이제는 <strong>배달원이 도망가는바람에 주방장이 음식 배달하러 갔다가, 교통사고가 나는바람에 음식점이 문을 닫는 경우는 (대체로) 없어졌다</strong>는 이야기죠.  </p>
<p>즉, (메시지 큐를 적당히 잘 썼다면)특정 컴포넌트데 문제가 발생해도 전체 시스템에 영향이 가는 경우는 없어졌다는 이야기죠.<br><del>(물론 손님없는 식당이 무턱대고 배달 서비스만 비싼곳 여기저기 다 쓰다가 기둥뿌리 뽑히는 안타까운 경우가 생긴것 같기도 합니다)</del></p>
<p>이외에도 아래와 같은 장점들도 있으나, 분량을 위해 간략하게만 이야기 하고 넘어가겠습니다.</p>
<ul>
<li><strong>확장성이 좋아진다.</strong><ul>
<li>이제는 장사가 잘되어 분점을 낼 때, 오토바이를 더 구매하고 배달원을 더 고용할 필요가 없습니다. 그냥 돈만 더 내면 됩니다!</li>
</ul>
</li>
<li><strong>유동성이 좋아진다.</strong><ul>
<li>가게에 손님이 아무리 몰려도 (이론상!)가게에 배달원이 부족해서 배달을 못하는 경우는 없습니다.</li>
</ul>
</li>
<li><strong>비동기 처리가 가능해진다.</strong><ul>
<li>(이건 좀 억지지만)주인이 직접 요리해서 배달까지 하는 것 보다는, 배달원을 둠으로서 업무의 효율성이 높아집니다.</li>
</ul>
</li>
</ul>
<p>이렇게 메시지 큐를 사용하면 여러 가지 장점이 있지만, 사실 이러한 모든 장점을 얻기 위해서는 분명 비용이 들어갑니다.<br>단순히 돈을 더내고 덜내고를 넘어서, 관리해야 할 다른 문제들도 생겼습니다.  </p>
<p>예를 들어, 이제는 식당 주인은 어떤 배달업체를 선정해야 하는지, 그 업체와의 계약은 어떻게 이루어지는지, 게다가 업체 안에서 우리 식당의 평이 나쁘지는 않은지 등등 알고 관리해야 할 것들이 많아졌습니다.<br>또한, 갑작스럽게 배달업체의 사정으로 인해 배달이 중지되거나 식당 주인이 처리 할 수 없는 문제가 발생하는 경우도 있겠죠.</p>
<p>이러한 현실처럼, 개발에서 메시지 큐를 도입하는 경우에도 마냥 좋은 일만 생기는 것은 아닙니다.<br>우리도 역시나 어떤 큐가 현재 상황에 가장 적절할지 고민해야 하고, 문제가 발생했을 때 어떻게 해결할지 고민해야 합니다.</p>
<p>그래서 이번 포스팅에서는 메시지 큐에서 자주 접하게 되는 문제인 전달 보장 수준에 대해 이야기해 보겠습니다.</p>
<h2 id="전달-보장이란">전달 보장이란?</h2>
<p>메시지 큐에서 가장 중요한 문제 중 하나는 메시지가 전달되는 것을 보장하는 것입니다.  </p>
<p>우리가 보낸 음식이 사라지면 안되잖아요?<br>물론 안타깝게도 여기서부터는 음식배달이라는 예시는 더이상 통하지 않겠지만 말이죠.</p>
<p>메시지 큐에서 전달보장(Delivery Guarantee)은 &#39;시스템이 메시지를 얼마나 안정적으로 전달할 수 있는가?&#39;를 의미합니다.<br>여기서는 크게 세 가지 수준으로 나눌 수 있습니다.  </p>
<ul>
<li>최대 한 번 전달(At Most Once)</li>
<li>최소 한 번 전달(At Least Once)</li>
<li>정확히 한 번 전달(Exactly Once)</li>
</ul>
<p>이들을 좀 더 쉽게, 간단하게(막말로)표현하자면 아래와 같습니다.</p>
<ul>
<li>가던가 말던가, 난 던지긴 했음.</li>
<li>일단 많이 던졌는데, 뭐라도 들어는 갔음.</li>
<li>기가 막히게 딱 던져서 들어갔음.</li>
</ul>
<p>이들을 엄밀하게 정의하려면 사실 수신확인(ack; acknowledgment)이라는 개념이 필요한데요.<br>수신확인은 말 그대로 메시지를 받은 컴포넌트가 메시지를 정상적으로 받았다는 것을 확인시켜주는 과정을 의미합니다.<del>(복명복창)</del>  </p>
<p>이제 아래에서 이들을 각각 좀 더 자세히 살펴보겠습니다.</p>
<blockquote>
<p>⚠️ 메시지 큐에서 수신확인과 처리 완료는 다른 개념입니다.  </p>
<p>수신확인은 메시지를 받았다는 것을 확인시켜주는 것일뿐, 처리 완료는 메시지를 정상적으로 처리했다는 것을 의미합니다.  </p>
<p>즉, 수신확인이 되었다고 해서 반드시 처리 완료가 된 것은 아닙니다!</p>
</blockquote>
<h3 id="최대-한-번-전달at-most-once-ack--0">최대 한 번 전달(At Most Once, ack = 0)</h3>
<p>정확하게 정의하자면, 최대 한 번 전달의 경우는 ack = 0인 경우입니다.  </p>
<p>즉, 메시지 큐는 컨슈머가 메시지를 받았는지 모릅니다. 다만 본인은 메시지를 보냈으니 메시지는 삭제되고, 만약 네트워크상에서 유실되었다면 메시지는 영영 사라지게 됩니다.</p>
<p>우리 생활에서 보자면 뭐 이런 경우가 있겠죠?</p>
<blockquote>
<p>(현관에서 나가기 직전에) 나 오늘 친구랑 저녁 먹고 들어올거야!</p>
</blockquote>
<p>안에있는 가족이 들었다면 좋겠지만, 사실 못들어도 큰일이 나는건 아니니 상관없습니다.</p>
<p>이를 구현하는 코드는 그만큼 간단하고, 고려할 사항도 적습니다.
전체 코드는 <a href="https://github.com/HC-kang/TIL/tree/main/etc/message-queue-delivery-guarantee/at-most-once">이곳에서 확인하실 수 있습니다.</a></p>
<pre><code class="language-ts">/** 
 * 컨슈머가 사용하는 큐 어댑터
 */
export type QueueAdapter = {
  // 메시지 처리. 수신확인 없음
  sendMessage(message: Message): Promise&lt;void&gt;;
  receiveMessage(): Promise&lt;Message | null&gt;;
};

/**
 * 큐로부터 메시지를 수신한다.
 * 이 과정에서 약 lossRate의 유실 확률을 가진다.
 */
async receiveMessage(lossRate = consumer.lossRate): Promise&lt;Message | null&gt; {
  // 수신 소요시간
  const receiveTimeAboutMS = this.jitter(consumer.receiveTimeAboutMS);
  await new Promise((resolve) =&gt; setTimeout(resolve, receiveTimeAboutMS));

  const message = await this.adapter.receiveMessage();
  if (!message) {
      console.log(`[${this.name}] No message received`);
      return null;
  }

  // 유실 확률을 모의합니다. 큐의 구현과는 무관하지만 &#39;네트워크상에서의 유실&#39;을 흉내내기 위한 코드입니다.
  if (Math.random() &lt; lossRate) {
      console.log(
      `[${this.name}] Message loss - ${message?.id} ${message?.content}`
      );
      this.messageCounter.incrementLostMessages();
      return null;
  }

return message;
}</code></pre>
<ul>
<li>수신확인같은 메서드는 없습니다. 그런건 쿨하지 못하니 관심 없습니다.</li>
<li>유실되어 메시지를 받지 못하더라도 알릴 수 있는 방법은 없습니다.</li>
</ul>
<p>결과적으로 이런 방식에서는 메시지 100건을 전송했을 때, 아래와 같은 결과를 얻을 수 있습니다.</p>
<p><strong>[Produced: 100, Consumed: 74, Lost: 22, Failed: 4, Total: 100]</strong></p>
<ul>
<li>총 100건의 메시지를 전송했지만, 컨슈머가 74건을 처리했습니다.</li>
<li>이 중 22건은 유실되었고, 4건은 실패했습니다.<ul>
<li>여기서는 재시도 로직은 생략했습니다.</li>
</ul>
</li>
<li>총 처리된 메시지는 74건이지만, 결과적으로 총 전송된 메시지는 100건이 맞습니다.</li>
</ul>
<h3 id="최소-한-번-전달at-least-once-ack--n">최소 한 번 전달(At Least Once, ack = n)</h3>
<p>이 경우에는 ack = n인 경우입니다.<br>즉, 메시지 큐는 컨슈머가 메시지를 받았는지 확인합니다.<br>만약 일정시간 내에 메시지를 받지 못했다면, 메시지를 다시 보냅니다.<br>이로인해 자연스럽게 메시지가 중복되어 전달될 수 있습니다.</p>
<p>이것도 실생활에 예를 들자면 뭐..</p>
<blockquote>
<p>(카톡으로) 올 때 메로나좀 사와<br>(인스타 DM으로) 메로나 사오는거 잊어버리지 마<br>(문자로) 메로나 사오라니까 왜 답이 없냐. 차단했냐?  </p>
</blockquote>
<p>이런 느낌이 아닐까 싶습니다.</p>
<p>이를 코드로 구현하자면, 확실히 이전보다는 복잡해집니다.
전체 코드는 <a href="https://github.com/HC-kang/TIL/tree/main/etc/message-queue-delivery-guarantee/at-least-once">이곳에서 확인하실 수 있습니다.</a></p>
<pre><code class="language-ts">/**
 * 컨슈머가 사용하는 큐 어댑터
 */
export type QueueAdapter = {
  // 메시지 처리
  sendMessage(message: Message): Promise&lt;void&gt;;
  receiveMessage(): Promise&lt;Message | null&gt;;

  // 이전과는 다르게 처리 확인 및 재처리를 위한 메서드가 추가되었습니다.
  acknowledgeMessage(message: Message): Promise&lt;void&gt;;
  getPendingMessages(): Promise&lt;Message[]&gt;;
  claimMessage(message: Message): Promise&lt;void&gt;;
};</code></pre>
<ul>
<li>이전과는 다르게 처리 확인 및 재처리를 위한 메서드가 추가되었습니다.</li>
<li>수신 확인은 스트림에서 처리되므로 별도의 메서드는 없습니다.</li>
</ul>
<p>실행한 결과는 아래와 같습니다.<br>다만, 최대 한 번 전달과는 다르게 검증이 조금 필요해서, 콘솔을 GPT에게 좀 부탁해보았습니다.</p>
<pre><code class="language-bash">Redis에 연결되었습니다.
소비자 그룹이 이미 존재합니다.
메시지가 전송되었습니다. 애플리케이션 ID: 3436c530-490f-40af-9245-d032414281b3, Stream ID: 1740667397832-0
[Producer] Produced message: green

...

메시지 확인됨 - 애플리케이션 ID: 8bec6613-eaa2-4bd1-9223-49959d232d3e, Stream ID: 1740667406420-0
[consumer-1] Checking for pending messages...
최종 메시지 처리 상태:
생산: 100, 소비: 112, 유실: 17, 실패: 7
Redis 연결이 종료되었습니다.

===== 메시지 처리 결과 분석 =====

[1] 생성되었지만 소비되지 않은 메시지:
  없음 (모든 생성된 메시지가 처리됨)

[2] 소비되었지만 생성되지 않은 메시지 (비정상):
  없음 (정상)

[3] 중복 처리된 메시지 (at-least-once 특성):
  총 12개 메시지가 중복 처리됨:
  - 메시지 ID: 3d2cf458-265b-4c67-9d4d-a0c6bd488721, 처리 횟수: 2회
  ...
  - 메시지 ID: c82e6dc0-deda-4131-bd2c-55b3b8f44aab, 처리 횟수: 2회

[4] 전체 처리 통계:
  - 생산된 메시지 수: 100개
  - 처리된 고유 메시지 수: 100개
  - 중복 처리된 메시지 수: 12개
  - 총 처리 횟수: 112회

[5] 최종 검증 결과:
  ✅ 성공: 모든 메시지가 최소 1회 이상 처리되었습니다.

===============================

Done</code></pre>
<p>여기서는 콘솔에 설명이 매우 잘 나와있으니, 말로는 설명할 필요가 없겠네요.  </p>
<h3 id="정확히-한-번-전달exactly-once-ack--1">정확히 한 번 전달(Exactly Once, ack = 1)</h3>
<p>이 경우에는 ack = 1인 경우입니다.<br>즉, 메시지 큐는 컨슈머가 메시지를 받았는지 확인합니다.<br>만약 일정시간 내에 메시지를 받지 못했다면, 메시지를 다시 보냅니다.  </p>
<p>하지만 이렇게되면 최소 한 번 전달처럼 중복되어 전달될 수 있는데요,<br>이를 해결하기 위해 보통 메시지 처리의 멱등성(idempotence)을 보장하는 방식을 사용합니다.  </p>
<p>사실 이건 실생활에서 정확한 예시를 들기가 좀 어렵네요.. 혹시 아이디어 있으시면 알려주세요!</p>
<p>전체 코드는 <a href="https://github.com/HC-kang/TIL/tree/main/etc/message-queue-delivery-guarantee/exactly-once">이곳에서 확인하실 수 있습니다.</a></p>
<p>이번에는 최소 한 번 전달에서 멱등성을 보장하는 방식으로 구현하였고, <code>Consumer</code> 클래스에서 이를 처리하는 부분만 추가되었습니다.</p>
<pre><code class="language-ts">/**
 * 컨슈머가 사용하는 큐 어댑터
 */
export type QueueAdapter = {
  // 메시지 처리
  sendMessage(message: Message): Promise&lt;void&gt;;
  receiveMessage(): Promise&lt;Message | null&gt;;
  acknowledgeMessage(message: Message): Promise&lt;void&gt;;
  getPendingMessages(): Promise&lt;Message[]&gt;;
  claimMessage(message: Message): Promise&lt;void&gt;;

  // 멱등성 처리를 위한 메서드가 추가되었습니다.
  isMessageProcessed(messageId: string): Promise&lt;boolean&gt;;
  markMessageAsProcessed(messageId: string): Promise&lt;void&gt;;
};

// 컨슈머에서 메시지를 처리하기 전에 아래와 같이 처리 여부를 확인합니다.
const isProcessed = await this.adapter.isMessageProcessed(message.id);

...

// 메시지가 처리되었다면 처리 기록을 남깁니다.
await this.adapter.markMessageAsProcessed(message.id);</code></pre>
<p>실행한 결과는 아래와 같습니다.</p>
<pre><code class="language-bash">Redis에 연결되었습니다.
소비자 그룹이 이미 존재합니다.
메시지가 전송되었습니다. 애플리케이션 ID: e833a747-04b1-4c20-af19-96875a499722, Stream ID: 1740897918361-0
[Producer] Produced message: green

...

메시지 확인됨 - 애플리케이션 ID: 2477f6eb-cb8b-4b4e-a010-208f57aeb0d6, Stream ID: 1740897925343-0
[Produced: 84, Consumed: 91, Lost: 13, Failed: 8, Total: 112]
[consumer-1] Checking for pending messages...
[consumer-2] Checking for pending messages...
[consumer-2] No pending messages found
[consumer-1] No pending messages found
최종 메시지 처리 상태:
생산: 100, 소비: 113, 유실: 15, 실패: 9
Redis 연결이 종료되었습니다.

===== 메시지 처리 결과 분석 =====

[1] 생성되었지만 소비되지 않은 메시지:
  없음 (모든 생성된 메시지가 처리됨)

[2] 소비되었지만 생성되지 않은 메시지 (비정상):
  없음 (정상)

[3] 중복 시도되었지만 처리되지 않은 메시지 (exactly-once 특성):
  총 13개 메시지가 중복 시도됨:
  - 메시지 ID: 7a70e2d9-8d47-43ef-adcf-a127292ad078, 처리 횟수: 2회
  ...
  - 메시지 ID: 1a96b4cb-d467-4234-9069-a51c7eb4d7c1, 처리 횟수: 2회

[4] 전체 처리 통계:
  - 생산된 메시지 수: 100개
  - 처리된 고유 메시지 수: 100개
  - 중복 시도되었지만 처리되지 않은 메시지 수: 13개

[5] 최종 검증 결과:
  ✅ 성공: 모든 메시지가 정확히 1회 처리되었습니다.

===============================

Done</code></pre>
<h2 id="정리">정리</h2>
<p>지금까지 메시지 큐의 개념과 세 가지 전달 보장 수준(최대 한 번, 최소 한 번, 정확히 한 번)에 대해 알아보았는데요.  </p>
<p>이제 위에서 알아보았던 세 가지 전달 보장 수준을 표로 간략하게 정리하고 마무리 하겠습니다.</p>
<table>
<thead>
<tr>
<th>전달 보장 수준</th>
<th>설명</th>
<th>ack</th>
<th>목적</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td>최대 한 번 전달</td>
<td>메시지를 전달하고 수신확인을 하지 않습니다.</td>
<td>0</td>
<td>메시지 유실이 허용되는 경우</td>
<td>중요하지 않은 정보 전달</td>
</tr>
<tr>
<td>최소 한 번 전달</td>
<td>메시지를 전달하고 수신확인을 합니다.</td>
<td>n</td>
<td>중복 처리가 허용되는 경우</td>
<td>이메일 등 꼭 필요한 정보 전달</td>
</tr>
<tr>
<td>정확히 한 번 전달</td>
<td>메시지를 전달하고 수신확인을 합니다.</td>
<td>1</td>
<td>메시지 유실이나 중복 처리가 허용되지 않는 경우</td>
<td>예금 이체, 거래 등</td>
</tr>
</tbody></table>
<p>여기까지 읽어주셔서 감사하고, 혹시 잘못된 부분이나 추가할 내용이 있으시다면 댓글로 알려주시면 감사하겠습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[40일차] | 대규모 시스템 설계 기초2 | 책너두]]></title>
            <link>https://velog.io/@hc-kang/40%EC%9D%BC%EC%B0%A8-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%882-%EC%B1%85%EB%84%88%EB%91%90</link>
            <guid>https://velog.io/@hc-kang/40%EC%9D%BC%EC%B0%A8-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%882-%EC%B1%85%EB%84%88%EB%91%90</guid>
            <pubDate>Sat, 22 Feb 2025 06:40:06 GMT</pubDate>
            <description><![CDATA[<h3 id="객체-버전">객체 버전</h3>
<ul>
<li>객체 버전은 버킷 안에 한 객체의 여러 버전을 보관하는 기능이다.<ul>
<li>이를 통해 이전 객체의 복구 등의 작업이 가능해진다.</li>
</ul>
</li>
</ul>
<img src="https://velog.velcdn.com/images/hc-kang/post/d991ccac-a094-468f-b760-8129eabd35e0/image.png" alt="9-22-object-versioning" width="600"/>

<ol>
<li>클라이언트가 HTTP로 PUT 요청을 보낸다.</li>
<li>API 서비스는 IAM 서비스로부터 사용자의 신원과 권한을 확인한다.</li>
<li>확인결과 이상이 없다면 데이터를 저장소로 업로드한다. 데이터 저장소는 이를 각각의 노드에 복제한 후 UUID를 반환한다.</li>
<li>API 서비스는 획득한 UUID를 바탕으로 메타데이터 저장소를 호출하여 메타데이터를 생성한다. 이 때 동일한 이름을 가진 기존 레코드가 있는지, 그리고 버저닝 기능이 활성화 되어있는지를 확인한다.</li>
<li>4번의 두 조건이 만족된다면 새로운 메타데이터를 생성한다. 이때 <code>bucket_id</code>, <code>object_name</code>은 같지만, <code>object_id</code>와 <code>object_version(TIMEUUID)</code>이 다른 새로운 레코드를 생성한다.</li>
</ol>
<ul>
<li>일반적으로 이런 데이터의 삭제는 데이터를 직접 삭제하지 않고 삭제 표시를 남기는(soft delete) 방식을 사용한다.<ul>
<li>이는 데이터 노드에서 데이터를 삭제하는 것이 아니라, 데이터 노드에 삭제 표시를 남기는 것이다.</li>
</ul>
</li>
</ul>
<h3 id="큰-파일의-업로드-성능-최적화">큰 파일의 업로드 성능 최적화</h3>
<ul>
<li>수 GB 이상의 큰 파일을 업로드하는 경우도 고려해야 한다.<ul>
<li>이 경우 데이터 전송에 오랜 시간이 걸리고, 그만큼 중단될 가능성도 높다.</li>
<li>따라서 다수의 작은 객체로 쪼갠 후 업로드하는 방식(multipart upload)을 사용한다.</li>
</ul>
</li>
</ul>
<img src="https://velog.velcdn.com/images/hc-kang/post/84d1a34e-758d-449f-af0d-c2e76f32a64c/image.png" alt="9-25-multipart-upload" width="500"/>

<ol>
<li>클라이언트가 대용량 파일을 업로드 요청을 보낸다.</li>
<li>데이터 저장소가 uploadID를 발급하고, 이를 클라이언트에 반환한다.</li>
<li>클라이언트가 파일을 작은 객체로 분할하여 업로드한다. 여기서는 1.6GB 파일을 8개로 분할하여 각 200MB씩 업로드한다고 가정하며, 각각의 파일을 2에서 얻은 uploadID를 포함하여 업로드한다.</li>
<li>조각 하나를 업로드 할 때 마다 데이터 저장소는 ETag를 클라이언트에 반환한다.</li>
<li>모든 조각이 업로드되면 클라이언트는 완료 요청을 보내며, 이때 uploadID와 각 조각의 ETag를 포함한다.</li>
<li>데이터 저장소는 이를 받아 각 조각을 복원하고 원본 파일을 재구성한다.</li>
</ol>
<ul>
<li>이 과정에서 실패하거나, 완성되더라도 개별 조각들이 삭제되지 않는다면 데이터 저장소에 계속 남아있을 것이다.</li>
<li>이를 방지하기 위해 데이터 저장소는 쓰레기 수집(garbage collection) 과정이 필요하다.</li>
</ul>
<h3 id="쓰레기-수집가비지-컬렉션">쓰레기 수집(가비지 컬렉션)</h3>
<ul>
<li><p>가비지 컬렉션은 더이상 사용되지 않을 데이터를 제거하여 저장 공간을 회수하는 절차이다.</p>
</li>
<li><p>이 설계에서는 대표적으로 아래와 같은 경우로 인해 가비지 컬렉션이 발생 할 수 있다.</p>
<ul>
<li>객체의 지연 삭제(lazy object deletion): 삭제 표시를 남긴 객체가 실제로 삭제되는 시점</li>
<li>버려진 데이터(orphaned data): 업로드 중 중단된 데이터</li>
<li>훼손된 데이터(corrupted data): 체크섬, ETag 검증 등이 실패한 데이터</li>
</ul>
</li>
<li><p>이러한 가비지 컬렉션은 매번 동작하지는 않는다.</p>
<ul>
<li>비용이 많이 들기 때문에 특정 주기에 따라 실행된다.</li>
</ul>
</li>
<li><p>가비지 컬렉션 과정에서 삭제뿐 아니라 데이터 저장소에 대한 정리 작업도 병행한다.</p>
<ul>
<li>삭제가 필요한 불필요 데이터를 삭제한 뒤 여러개의 객체를 하나의 객체로 합치는 작업도 포함된다.</li>
</ul>
</li>
</ul>
<h2 id="4단계-마무리">4단계: 마무리</h2>
<ul>
<li>생략</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[39일차] | 대규모 시스템 설계 기초2 | 책너두]]></title>
            <link>https://velog.io/@hc-kang/39%EC%9D%BC%EC%B0%A8-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%882-%EC%B1%85%EB%84%88%EB%91%90-hhuic0my</link>
            <guid>https://velog.io/@hc-kang/39%EC%9D%BC%EC%B0%A8-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%882-%EC%B1%85%EB%84%88%EB%91%90-hhuic0my</guid>
            <pubDate>Sat, 22 Feb 2025 05:39:12 GMT</pubDate>
            <description><![CDATA[<h3 id="메타데이터-데이터모델">메타데이터 데이터모델</h3>
<h4 id="스키마">스키마</h4>
<ul>
<li><p>메타데이터를 통해 아래와 같은 질의를 지원해야 한다.</p>
<ul>
<li>객체 이름으로 객체 ID 찾기</li>
<li>객체 이름을 기반으로 삽입, 또는 삭제 처리</li>
<li>같은 접두어를 갖는 버킷 내의 모든 객체 찾기</li>
</ul>
</li>
<li><p>이러한 요구사항을 만족하기 위한 메타데이터 테이블 구조는 다음과 같다:</p>
</li>
</ul>
<table>
<thead>
<tr>
<th>테이블명</th>
<th>컬럼</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Buckets</td>
<td>bucket_name</td>
<td>버킷의 이름</td>
</tr>
<tr>
<td></td>
<td>bucket_id(PK)</td>
<td>버킷의 고유 식별자</td>
</tr>
<tr>
<td></td>
<td>owner_id</td>
<td>버킷 소유자 ID</td>
</tr>
<tr>
<td></td>
<td>enable_versioning</td>
<td>버전 관리 활성화 여부</td>
</tr>
<tr>
<td>Objects</td>
<td>bucket_name</td>
<td>객체의 이름</td>
</tr>
<tr>
<td></td>
<td>object_name</td>
<td>객체의 이름</td>
</tr>
<tr>
<td></td>
<td>object_version</td>
<td>객체의 버전</td>
</tr>
<tr>
<td></td>
<td>object_id</td>
<td>객체의 고유 식별자</td>
</tr>
</tbody></table>
<h4 id="bucket-테이블의-규모-확장">bucket 테이블의 규모 확장</h4>
<ul>
<li><p>버킷 테이블의 규모 산정</p>
<ul>
<li>고객은 백만 명</li>
<li>고객당 평균 10개의 버킷을 보유</li>
<li>버킷의 레코드는 10kb라고 가정하면,</li>
<li>전체 크기는 (100만 * 10 * 10kb) = 10GB</li>
</ul>
</li>
<li><p>하나의 데이터베이스로 처리 가능한 크기이지만, 모든 읽기/쓰기를 감당하기에는 대역폭이 부족할 수 있음.</p>
</li>
<li><p>따라서 데이터베이스 사본을 활용하는 것이 적절함.</p>
</li>
</ul>
<h4 id="object-테이블의-규모-확장">object 테이블의 규모 확장</h4>
<ul>
<li>객체는 당연하게도 버킷에 비해 훨씬 많은 데이터를 저장함.</li>
<li>이를 단일 데이터베이스에 저장하는 것은 불가능함.</li>
<li>따라서 이는 샤딩(sharding)을 통해 분할하여 저장함.<ul>
<li><code>bucket_id</code>를 기준으로 샤딩한다면, 헤비 유저의 버킷이 핫스팟이 되므로 부하가 분산되지 않음.</li>
<li><code>object_id</code>를 기준으로 샤딩한다면, 핫스팟은 줄일 수 있지만 앞서 이야기한 1, 2번 질의를 효과적으로 처리하기 어렵다. 이는 두 질의는 URI를 기준으로 처리되기 때문임.</li>
<li>결과적으로 여기에서는 <code>bucket_id</code>와 <code>object_name</code>을 결합하여 샤딩에 활용할 수 있음.</li>
</ul>
</li>
</ul>
<blockquote>
<p>Q. URI를 기준으로 처리하는것과 <code>object_id</code>를 기준으로 샤딩하는것이 무슨 관계가 있지?</p>
<p>두 질의는 모두 URI를 기준으로 처리되기 때문에, 객체의 이름이 문자열로 제공됨.
따라서 이 문자열을 <code>object_id</code>(UUID)로 변환하기 위해 메타데이터가 필요한데, 이는 결국 모든 샤드를 탐색해야 알 수 있음.
반면 <code>bucket_id</code>와 <code>object_name</code>을 결합하여 샤딩하면, 이는 두 문자열을 결합하여 하나의 샤드를 특정할 수 있으므로 효율적임.</p>
</blockquote>
<h3 id="버킷-내-객체-목록-확인">버킷 내 객체 목록 확인</h3>
<ul>
<li>객체 저장소는 파일 시스템처럼 객체를 계층적으로 보관하지 않는다.</li>
<li>우리가 디렉토리로 알고있는것도 결국은 객체 이름의 프리픽스(prefix)일 뿐이다.</li>
</ul>
<h4 id="단일-데이터베이스-서버">단일 데이터베이스 서버</h4>
<ul>
<li><p>단일 데이터베이스 서버에서 객체를 조회하는 방식은 아래와 같다.</p>
<pre><code class="language-sql"># 사용자의 모든 버킷 조회
SELECT * FROM bucket WHERE owner_id={id}

# 버킷 내의 특정 디렉토리 내의 모든 객체 선택(접두어)
SELECT * FROM object
  WHERE bucket_id = &quot;123&quot; AND object_name LIKE `abc/%`</code></pre>
</li>
<li><p>이 작업에서 <code>abc</code>이하의 디렉토리 처리는 어플리케이션에서 처리한다.</p>
</li>
</ul>
<h4 id="분산-데이터베이스-서버">분산 데이터베이스 서버</h4>
<ul>
<li><p>분산 데이터베이스에서는 우리가 찾는 데이터가 어떤 샤드에 있는지 모른다.</p>
</li>
<li><p>가장 쉬운 방법은 모든 샤드에 검색을 하고, 이 결과를 종합하는 것이다.</p>
<pre><code class="language-sql"># 모든 샤드에 아래 쿼리 실행 및 결과 취합
SELECT * FROM object
  WHERE bucket_id = &quot;123&quot; AND object_name LIKE `a/b/%`</code></pre>
<ul>
<li>이 방식은 동작하지만 페이지네이션 적용이 어렵다.<ul>
<li>모든 데이터를 취합 후 재정렬 해야 하는 소요가 있다.</li>
<li>또한, 다음 페이지를 위한 커서 등을 사용할 때 각 샤드마다 기준이 달라질 수 있다.<ul>
<li>따라서 샤드 수 만큼 각각 개별적인 오프셋(커서)을 관리해야 한다.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[38일차] | 대규모 시스템 설계 기초2 | 책너두]]></title>
            <link>https://velog.io/@hc-kang/38%EC%9D%BC%EC%B0%A8-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%882-%EC%B1%85%EB%84%88%EB%91%90</link>
            <guid>https://velog.io/@hc-kang/38%EC%9D%BC%EC%B0%A8-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%882-%EC%B1%85%EB%84%88%EB%91%90</guid>
            <pubDate>Wed, 19 Feb 2025 23:28:13 GMT</pubDate>
            <description><![CDATA[<h3 id="데이터-내구성">데이터 내구성</h3>
<ul>
<li>데이터 내구성은 저장 시스템에 있어 핵심적인 요소이다.</li>
</ul>
<h4 id="하드웨어-장애와-장애-도메인">하드웨어 장애와 장애 도메인</h4>
<ul>
<li>아무리 뛰어난 설계를 가진 소프트웨어 제품이라도, 하드웨어 장애는 피할 수 없다.</li>
<li>일반적으로 하드 디스크의 연간 장애율은 0.81%라고 한다. - <a href="https://www.backblaze.com/blog/cloud-storage-durability/">출처</a><ul>
<li>이를 기반으로 6 nines(99.9999%)의 내구성을 만족하기 위해서는 최소 3중 복사가 필요하다.<ul>
<li>0.81 * 0.81 * 0.81 = 0.999999..</li>
</ul>
</li>
</ul>
</li>
<li>인프라를 공유하지 않는 완전한 분리를 위해서는 가용 영역(AZ)를 분리해야 한다.<ul>
<li>가용 영역(가용성 구역, Availability Zone)은 전원, 시설 등 모든 서버의 구성요소(인프라)가 독립적으로 구성된 영역이다.</li>
</ul>
</li>
</ul>
<h4 id="소거-코드">소거 코드</h4>
<ul>
<li><p>소거 코드(삭제 코드; 이레이저 코딩; erasure coding)는</p>
<ul>
<li>데이터를 여러 블록으로 분할하고, 각 블록을 여러 디스크에 복사하여 데이터를 보호한다.</li>
<li>이렇게 하면 데이터 노드 중 일부가 장애가 발생하더라도 데이터를 복구할 수 있다.</li>
</ul>
</li>
<li><p>소거 코드의 원리</p>
<ul>
<li><img src="https://velog.velcdn.com/images/hc-kang/post/60f3c17d-cb2f-4202-ae26-597080d27a58/image.png" alt="9-15-data-recovery-using-erasure-coding" width="600"/>

<ol>
<li>데이터를 네 개의 단위 크기로 분할한다.</li>
<li>수학 공식을 사용하여 패리티 블록(<code>p1</code>, <code>p2</code>)를 계산한다.</li>
<li>데이터 <code>d3</code>, <code>d4</code>가 소실되었다고 가정하면,</li>
<li>남은 값 <code>d1</code>, <code>d2</code>, <code>p1</code>, <code>p2</code>를 사용하여 데이터 <code>d3</code>, <code>d4</code>를 복구 할 수 있다.</li>
</ol>
</li>
</ul>
</li>
<li><p>소거 코드의 특징과 일반적인 다중화 방법과의 비교</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>다중화</th>
<th>소거 코드</th>
</tr>
</thead>
<tbody><tr>
<td>내구성</td>
<td>99.9999%(6nines, 3중 복제)</td>
<td>99.999999999%(11nines, 8+4 소거코드)</td>
</tr>
<tr>
<td>저장공간</td>
<td>200% 오버헤드</td>
<td>50% 오버헤드</td>
</tr>
<tr>
<td>계산 자원</td>
<td>계산 필요 없음</td>
<td>계산 필요</td>
</tr>
<tr>
<td>쓰기 성능</td>
<td>계산 필요 없음</td>
<td>계산 필요. 응답속도 저하</td>
</tr>
<tr>
<td>읽기 성능</td>
<td>계산 필요 없음</td>
<td>계산 필요. 최대 8개의 노드에서 데이터를 가져와야 함. 응답속도 저하</td>
</tr>
</tbody></table>
</li>
</ul>
<h4 id="정확성-검증">정확성 검증</h4>
<ul>
<li>디스크 뿐만 아니라 메모리의 데이터가 손상되는 경우도 발생함.</li>
<li>이를 위해 체크섬(checksum)을 사용하여 데이터의 정확성을 검증함.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[37일차] | 대규모 시스템 설계 기초2 | 책너두]]></title>
            <link>https://velog.io/@hc-kang/37%EC%9D%BC%EC%B0%A8-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%882-%EC%B1%85%EB%84%88%EB%91%90</link>
            <guid>https://velog.io/@hc-kang/37%EC%9D%BC%EC%B0%A8-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%882-%EC%B1%85%EB%84%88%EB%91%90</guid>
            <pubDate>Mon, 17 Feb 2025 23:56:07 GMT</pubDate>
            <description><![CDATA[<h2 id="3단계-상세-설계">3단계: 상세 설계</h2>
<ul>
<li>주요 주제<ul>
<li>데이터 저장소</li>
<li>메타데이터 데이터 모델</li>
<li>버킷 내의 객체 목록 확인</li>
<li>객체 버전</li>
<li>큰 파일의 업로드 성능 최적화</li>
<li>쓰레기 수집(garbage collection)</li>
</ul>
</li>
</ul>
<h3 id="데이터-저장소">데이터 저장소</h3>
<ul>
<li>데이터 저장소는 실제 바이너리 데이터를 저장하는 곳임.</li>
<li>API 서비스와 연계하여 객체 업로드/다운로드 요청을 처리한다.</li>
</ul>
<h4 id="데이터-저장소의-개략적-설계">데이터 저장소의 개략적 설계</h4>
<img src="https://velog.velcdn.com/images/hc-kang/post/2f9ab3a9-cd23-41ff-980e-127cee1bdfde/image.png" alt="9-8-data-storage-componenet" width="600"/>

<h5 id="데이터-라우팅-서비스">데이터 라우팅 서비스</h5>
<ul>
<li><p>데이터 노드 클러스터에 접근하기위한 REST, 또는 gRPC 인터페이스를 제공한다.</p>
</li>
<li><p>무상태 서비스로 확장성을 고려한다.</p>
</li>
<li><p>배치 서비스를 호출하여 데이터를 저장할 노드를 선정한다.</p>
</li>
<li><p><strong>배치 서비스</strong></p>
<ul>
<li><p>데잍를 데이터 노드 클러스터 중 어느 노드에 저장할지 결정한다.</p>
</li>
<li><p>내부적으로 가상 클러스터 지도를 유지하며, 이를 바탕으로 원본과 사본이 다른 노드에 저장되도록 한다.</p>
<ul>
<li><img src="https://velog.velcdn.com/images/hc-kang/post/a014dcce-db96-4815-a0a1-22b4451e9d46/image.png" alt="9-9-virtual-cluster-map" width="600"/>
</li>
</ul>
</li>
<li><p>모든 데이터 노드의 하트비트를 확인하며, 팩서스(Paxos), 래프트(Raft) 등의 분산 합의 알고리즘을 사용하여 데이터 노드 클러스터의 리더를 선출한다.</p>
</li>
<li><p>새로운 노드가 추가되면, 이를 가상 클러스터 지도에 추가하고 아래 정보를 반환한다.</p>
<ul>
<li>추가된 데이터 노드의 고유 식별자</li>
<li>가상 클러스터 지도</li>
<li>데이터 사본을 보관할 위치</li>
</ul>
</li>
</ul>
</li>
</ul>
<blockquote>
<p>❗ Paxos, Raft 알고리즘</p>
<p>분산 합의 알고리즘은 분산 시스템에서 여러 노드가 일관된 상태를 유지하고 데이터를 동기화하기 위해 사용되는 알고리즘임.</p>
<p>두 알고리즘의 특징은 다음과 같다.</p>
<ul>
<li><p>Paxos</p>
<ul>
<li>역할: Proposer, Acceptor, Learner (노드가 다중 역할 가능)</li>
<li>과정: 2단계 커밋 (Prepare → Accept)</li>
<li>특징: 이론적 완성도 높음, 구현 복잡, 최적화 가능성 많음</li>
<li>사용: Google Chubby, Microsoft Azure</li>
</ul>
</li>
<li><p>Raft</p>
<ul>
<li>역할: Leader, Follower, Candidate (한 노드 = 한 역할)</li>
<li>과정: 리더 선출 → 로그 복제 (임기 기반)</li>
<li>특징: 이해/구현 쉬움, 강한 리더십, 단순한 설계</li>
<li>사용: etcd, Consul</li>
</ul>
</li>
</ul>
</blockquote>
<ul>
<li><strong>데이터 노드</strong><ul>
<li>실제 데이터가 보관되는 곳.</li>
<li>다수의 노드에 데이터를 복제하여 안정성과 내구성을 보증(다중화 그룹; replication group)</li>
<li>각 노드에 서비스 데몬(service daemon)이 실행되어 하트비트를 보내며, 아래와 같은 정보를 함께 전달한다.<ul>
<li>해당 노드에 부착된 디스크(HDD/SSD)의 수</li>
<li>각 드라이브에 저장된 데이터의 양</li>
</ul>
</li>
<li>새로운 데이터 노드가 추가되면</li>
</ul>
</li>
</ul>
<h4 id="데이터-저장-흐름">데이터 저장 흐름</h4>
<img src="https://velog.velcdn.com/images/hc-kang/post/a4932346-650b-4546-96ff-aa4737460191/image.png" alt="9-10-persistant-data-storage-flow" width="600"/>

<ol>
<li>API 서비스가 객체 데이터를 데이터 저장소로 포워딩</li>
<li>데이터 라우팅 서비스는 객체에 UUID를 할당하고, 배치서비스를 통해 데이터를 보관할 노드를 결정</li>
<li>데이터 라우팅 서비스가 UUID와 데이터를 주 데이터 노드에게 전달</li>
<li>주 데이터 노드는 수신한 데이터를 저장하고, 두 개의 부 데이터 노드에 다중화. 완료 후 데이터 라우팅 서비스에 알림</li>
<li>객체의 UUID를 API 서비스에 반환</li>
</ol>
<img src="https://velog.velcdn.com/images/hc-kang/post/39a11bce-791d-4122-b43c-dbbb73cbbec1/image.png" alt="9-11-data-consistency-vs-latency-tradeoff" width="600"/>

<h5 id="데이터는-어떻게-저장되는가">데이터는 어떻게 저장되는가</h5>
<ul>
<li><p>가장 단순한 방안은 객체 데이터 각각을 파일로 저장하는 것임.</p>
<ul>
<li>이 경우 파일 시스템의 특성상 낭비되는 데이터 블록(일반적으로 4kb)의 수가 늘어남</li>
<li>또한 아이노드(inode) 수가 한계를 초과할 수 있음<ul>
<li>일반적인 경우 아이노드는 디스크 초기화 순간에 결정됨</li>
</ul>
</li>
</ul>
</li>
<li><p>따라서 작은 객체들을 하나의 큰 파일(수 GB)로 모아서 저장</p>
<ul>
<li>이 방법은 같은 파일에 여러 객체를 써야하므로, 순차쓰기가 보장되어야 함.</li>
<li>이렇게 객체 소재 확인을 위해 아래의 세 가지 정보가 필요함.<ul>
<li>객체의 소재 위치(파일 이름)</li>
<li>객체의 크기</li>
<li>객체의 길이</li>
</ul>
</li>
<li>이 정보를 관리하기위해 각 노드에서 SQLite 등의 관계형 데이터베이스를 사용할 수 있음.</li>
</ul>
</li>
</ul>
<img src="https://velog.velcdn.com/images/hc-kang/post/ef598cea-779e-40b9-a6cf-79749b7b866f/image.png" alt="9-13-data-storage-flow" width="600"/>

<ol>
<li>API 서비스는 데이터를 데이터 노드 서비스로 전달</li>
<li>데이터 노드 서비스는 데이터를 파일의 뒤쪽에 append</li>
<li>해당 객체에 대한 새로운 레코드를 관계형 DB 내에 row로 추가</li>
<li>데이터 노드 서비스는 API 서비스에 객체의 UUID를 반환</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[36일차] | 대규모 시스템 설계 기초2 | 책너두]]></title>
            <link>https://velog.io/@hc-kang/36%EC%9D%BC%EC%B0%A8-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%882-%EC%B1%85%EB%84%88%EB%91%90</link>
            <guid>https://velog.io/@hc-kang/36%EC%9D%BC%EC%B0%A8-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%882-%EC%B1%85%EB%84%88%EB%91%90</guid>
            <pubDate>Sun, 16 Feb 2025 23:49:01 GMT</pubDate>
            <description><![CDATA[<h2 id="2단계-개략적-설계안-제시-및-동의-구하기">2단계: 개략적 설계안 제시 및 동의 구하기</h2>
<ul>
<li><p>알아두어야 할 객체 저장소의 특성</p>
<ul>
<li><p>객체 불변성(object immutability): </p>
<ul>
<li>객체 저장소에 저장된 객체는 수정할 수 없다.</li>
<li>오로지 완전한 대체만 가능하다.</li>
</ul>
</li>
<li><p>키-값 저장소(key-value store): 객체가 저장될 때 특정 URI를 부여한다.</p>
</li>
<li><p>저장은 1회, 읽기는 여러 번: 객체 저장소에 대한 요청의 95%는 읽기이다.</p>
<blockquote>
<p>❗ 객체 저장소의 읽기, 쓰기 비율</p>
<p>생각했던것보다 쓰기 비율이 높다(5%).
5%라 하면, 1번 쓴 객체를 20번밖에 읽지 않는다는게 아닌가?</p>
</blockquote>
</li>
</ul>
</li>
</ul>
<h3 id="개략적-설계안">개략적 설계안</h3>
<img src="https://velog.velcdn.com/images/hc-kang/post/2946ab4e-cfda-4735-85e3-9d0bd7a29665/image.png" alt="9-4-blueprint" width="500"/>

<ul>
<li>주요 컴포넌트<ul>
<li>API 서비스: 무상태 서비스로 확장성 고려</li>
<li>IAM 서비스<ul>
<li>인증(authentication)</li>
<li>권한 부여(authorization)</li>
<li>접근 제어(access control)</li>
</ul>
</li>
<li>메타데이터 저장소: 객체의 메타데이터만 별도로 저장</li>
<li>데이터 저장소: 객체의 바이너리 데이터만 별도로 저장</li>
</ul>
</li>
</ul>
<h3 id="객체-업로드">객체 업로드</h3>
<img src="https://velog.velcdn.com/images/hc-kang/post/32b27872-6336-4eaf-b0c5-1ceecd09163b/image.png" alt="9-5-object-upload" width="500"/>

<ol>
<li>버킷 생성(bucket-to-share)</li>
<li>IAM 서비스로부터 신원 확인</li>
<li>버킷 메타데이터 생성</li>
<li>객체(script.txt) 업로드 요청</li>
<li>IAM 서비스로부터 권한 확인</li>
<li>객체 업로드</li>
<li>객체 메타데이터 업로드</li>
</ol>
<blockquote>
<p>Q. 객체 업로드와 메타데이터 업로드의 순서</p>
<p>메타데이터 업로드가 객체 업로드 이전에 이루어지는 이유는 무엇인가?</p>
<ol>
<li>데이터 정합성 보장
바이너리 객체가 먼저 성공적으로 저장되면, 실제 데이터가 이미 안전하게 보관된 상태임
메타데이터 저장 중 오류가 발생하더라도 실제 데이터는 이미 저장되어 있어 데이터 손실 위험이 적음</li>
<li>롤백 처리의 용이성
바이너리 저장 후 메타데이터 저장에 실패한 경우: 바이너리 데이터만 삭제하면 됨
메타데이터 저장 후 바이너리 저장에 실패한 경우: 메타데이터가 가리키는 실제 데이터가 없는 상태가 되어 데이터 정합성이 깨질 수 있음</li>
<li>성능 최적화
큰 바이너리 데이터의 전송과 저장에는 더 많은 시간이 소요됨
바이너리를 먼저 저장함으로써, 실제 데이터 전송 중 발생할 수 있는 네트워크 지연이나 오류에 더 효과적으로 대응 가능</li>
</ol>
</blockquote>
<h3 id="객체-다운로드">객체 다운로드</h3>
<img src="https://velog.velcdn.com/images/hc-kang/post/371ada76-3008-4fd5-8634-cd2f0ccff0c8/image.png" alt="9-6-object-download" width="500"/>

<ol>
<li><code>bucket-to-share/script.txt</code> 요청</li>
<li>IAM 서비스로부터 READ 권한 확인</li>
<li>메타데이터 조회 및 UUID 확인</li>
<li>UUID를 바탕으로 객체의 바이너리 데이터 조회가</li>
<li>데이터 전송</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[10년 전 노트 다시 들여다 보기. 그때는 내가 개발을 할 줄 몰랐지]]></title>
            <link>https://velog.io/@hc-kang/10%EB%85%84-%EC%A0%84-%EB%85%B8%ED%8A%B8-%EB%8B%A4%EC%8B%9C-%EB%93%A4%EC%97%AC%EB%8B%A4-%EB%B3%B4%EA%B8%B0.-%EA%B7%B8%EB%95%8C%EB%8A%94-%EB%82%B4%EA%B0%80-%EA%B0%9C%EB%B0%9C%EC%9D%84-%ED%95%A0-%EC%A4%84-%EB%AA%B0%EB%9E%90%EC%A7%80</link>
            <guid>https://velog.io/@hc-kang/10%EB%85%84-%EC%A0%84-%EB%85%B8%ED%8A%B8-%EB%8B%A4%EC%8B%9C-%EB%93%A4%EC%97%AC%EB%8B%A4-%EB%B3%B4%EA%B8%B0.-%EA%B7%B8%EB%95%8C%EB%8A%94-%EB%82%B4%EA%B0%80-%EA%B0%9C%EB%B0%9C%EC%9D%84-%ED%95%A0-%EC%A4%84-%EB%AA%B0%EB%9E%90%EC%A7%80</guid>
            <pubDate>Sun, 16 Feb 2025 05:53:30 GMT</pubDate>
            <description><![CDATA[<p>오늘은 개발과는 다소 관련없는 글을 들고 왔습니다.</p>
<p>정말 오래전부터 노트(바인더)를 쓰는 것을 좋아했는데요, 개발자가 된 뒤로는 아날로그 노트를 거의 쓰지 않게 되어 아쉽기도 합니다.</p>
<p>그래도 최근에 다시 아이패드로 예전 다이어리를 마이그레이션 하면서, 예전에 정리했던 글귀들을 하나씩 곱씹어 보았는데요
그 중 지금도 와닿는 몇 가지를 정리해봤습니다.</p>
<p>거의 10년 전에 적은 글을 수년간 몇번이고 옮겨 적었던 기록이다 보니 조금씩 원문과 다른 문구가 되었을 수도 있으니 참고 바랍니다!</p>
<h2 id="1-지식-없는-훈련은-맹목적이-되고-훈련-없는-지식은-쓸모가-없다---켈리-맥고니걸">1. 지식 없는 훈련은 맹목적이 되고, 훈련 없는 지식은 쓸모가 없다. - 켈리 맥고니걸</h2>
<ul>
<li>말 그대로, 맨 땅에 헤딩하는 훈련은 맹목적이다. 이는 향상에 도움도 되지 않고, 오히려 부상 등 상황의 악화를 초래하기도 한다.</li>
<li>반면 지식은 있지만 그 내용대로 실천하지 않는다면, 즉 훈련이 없는 지식이라면 그 지식은 전혀 쓸모가 없다.</li>
<li>그렇기에 우리는 훈련에 앞서 적당한 지식을 반드시 갖추어야 하며, 추가로 획득되는 지식 등을 즉각적으로 훈련에 적용하여야 한다.</li>
<li>개발자로서의 삶에서도 마찬가지인 것 같다.</li>
<li>이론적인 지식이 있더라도 활용하지 않는다면 의미가 없고, 지식 없이 무작정 작성된 코드는 독이 된다.</li>
<li>이 둘은 상호 보완적이고 서로 필수 불가결한 관계이다.</li>
</ul>
<h2 id="2-aim-small-miss-small---아메리칸-스나이퍼">2. Aim Small, Miss Small - 아메리칸 스나이퍼</h2>
<ul>
<li>절대로 목표를 작게 잡으면 실망이 적다는 뜻이 아니다.</li>
<li>2015년 7월 하계 훈련, 사격장에서 이 글귀의 의미가 확실히 다가왔다.</li>
<li>&#39;아메리칸 스나이퍼&#39;라는 영화 상에서는 더 조그마한 물체를 조준해야 그만큼 덜 빗나간다는 의미로 주인공이 언급한 대사이다.</li>
<li>하지만 이는 단순히 사격술에만 국한된 내용이 아니다.</li>
<li>우리가 목표를 설정하고, 이를 수행하는 과정에서, 목표를 구체적으로 설정할수록 우리가 엉뚱한 타겟을 맞출 확률이 줄어든다.</li>
<li>즉, 모든 경우에서 효율성을 높이려면 목표를 구체적으로 설정해야 한다.</li>
</ul>
<h2 id="3-당연함을-의심하고-권위에-도전하는-자세가-지금껏-학문의-세계에서-기술-혁신을-만들었다---야마구치-마유">3. &#39;당연함&#39;을 의심하고 &#39;권위&#39;에 도전하는 자세가 지금껏 학문의 세계에서 기술 혁신을 만들었다. - 야마구치 마유</h2>
<ul>
<li>익숙치 않은 곳에 처음 갔을 때가 기회이고, 그렇지 않다면 새롭게 보는 법을 터득하고, 여의치 않다면 다른 사람을 빌어서라도 보아라.</li>
<li>이 세상에 당연한 것은 없다. 항상 당연하다는 것을 의심해라.</li>
<li>또한 권위에 도전하는 용기도 필요하다.</li>
<li>다수, 혹은 권위자라고 해서 그들이 항상 옳은 것은 아니다.<ul>
<li>필요하다면 일종의 불순분자가 되거나, 하나쯤 장만해라.</li>
</ul>
</li>
<li>사사건건 의심하고 도전하며 &#39;Why?&#39;를 가슴에 품고 있는 사람이 되어라.</li>
</ul>
<h2 id="4-god-is-in-the-details">4. God is in the details</h2>
<ul>
<li>신은 사소한 것에 깃든다.</li>
<li>평소 나는 이 이야기에 잘 공감을 못했다.<ul>
<li>나는 평소 일을 우선 대충, 빠르게 처리한 후 이터레이션을 돌리는 방식으로 진행하는 것을 선호했다.</li>
<li>물론, 신을 믿지 않는 것도 맞기는 하다..</li>
</ul>
</li>
<li>하지만 신이 없더라도, 적어도 사람들은 확실히 사소한 것에 감동한다.</li>
</ul>
<h2 id="5-인생은-가까이서-보면-비극이지만-멀리서-보면-희극이다---찰리-채플린">5. 인생은 가까이서 보면 비극이지만 멀리서 보면 희극이다. - 찰리 채플린</h2>
<ul>
<li>솔직히 말해서, 이 말뜻이 확 와닿지는 않는다.<ul>
<li>지금은 슬프더라도, 나중에 돌아보면 결국에는 기쁠 것이라는 뜻일까?</li>
</ul>
</li>
<li>검색해 보니 앤트맨에 대한 이야기가 있더라.<ul>
<li>앤트맨의 마지막 장면은 앤트맨의 입장에서 정말 치열한 액션이었다.</li>
<li>하지만 보통 사람의 시선에서 한 걸음만 떨어져서 보아도 단순히 장난감들이 움직이는 정도이다.</li>
</ul>
</li>
<li>우리네 인생도 매번 치열하고, 우울하고, 서로 시기질투할 수도 있겠지만 결국 세상은 잘 돌아간다.</li>
<li>약간, 인생무상 아무것도 소용없어, 라는 의미로 들릴 수도 있겠지만 그래도 가끔은 목과 어깨에 힘 빼고 살아보자.</li>
</ul>
<h2 id="6-에이브라함-링컨의-설득법">6. 에이브라함 링컨의 설득법</h2>
<blockquote>
<p>내가 다른 사람을 설득할 준비를 할 때, 나는 내가 말하고자 하는 것이 무엇인지와 나 자신에 대하여 생각하는 데에 시간의 1/3을 보내고, 상대가 말하려는 것이 무엇일까, 상대에 대해 생각하는 데에 나머지 2/3을 보낸다.</p>
</blockquote>
<ul>
<li>비슷한 이야기로 귀가 두 개인 것은 말하는 것 보다 듣는 것을 두 배 더 하라는 뜻이다 라는 이야기도 있다는 생각이 들었다.</li>
<li>개인적인 경험으로도, 말하는 것 보다는 상대의 이야기를 듣고 이해하며 받아들일 수 있는 자세가 더 중요한 것 같다. 과연 나는 잘 하고 있을까.</li>
</ul>
<h2 id="7-진정한-천재란">7. 진정한 천재란.</h2>
<blockquote>
<p>진정한 천재란, 비범한 일을 수행하는 능력을 가진 자가 아니다.<br>진정한 천재란, 평범한 일을 비범하게 수행하는 능력을 가진 자를 말한다.<br>루이스 윌턴(저술가)</p>
</blockquote>
<ul>
<li>나는 어릴적 부터 천재라는 말을 굉장히 좋아했다.<ul>
<li>그래서 무슨 일을 하건, 처음부터 잘 하는 사람들을 천재라고 생각하고, 그게 정말 멋지다고 생각했다.</li>
</ul>
</li>
<li>하지만, 나이를 먹을수록 깨달았다. 타고 나서 처음부터 잘하는 사람보다, 평범한 사람이 평범한 작은 일을, 비범한 노력으로 쌓아올림으로써 후천적으로 &#39;천재&#39;가 되어가는 것이 더 멋지다고 생각하게 되었다.</li>
</ul>
<h2 id="8-2천-번의-실패라니요-저는-단지-2천-번의-과정을-거쳐서-전구를-발명했을-뿐입니다---토머스-에디슨">8. &quot;2천 번의 실패라니요? 저는 단지 2천 번의 과정을 거쳐서 전구를 발명했을 뿐입니다.&quot; - 토머스 에디슨</h2>
<ul>
<li>유명한 문구다. 그런데 이 문구를, 단순히 &#39;아 긍정적인 사람이구나.&#39; 하고 넘길 게 아니라, 좀 다르게 보아야 할 것 같다.</li>
<li>이런 말을 한 당시 에디슨의 머릿속에는 실패라는 개념이 없었을 것이다.<ul>
<li>남들이 실패라고 말하는 것 모두 그저 과정일 뿐이었고, 그는 이런 과정 끝에 결국 성공할 것이라는 신념을 가지고 있었을 것이다.</li>
</ul>
</li>
</ul>
<h2 id="9-유능한-예술가는-모방하고-위대한-예술가는-훔친다---피카소">9. 유능한 예술가는 모방하고, 위대한 예술가는 훔친다. - 피카소</h2>
<ul>
<li>이 문구를 보고 바로 스티브 잡스가 떠올랐다.</li>
<li>단순히 유능한 예술가들은 모방을 한다. 왜냐하면 인간은 &#39;완전히 새로운 무언가&#39;를 만들어 낼 수는 없다고 생각하기 때문이다.</li>
<li>하지만, &#39;유능한 예술가&#39;와 &#39;위대한 예술가&#39;는 그 수준이 다르다.<ul>
<li>유능한 예술가도 모방을 한다. 하지만 그들은 그 무언가를 훔쳐, 완전히 자신의 것으로 새롭게 보이도록 만들어내는 능력을 가졌기 때문이다.</li>
</ul>
</li>
</ul>
<h2 id="10-당신이-진정으로-성공하고자-한다면-자기-훈련을-두-번째-사랑으로-목표-설정을-첫-번째-사랑으로-삼아라---월터-크라이슬러">10. 당신이 진정으로 성공하고자 한다면, 자기 훈련을 두 번째 사랑으로, 목표 설정을 첫 번째 사랑으로 삼아라. - 월터 크라이슬러</h2>
<ul>
<li>이 글을 보고 나는 &#39;천재는 1%의 영감과 99%의 노력으로 탄생한다.&#39;라는 문구가 떠올랐다.<ul>
<li>사실 이 문구는 와전되었는데, 사실 에디슨은 1%의 영감이 제대로 되어야 99%의 노력이 효과가 있다는 뜻으로 한 말이라고 한다.</li>
</ul>
</li>
<li>즉, 아무리 노력하더라도 올바른 방향 설정이 되지 않는다면 말짱 도루묵이라는 것이다.</li>
<li>물리학과로서 다시 말하자면, 인생은 속력이 중요한 것이 아니라 속도가 중요한 것이다.</li>
</ul>
<h2 id="11-전심치지專心致志---맹자">11. 전심치지(專心致志). - 맹자</h2>
<ul>
<li>다른 생각 하지 말고, 맡은 일에만 온 마음을 다해 힘을 써라.</li>
<li>뭔가 하고 싶은 일이 있다면, 모든 것을 투자해서 그것을 이루어 내라.</li>
</ul>
<h2 id="12-부드러운-바람과-잔잔한-비로-소리없이-사물을-적신다">12. 부드러운 바람과 잔잔한 비로 소리없이 사물을 적신다.</h2>
<ul>
<li>무언가를 적시는 방법은 크게 두 가지가 있다.<ul>
<li>한 번에 적시는 것과 조금씩 천천히 적시는 것. 내 경험상 천천히 적히는 것이 훨씬 흠뻑, 깊숙하고 확실하게 젖는다.</li>
</ul>
</li>
<li>이와 같이 주변의 누군가를 가르치거나 이끌어야 할 때, 아무도 모르게 천천히 조금씩 적히는 것이 좋다.</li>
<li>직접적인 교육을 통해 인위적으로 상대방을 가르칠 게 아니라, 스스로의 행동을 통해 상대방이 조금씩 변화할 수 있도록 하라.</li>
</ul>
<h2 id="13-군자의-마음---채근담">13. 군자의 마음 - 채근담</h2>
<blockquote>
<p>성긴 대숲에 바람이 불어오되, 바람이 지나가면 대숲은 바람을 머금지 아니하고<br>차가운 연못 위로 기러기 날아가되, 기러기 지나가면 연못은 그림자를 붙들지 아니한다.<br>그와 같이 군자는 일이 생기면 비로소 마음이 일고, 일이 끝나면 마음도 따라서 빈다.</p>
</blockquote>
<ul>
<li>요즘 우리가 아는 점잖기만한 &#39;선비&#39;는 진짜 &#39;선비&#39;가 아니다.</li>
<li>군자는 즐거울 때 즐길 줄을 알되, 그 끝을 확실히 한다.<ul>
<li>진정한 선비는 일에 앞서 즐거워하지 않고, 일이 끝난 후 즐거워하거나 슬퍼하지 않는다.</li>
</ul>
</li>
<li>쉽게 말해, 김치국 마시지 않고 어떤 상황에도 크게 동요하지 않는다.</li>
<li>그들은 갈대처럼 쉬이 휘어지지 아니하고 고목처럼, 호수처럼, 그들 자신의 선율을 이어 나간다.</li>
<li>율리우스 시저의 &#39;이 또한 지나가리.&#39;라는 문구가 생각나는 시조이다.</li>
<li>일희일비 하지 말고, 맺고 끊음을 확실히 하자.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>