<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>현주먹의 개발로그</title>
    <link>https://javacatcher.tistory.com/</link>
    <description>대구 불주먹 출신 현주먹의 개발.log</description>
    <language>ko</language>
    <pubDate>Tue, 16 Jun 2026 18:21:08 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>현주먹</managingEditor>
    <image>
      <title>현주먹의 개발로그</title>
      <url>https://tistory1.daumcdn.net/tistory/3964370/attach/273caed710544c08930a571c643358b2</url>
      <link>https://javacatcher.tistory.com</link>
    </image>
    <item>
      <title>F-Lab 블로그 챌린지 2기를 마치며</title>
      <link>https://javacatcher.tistory.com/234</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 참여했냐면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;F-Lab은 1:1 멘토링 기반 서비스로, 수료 이후에도 수료생 채널을 통해 다양한 세션들과 네트워킹을 제공하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(에프랩 제일 잘 써먹는 사람 중에 1명일 듯 움하하)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한창 포스팅을 열심히 하다가, 프로젝트&amp;amp;이직 준비 하다 보니 포스팅에 뜸해져서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강제성이 필요하겠다..!라고 생각하던 참&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1554&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmdxRC/btsQSvIjCOY/y8Olw18FTwLczT6tmz4pqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmdxRC/btsQSvIjCOY/y8Olw18FTwLczT6tmz4pqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmdxRC/btsQSvIjCOY/y8Olw18FTwLczT6tmz4pqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmdxRC%2FbtsQSvIjCOY%2Fy8Olw18FTwLczT6tmz4pqk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;544&quot; height=&quot;215&quot; data-origin-width=&quot;1554&quot; data-origin-height=&quot;614&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침 에프랩에서 수료생들을 대상으로 블로그 챌린지 2기를 주최한다길래 바로 신청했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;진행 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8월 초부터 시작해서 격주에 한 번씩 블로그를 작성하고, 일요일마다 회고하는 방식이었다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;진행 기간&lt;/td&gt;
&lt;td&gt;8월 초부터 2달간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;포스팅 주기&lt;/td&gt;
&lt;td&gt;2주에 1회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;회고 일정&lt;/td&gt;
&lt;td&gt;매주 일요일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;목표 포스팅 수&lt;/td&gt;
&lt;td&gt;4개&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신청할 때만해도 열심히 면접 보러 다닐 때라,, 아무 걱정 없었는데 그땐 몰랐지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이직+디프만+현차 전형과 겹칠 줄 허허&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각자 포스팅 한 뒤 슬랙에 어떤 글을 썼는지 공유하고, 회고를 시작한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1562&quot; data-origin-height=&quot;1308&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BED4G/btsQUhh0L1G/vbUhPPwA4gWKyjAOkhF0zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BED4G/btsQUhh0L1G/vbUhPPwA4gWKyjAOkhF0zk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BED4G/btsQUhh0L1G/vbUhPPwA4gWKyjAOkhF0zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBED4G%2FbtsQUhh0L1G%2FvbUhPPwA4gWKyjAOkhF0zk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;1308&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1562&quot; data-origin-height=&quot;1308&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 내 거 쓰기도 바빠서 다른 분들 글을 못 보고 회고에 참여했었는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 회차에 보고 갔더니 같이 얘기할 거리도 많고, 고민했던 점도 공감이 잘돼서 웬만하면 틈틈이 보려고 노력했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;나의 포스팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2주에 1번씩, 2 달이니 원래는 4번의 포스팅을 작성했어야 헀는데.....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 이때 풀야근+주말 업무에다가 다른 일 들까지 겹쳐서 2번밖에 쓰지 못했다 흑흑&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&lt;a href=&quot;https://javacatcher.tistory.com/232&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt; 8주간의&amp;nbsp;개발자&amp;nbsp;스터디를&amp;nbsp;운영하며&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1758973563038&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;8주간의 개발자 스터디를 운영하며&quot; data-og-description=&quot;최근 &amp;quot;가상 면접 사례로 배우는 대규모 시스템 설계 기초&amp;quot; 스터디를 마무리했다. 사내 JPA 스터디 이후로 오랜만에 운영했던 스터디인데, 성공적으로 마무리된 것 같아서 뿌듯하다!무엇보다 적극&quot; data-og-host=&quot;javacatcher.tistory.com&quot; data-og-source-url=&quot;https://javacatcher.tistory.com/232&quot; data-og-url=&quot;https://javacatcher.tistory.com/232&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/sb3Fv/hyZJPqMent/Bz3imRHve8pP0pYG3i25E1/img.jpg?width=251&amp;amp;height=201&amp;amp;face=86_31_172_125,https://scrap.kakaocdn.net/dn/bliZHY/hyZKe4fGEk/AJLVS7g579KGQAaRiQiTmK/img.jpg?width=251&amp;amp;height=201&amp;amp;face=86_31_172_125,https://scrap.kakaocdn.net/dn/cW20B9/hyZJPxxxbJ/6WwvQLJav9vAGoBYHCjWr0/img.png?width=1660&amp;amp;height=910&amp;amp;face=0_0_1660_910&quot;&gt;&lt;a href=&quot;https://javacatcher.tistory.com/232&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://javacatcher.tistory.com/232&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/sb3Fv/hyZJPqMent/Bz3imRHve8pP0pYG3i25E1/img.jpg?width=251&amp;amp;height=201&amp;amp;face=86_31_172_125,https://scrap.kakaocdn.net/dn/bliZHY/hyZKe4fGEk/AJLVS7g579KGQAaRiQiTmK/img.jpg?width=251&amp;amp;height=201&amp;amp;face=86_31_172_125,https://scrap.kakaocdn.net/dn/cW20B9/hyZJPxxxbJ/6WwvQLJav9vAGoBYHCjWr0/img.png?width=1660&amp;amp;height=910&amp;amp;face=0_0_1660_910');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;8주간의 개발자 스터디를 운영하며&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;최근 &quot;가상 면접 사례로 배우는 대규모 시스템 설계 기초&quot; 스터디를 마무리했다. 사내 JPA 스터디 이후로 오랜만에 운영했던 스터디인데, 성공적으로 마무리된 것 같아서 뿌듯하다!무엇보다 적극&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;javacatcher.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;가상 면접 사례로 배우는 대규모 시스템 설계 기초&quot; 스터디를 8주 동안 운영한 후기를 썼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내 JPA 스터디 이후 오랜만에 운영해 본 스터디였는데, 성공적으로 마무리돼서 뿌듯했던 경험이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://javacatcher.tistory.com/233&quot;&gt;대용량 트래픽을 견디는 콘서트 예약 시스템을 설계해 보자&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1758973558875&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;대용량 트래픽을 견디는 콘서트 예약 시스템을 설계해보자&quot; data-og-description=&quot;F-Lab에서 참가하고 있는 블로그 챌린지 2기, 이번 회차에 어떤 내용을 포스팅해볼거냐면~ 디프만 스터디에서 시스템 디자인 설계 해봤던 내용을 적을 것이다. 왜 해보게 됐냐면최근 토스 NEXT 챌&quot; data-og-host=&quot;javacatcher.tistory.com&quot; data-og-source-url=&quot;https://javacatcher.tistory.com/233&quot; data-og-url=&quot;https://javacatcher.tistory.com/233&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bTPJyX/hyZJ5TMnuZ/dZCSwJLKJesYhYRqu7LjU0/img.jpg?width=254&amp;amp;height=199&amp;amp;face=0_0_254_199,https://scrap.kakaocdn.net/dn/eN0E2/hyZJRB8OtD/4uF2U2vGqibDTJwuSbJUXk/img.jpg?width=254&amp;amp;height=199&amp;amp;face=0_0_254_199,https://scrap.kakaocdn.net/dn/bWk22O/hyZJSue1aA/YxHZuIcte8uz3jtwyvKLI0/img.jpg?width=251&amp;amp;height=201&amp;amp;face=15_47_233_140&quot;&gt;&lt;a href=&quot;https://javacatcher.tistory.com/233&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://javacatcher.tistory.com/233&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bTPJyX/hyZJ5TMnuZ/dZCSwJLKJesYhYRqu7LjU0/img.jpg?width=254&amp;amp;height=199&amp;amp;face=0_0_254_199,https://scrap.kakaocdn.net/dn/eN0E2/hyZJRB8OtD/4uF2U2vGqibDTJwuSbJUXk/img.jpg?width=254&amp;amp;height=199&amp;amp;face=0_0_254_199,https://scrap.kakaocdn.net/dn/bWk22O/hyZJSue1aA/YxHZuIcte8uz3jtwyvKLI0/img.jpg?width=251&amp;amp;height=201&amp;amp;face=15_47_233_140');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;대용량 트래픽을 견디는 콘서트 예약 시스템을 설계해보자&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;F-Lab에서 참가하고 있는 블로그 챌린지 2기, 이번 회차에 어떤 내용을 포스팅해볼거냐면~ 디프만 스터디에서 시스템 디자인 설계 해봤던 내용을 적을 것이다. 왜 해보게 됐냐면최근 토스 NEXT 챌&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;javacatcher.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디프만 스터디에서 시스템 디자인 설계를 해본 내용을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토스 NEXT 챌린지를 계기로 시작하게 된 스터디였는데, 1회만 했어도 꽤 재밌었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 포스팅은 10줄 이상이라 허들이 높지 않은데, 난 글 하나에 공을 많이 들이기 때문에 회차 채우려고 대충 글을 쓰고 싶진 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;우수 회고자에 선정됐어요!&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1554&quot; data-origin-height=&quot;924&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QggXk/btsQTA3d8l6/PeNxeSDOFDTzdsRzHt8TI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QggXk/btsQTA3d8l6/PeNxeSDOFDTzdsRzHt8TI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QggXk/btsQTA3d8l6/PeNxeSDOFDTzdsRzHt8TI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQggXk%2FbtsQTA3d8l6%2FPeNxeSDOFDTzdsRzHt8TI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;609&quot; height=&quot;924&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1554&quot; data-origin-height=&quot;924&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짜잔 &lt;b&gt;우수 블로거&amp;amp;회고자&lt;/b&gt;로 선정됐다.  &lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;262&quot; data-origin-height=&quot;202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsxY4U/btsQUaSH26Q/T7nEEdYXzRHd9o1HDkv7j1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsxY4U/btsQUaSH26Q/T7nEEdYXzRHd9o1HDkv7j1/img.gif&quot; data-alt=&quot;제가요?&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsxY4U/btsQUaSH26Q/T7nEEdYXzRHd9o1HDkv7j1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bsxY4U/btsQUaSH26Q/T7nEEdYXzRHd9o1HDkv7j1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;262&quot; height=&quot;202&quot; data-origin-width=&quot;262&quot; data-origin-height=&quot;202&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;제가요?&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 죄송할 정도로 적극적으로 참여하지 못했는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 회고에는 꼬박꼬박 참여하고 정성스럽게 포스팅한 걸 좋게 봐주신 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;야근할 때는 듣기만 하면서 채팅만 치기도 했다,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;264&quot; data-origin-height=&quot;191&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dR1lG9/btsQSJmlRNJ/pRNKjTDeXVhaJzbpeToZa0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dR1lG9/btsQSJmlRNJ/pRNKjTDeXVhaJzbpeToZa0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dR1lG9/btsQSJmlRNJ/pRNKjTDeXVhaJzbpeToZa0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdR1lG9%2FbtsQSJmlRNJ%2FpRNKjTDeXVhaJzbpeToZa0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;264&quot; height=&quot;191&quot; data-origin-width=&quot;264&quot; data-origin-height=&quot;191&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평일엔 일, 토요일엔 디프만, 일요일엔 블챌 하니까 진짜 바빴던 것 같다 ㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 3기에 또 참여할 의향 있음!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실히 블로그 챌린지에 참여할 정도면 나름 포스팅을 열심히 하시는 분들이기 때문에, 퀄리티도 굉장히 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 직군이 ML, FE, BE로 다양해서 그 부분도 재밌었고, ML에도 좀 관심이 생겼다...(?)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 진행해 주시는 Theo님의 얘기 듣는 것도 재밌고, 수료생분들과 자유롭게 이야기하는 분위기라 즐거웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;F-Lab에서 수료생들을 위해서도 이런 다양한 세션을 기획해 주고, &lt;b&gt;보상&lt;/b&gt;까지 있으니 역시 에프랩 하길 잘했다는 생각이 또 들었다. &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 기술 블로그는 앞으로도 꾸준히 해야겠다고 다시 한번 다짐했다!!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;에프랩 홍보직원 아님..  &amp;nbsp;&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-03 오후 8.28.18.png&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;1006&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpjDrl/dJMb9XK7Jav/0sMJ4kKLtk9VmbiUtjXblk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpjDrl/dJMb9XK7Jav/0sMJ4kKLtk9VmbiUtjXblk/img.png&quot; data-alt=&quot;도란도란&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpjDrl/dJMb9XK7Jav/0sMJ4kKLtk9VmbiUtjXblk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpjDrl%2FdJMb9XK7Jav%2F0sMJ4kKLtk9VmbiUtjXblk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;560&quot; height=&quot;1006&quot; data-filename=&quot;스크린샷 2025-08-03 오후 8.28.18.png&quot; data-origin-width=&quot;1540&quot; data-origin-height=&quot;1006&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;도란도란&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #505860; text-align: start;&quot;&gt;저도 에프랩을 등록하기까지 1년 넘게 걸렸는데요, 비슷한 고민을 하고 계신 분들께 도움이 되었으면 좋겠습니다. &lt;br /&gt;궁금하신 점이 있다면 추천인 코드와 함께 도움을 드릴 수 있으니 편하게 댓글이나&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;background-color: #ffffff; color: #0070d1; text-align: start;&quot; href=&quot;https://open.kakao.com/o/sq1Zy4dh&quot;&gt;채팅&lt;/a&gt;&lt;span style=&quot;background-color: #ffffff; color: #505860; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;주세요!&lt;/span&gt;&lt;/p&gt;</description>
      <category>  끄적끄적/후기 및 회고</category>
      <category>F-Lab</category>
      <category>기술블로그</category>
      <category>에프랩</category>
      <author>현주먹</author>
      <guid isPermaLink="true">https://javacatcher.tistory.com/234</guid>
      <comments>https://javacatcher.tistory.com/234#entry234comment</comments>
      <pubDate>Sat, 27 Sep 2025 20:55:12 +0900</pubDate>
    </item>
    <item>
      <title>대용량 트래픽을 견디는 콘서트 예약 시스템을 설계해보자</title>
      <link>https://javacatcher.tistory.com/233</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;F-Lab에서 참가하고 있는 블로그 챌린지 2기, 이번 회차에 어떤 내용을 포스팅해볼거냐면~&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;디프만 스터디에서 시스템 디자인 설계 해봤던 내용을 적을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 해보게 됐냐면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 토스 NEXT 챌린지나, 스타트업 면접을 보면서 시스템 디자인 설계 능력을 요구하는 곳이 많다는 걸 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 설계 능력은 원래 주니어에서 시니어로 넘어갈 때 요구되는 능력이라고 생각했는데,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;AI가 등장하면서 시기가 많이 앞당겨진 것 같아 위기감을 느꼈다.. 또륵  &lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;디프만&lt;/b&gt;에서 열리는 &lt;b&gt;시스템 디자인 설계 스터디&lt;/b&gt;에 참여했다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(회사+ 디프만 프로젝트를 병행하면서 스터디까지 할 수 있을까 싶었지만 일단 갈겨)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;254&quot; data-origin-height=&quot;199&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKRPjj/btsQeES76Jj/YolE81ycDlJckxoYyIIHd1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKRPjj/btsQeES76Jj/YolE81ycDlJckxoYyIIHd1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKRPjj/btsQeES76Jj/YolE81ycDlJckxoYyIIHd1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKRPjj%2FbtsQeES76Jj%2FYolE81ycDlJckxoYyIIHd1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;254&quot; height=&quot;199&quot; data-origin-width=&quot;254&quot; data-origin-height=&quot;199&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설계 과제 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 과제는 콘서트 좌석 예매 시스템을 설계하는 것이고, 상세한 task는 아래와 같다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 공연장의 좌석을 온라인으로 판매하려고 합니다. &lt;br /&gt;사용자는 좌석 배치를 보고 원하는 좌석을 선택한 뒤 결제할 수 있어야 하며, 이미 판매된 좌석은 다시 판매되면 안 됩니다. &lt;br /&gt;또한 좌석을 고른 뒤 일정 시간 안에 결제를 완료하지 않으면 좌석은 자동으로 풀려야 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;가정&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 이벤트(공연 1회차)만 다룬다.&lt;/li&gt;
&lt;li&gt;PC 웹 기준(모바일 고려는 선택).&lt;/li&gt;
&lt;li&gt;외부 결제는 &lt;b&gt;&amp;ldquo;결제 성공 콜백이 온다&amp;rdquo;&lt;/b&gt; 가정(실제 결제 연동은 모의).&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기능적 요구사항&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자는 해당 페이지에서, 전체 좌석, 열, 그리고 좌석 상태에 대해 조회 할 수 있다.&lt;/li&gt;
&lt;li&gt;좌석은 &lt;b&gt;선택 &amp;rarr; 결제 &amp;rarr; 확정(판매완료)&lt;/b&gt; 의 흐름을 가진다.&lt;/li&gt;
&lt;li&gt;결제가 완료된 좌석은 다시 판매될 수 없다.&lt;/li&gt;
&lt;li&gt;사용자는 예매가 완료되기까지 동기적으로 응답을 확인해야 한다.&lt;/li&gt;
&lt;li&gt;사용자는 자신의 예매 내역을 조회할 수 있다.&lt;/li&gt;
&lt;li&gt;사용자는 예매가 완료되기까지 대기열에서 대기해야한다.&lt;/li&gt;
&lt;li&gt;좌석 예매와 관련된 로그를 저장하고 조회해야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;비기능적 요구사항&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;중복 판매가 되어서는 안된다.&lt;/li&gt;
&lt;li&gt;예매가 열리는 순간 피크 사용자가, 20만명이라고 가정한다.&lt;/li&gt;
&lt;li&gt;전체 좌석은 만오천석이라고 가정.&lt;/li&gt;
&lt;li&gt;좌석 조회는 p95,p99 &amp;lt;150ms 이내&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이런 거를 해보면 좋아요&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ERD 설계&lt;/li&gt;
&lt;li&gt;아키텍처 다이어그램 그리기&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(Optioanl) 추가 고민해 보면 좋을만한 사항들&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시에 여러 사람이 같은 좌석을 선택하면 어떻게 막을까?&lt;/li&gt;
&lt;li&gt;결제 실패/타임아웃이 발생하면 좌석 상태를 어떻게 돌려놓을까?&lt;/li&gt;
&lt;li&gt;같은 주문 요청이 여러 번 오면(중복 클릭 등) 어떻게 막을 수 있을까?&lt;/li&gt;
&lt;li&gt;수십만 명이 동시에 접속하면 어떤 부분이 가장 먼저 병목이 될까?&lt;/li&gt;
&lt;li&gt;00명이 대기중입니다.와 같은 대기열을 어떻게 만들 수 있을까?&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;내가 접근한 방법과 설계&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;251&quot; data-origin-height=&quot;201&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WuhBx/btsQerfyGhn/WqDXorPr0KkHkZ9cCTgq41/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WuhBx/btsQerfyGhn/WqDXorPr0KkHkZ9cCTgq41/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WuhBx/btsQerfyGhn/WqDXorPr0KkHkZ9cCTgq41/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWuhBx%2FbtsQerfyGhn%2FWqDXorPr0KkHkZ9cCTgq41%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;251&quot; height=&quot;201&quot; data-origin-width=&quot;251&quot; data-origin-height=&quot;201&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 과제를 처음 봤을 땐 대체 뭐부터 설계해야 하는 걸까? 하고 되게 막막했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ERD 설계를 해보라고 했지만, ERD는 기능 요구사항이나 화면 구성에 따라 너무 많이 달라지는 부분이라 생각해서 아키텍처 설계 위주로 접근했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 우선적으로 요구사항은 아래와 같이 정리했다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 50.2326%; height: 163px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 30.4103%;&quot;&gt;동시 접속자&lt;/td&gt;
&lt;td style=&quot;width: 69.4735%;&quot;&gt;20만명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 30.4103%;&quot;&gt;전체 좌석 수&lt;/td&gt;
&lt;td style=&quot;width: 69.4735%;&quot;&gt;15,000석&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 30.4103%;&quot;&gt;응답시간&lt;/td&gt;
&lt;td style=&quot;width: 69.4735%;&quot;&gt;P95/P99 &amp;lt; 150ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 30.4103%;&quot;&gt;중복 판매&lt;/td&gt;
&lt;td style=&quot;width: 69.4735%;&quot;&gt;절대 금지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 30.4103%;&quot;&gt;예매 플로우&lt;/td&gt;
&lt;td style=&quot;width: 69.4735%;&quot;&gt;좌석 선택&amp;rarr;결제&amp;rarr;예약 확정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직은 정량적 수치로 아키텍처를 어떻게 설계해야 하는지 경험이 부족해서, '대용량 트래픽'이라는 관점에서 아래의 `추가 고려사항`들을 중심으로 설계를 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시에 여러 사람이 같은 좌석을 선택하면 어떻게 막을까?&lt;/li&gt;
&lt;li&gt;결제 실패/타임아웃이 발생하면 좌석 상태를 어떻게 돌려놓을까?&lt;/li&gt;
&lt;li&gt;같은 주문 요청이 여러 번 오면(중복 클릭 등) 어떻게 막을 수 있을까?&lt;/li&gt;
&lt;li&gt;수십만 명이 동시에 접속하면 어떤 부분이 가장 먼저 병목이 될까?&lt;/li&gt;
&lt;li&gt;00명이 대기중입니다.와 같은 대기열을 어떻게 만들수 있을까?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 동시에 여러 사람이 같은 좌석을 선택하면 어떻게 막을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시에 여러 사람이 같은 좌석을 선택하는 상황을 어떻게 막을지가 가장 핵심적인 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굉장히 다양한 방법들이 있지만, 아키텍처에 따라 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글 인스턴스라면 CAS 알고리즘이나 synchronized를 사용할 수 있지만, 실무에서는 대부분 멀티 인스턴스이므로 낙관적 락, 비관적 락, 분산락, 메시지 큐 중에서 선택해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 방식의 특징을 정리하면 다음과 같다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;방식&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;낙관적 락&lt;/td&gt;
&lt;td&gt;성능 우수&lt;/td&gt;
&lt;td&gt;충돌 시 재시도 비용 높음&lt;/td&gt;
&lt;td&gt;충돌이 적은 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비관적 락&lt;/td&gt;
&lt;td&gt;충돌 상황에서 안정적&lt;/td&gt;
&lt;td&gt;락 대기로 인한 성능 저하&lt;/td&gt;
&lt;td&gt;일반적인 상황&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;분산락&lt;/td&gt;
&lt;td&gt;즉시 피드백 가능&lt;/td&gt;
&lt;td&gt;구현 복잡도 높음&lt;/td&gt;
&lt;td&gt;실시간 응답 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메시지 큐&lt;/td&gt;
&lt;td&gt;공정성 보장&lt;/td&gt;
&lt;td&gt;비동기 처리로 즉시성 부족&lt;/td&gt;
&lt;td&gt;선착순 이벤트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘서트 예매의 특성상 다수의 사람들이 소수의 명당(앞줄, 가운데 좌석 등)에 몰리는 현상이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황에서는 동일한 좌석에 대한 충돌이 빈번하게 일어나고, &lt;b&gt;낙관적 락&lt;/b&gt;을 사용하면 대부분의 트랜잭션이 실패해서 재시도해야 한다. 재시도 비용이 누적되면 전체 시스템 성능이 낮아질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 비관적 락&lt;/b&gt;은 충돌이 많은 경우에 낙관적 락보다 성능상 이점은 있다, 근데 아무래도 DB 단까지 와야한 다는 생각에 걸렀는데, &quot;엔티티 단위에만 락을 걸기 때문에 괜찮으려나?&quot;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 낙관적 락과 같은 이유로 명당에 몰리는 이슈때문에 성능상 문제가 있겠다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 최종적으로 &lt;b&gt;분산락, 메시지 큐&lt;/b&gt;를 생각했는데, 그 중에서도 콘서트 예매/진짜 선착순(쿠폰 같은) 이벤트 컨셉을 고려해 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘서트 예매는 사용자에게 즉시 피드백을 주는 것이 중요하므로, 분산락을 메인 플로우로 하고 메시지 큐를 부가 기능으로 활용하는 것이 적절하다고 결론지었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, 배달앱 쿠폰처럼 진짜 선착순 이벤트라면 공정성이 우선이므로 메시지 큐를 메인 플로우로 하고 분산락을 중복 요청 방지 보조 역할로 사용하는 게 좋겠다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(하지만 다 이론적인 내용이고 실제로는 부하 테스트를 해보지 않는 이상 모른다고 생각함...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. &quot;00명이 대기 중입니다&quot;와 같은 대기열 시스템을 어떻게 만들어야 할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;20만 명의 동시 접속자를 처리하기 위해서는 대기열 시스템이 필수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 좌석이 15,000석이므로, 실제 예매 가능한 인원은 그보다 훨씬 적다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 시스템이 안정적으로 처리할 수 있는 5,000명 정도만 실제 예매 페이지에 진입시키고, 나머지는 대기열에서 순차적으로 처리하는 방식을 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대기열 시스템 구현 방법으로는 RabbitMQ나 Kafka 같은 &lt;b&gt;메시지 큐&lt;/b&gt;를 처음에 생각했지만, &lt;b&gt;Redis의 Sorted Set&lt;/b&gt;을 활용하는 방법도 있다는 걸 알게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Sorted Set을 사용하면 타임스탬프를 점수로 하여 자연스럽게 순서를 관리할 수 있고, 대기 순번 조회도 쉽게 구현할 수 있다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세히는 아직 잘 몰라서 더 찾아봐야 할 것 같다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 결제 실패/타임아웃이 발생하면 좌석 상태를 어떻게 돌려놓을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 &quot;장애 복구 메커니즘을 구축해야 하지 않을까?&quot; 해서 몇 가지 방법을 고려했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째는 &lt;b&gt;Redis TTL&lt;/b&gt;을 활용하는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌석을 선점할 때 Redis에 일정 시간(ex. 10분) 동안만 유지되는 키를 생성하고, 시간이 지나면 자동으로 해제되도록 하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째는 &lt;b&gt;비동기 배치 작업&lt;/b&gt;으로 주기적으로 좌석 상태를 체크하는 방법이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결제 대기 상태인 좌석들을 스캔해서 일정 시간이 지난 것들을 자동으로 해제하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째는 PG사에서 &lt;b&gt;결제 실패 콜백이 오면 즉시 복구&lt;/b&gt;하는 방법이다. 이벤트 드리븐 방식으로 가장 빠른 복구가 가능하지만, 콜백이 오지 않는 경우를 대비한 로직이 또 필요할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 같은 주문 요청이 여러 번 오면(중복 클릭 등) 어떻게 막을 수 있을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 처음에 동시성? 인가 생각했는데 뭔가 그것 보단 중복 요청에 대한 트래픽이라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프론트엔드&lt;/b&gt;에서는 1차 방어로 버튼 클릭 시 `disabled` 처리를 해야 한다. 하지만 이것만으로는 충분하지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐면 네트워크 문제나 악의적인 요청 등으로 여전히 중복 요청이 발생할 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;백엔드&lt;/b&gt;에서는 `Rate Limiter`를 구현해서 1인당 요청 횟수를 제한할 수 있다. 사용자 IP나 세션 기준으로 일정 시간 동안 허용 가능한 요청 수를 제한하는 방식이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://javacatcher.tistory.com/224&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;가상 면접 사례 4장에 나왔던 처리율 제한 장치&lt;/a&gt;를 여기다 써먹는 건가..라고 생각함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 수십만 명이 동시에 접속하면 어떤 부분이 가장 먼저 병목이 될까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수십만 명이 동시에 접속하면 어떤 부분이 먼저 병목이 될지 분석해 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 병목 지점은 &lt;b&gt;네트워크, 로드밸런서&lt;/b&gt;가 아닐까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 앞단에서 20만 명의 동시 접속을 받아야 하므로, 네트워크 대역폭이나 로드밸런서 자체가 먼저 한계에 도달할 가능성이 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 대기열 시스템으로 방어할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;두 번째 병목 지점은&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;웹 애플리케이션 서버&lt;/b&gt;라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대기열을 통과한 사용자들이 실제 예매 로직을 처리하는 단계에서 CPU와 메모리 사용량이 급증한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 동시성 제어 로직이나 비즈니스 규칙 검증 과정에서 병목이 발생할 수 있다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째 병목 지점은 &lt;b&gt;데이터베이스&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스크 I/O가 메모리 접근보다 훨씬 느리기 때문에 동시 접속자가 많아지면 DB가 병목이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌석 상태 조회와 업데이트가 빈번하게 일어나면서 DB 커넥션 풀 고갈이나 락 경합으로 인한 성능 저하가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 스케일 업/아웃은 비용이 많이 들기 때문에, 우선은 현재 리소스 내에서 성능을 개선하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 성능 개선 방법으로는 인덱스 최적화로 조회 성능을 개선하고, 읽기 전용 레플리카로 조회 부하를 분산할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 좌석 정보는 실시간 정합성이 중요하므로 캐싱을 적용할 때는 매우 짧은 TTL을 설정하거나 이벤트 기반 캐시 무효화 전략이 필요하다. CQRS 패턴을 적용해서 읽기와 쓰기를 분리하는 것도 고려할 수 있지만, 복잡도가 증가하므로 트래픽 규모와 팀 역량을 고려해서 결정해야 할 듯,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;팀원들의 설계 방향&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스터디 내는 대학생 분들도 많이 계셨는데, 정말 꼼꼼하고 체계적으로 설계해 오신 모습에 많이 반성했다.. 허허  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 시간이 부족해서 주요 쟁점들만 정리했는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 분들은 ERD 설계부터 API 명세, 전체 플로우까지 모든 단계를 고려해서 각 병목 지점의 개선 방안을 상세히 준비해 왔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;219&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMvqzu/btsQgbiwrcu/g2mk9HqwFqk5uZHFMQlIO0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMvqzu/btsQgbiwrcu/g2mk9HqwFqk5uZHFMQlIO0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMvqzu/btsQgbiwrcu/g2mk9HqwFqk5uZHFMQlIO0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMvqzu%2FbtsQgbiwrcu%2Fg2mk9HqwFqk5uZHFMQlIO0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;219&quot; height=&quot;230&quot; data-origin-width=&quot;219&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항에 나온 정량적 수치를 고려한 부분도 있었고, 내가 놓쳤던 부분들을 많이 다뤄주셔서 자극을 많이 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 레포지토리를 막 만든 상태라 서로 자료가 공유되지 않았는데, 추후에 동의를 구해서 다양한 설계 방안들을 첨부해 수정할 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;토론에서 나온 주요 쟁점들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 설계 내용을 발표한 후에 공통점도 있었지만 다르게 접근한 부분들이 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답이 없는 영역이기 때문에 다양한 설계 방법이 나올 수밖에 없고, 이 부분에서 각자의 기술 선택 근거와 트레이드오프를 공유하면서 서로 성장할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 나눴던 3가지 쟁점들을 정리해 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 좌석 선점 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용자가 처음 좌석을 클릭했을 때 바로 선점할 것인지&lt;/b&gt;에 대한 의견이 갈렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KTX나 영화 예매는 좌석을 클릭하면 바로 선점되어서 다른 사용자가 해당 좌석을 선택할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 콘서트 예매를 많이 해본 스터디원이 말하길 인터파크 같은 경우는 좌석을 선점하지 않아서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 사용자가 동일한 좌석으로 결제를 시도할 수 있다고 한다. 이 경우 누가 먼저 결제를 완료하느냐가 관건이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 방식의 장단점을 정리하면 다음과 같다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 17.7907%;&quot;&gt;&lt;b&gt;방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 28.8372%;&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 53.2558%;&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 17.7907%;&quot;&gt;즉시 선점&lt;/td&gt;
&lt;td style=&quot;width: 28.8372%;&quot;&gt;사용자 경험 좋음, 중복 시도 방지&lt;/td&gt;
&lt;td style=&quot;width: 53.2558%;&quot;&gt;결제 포기 시 좌석 낭비 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 17.7907%;&quot;&gt;결제 시 선점&lt;/td&gt;
&lt;td style=&quot;width: 28.8372%;&quot;&gt;좌석 활용도 높음&lt;/td&gt;
&lt;td style=&quot;width: 53.2558%;&quot;&gt;결제 실패 확률 높음, UX 떨어짐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 다시 생각해 보면 어떤 방식을 선택할지는 비즈니스 정책에 따라 달라질 수 있지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 만족도를 우선시한다면 즉시 선점 방식이 좋고, 좌석 판매율을 높이려면 결제 시 선점 방식이 유리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 트랜잭션 범위&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;좌석 선택 &amp;rarr; 결제 &amp;rarr; 예약 완료&quot; 흐름에서 트랜잭션을 어떻게 나눌지&lt;/b&gt;도 중요한 쟁점이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽이 적다면 전체를 하나의 트랜잭션으로 처리해도 문제없지만, 동시 접속자가 많으면 네트워크 지연이나 외부 시스템(PG사) 연동 실패 등으로 트랜잭션이 길어질 수 있다. 트랜잭션이 길어지면 락 보유 시간도 길어져서 전체 시스템 성능에 악영향을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 올리브영이나 다른 커머스 기업들의 기술 블로그를 보면&lt;b&gt; 결제 트랜잭션을 분리&lt;/b&gt;한 사례들이 많이 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문 생성과 결제 처리를 별도 트랜잭션으로 나누고, 이벤트 기반으로 상태를 동기화하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;Spring의 Events를 사용할 수도 있고, 더 나아가면 Kafka, RabbitMQ, Redis Pub/Sub 같은 메시지 브로커를&amp;nbsp;사용하면 된다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;예시&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://oliveyoung.tech/2022-12-13/oliveyoung-transaction-orderstock/&quot;&gt;올리브영 결제 이야기&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://seungjjun.tistory.com/322&quot;&gt;주문 결제 트랜잭션 단위를 어떻게 가져가야 할까&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결제 트랜잭션 분리를 구글링 하면 꽤 많이 나오는데, 여기서 이벤트 콜백 기반한 이벤트 프로그래밍 얘기가 많이 나오는 듯하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 좌석을 선택해 결제 진행 단계로 들어오는 트랜잭션&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 결제 주문서 발행 트랜잭션&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 결제 완료/실패의 콜백을 받고 예약을 완료하는 트랜잭션&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 3단계로 분리하는 게 좋지 않을까?라는 생각을 해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 동시성 제어법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 오랫동안 토론했던 주제는 &lt;b&gt;동시성을 어떻게 해결할 것인가&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 인스턴스를 가정하고 여러 방법의 장단점을 비교했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DB Unique 제약조건만 사용하는 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간단한 방법이다. 테이블에 유니크 제약조건을 걸어서 DB 레벨에서 중복을 방지하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현이 간단하고 확실하게 중복을 막을 수 있지만, 동시 요청이 많으면 많은 트랜잭션이 실패해서 사용자 경험이 나빠질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비관적 락 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 시작 시점에 해당 레코드에 락을 걸어서 다른 트랜잭션이 접근하지 못하게 하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데드락 위험이 있지만, 충돌이 예상되는 상황에서는 낙관적 락보다 성능이 좋을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;분산락 활용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis나 Zookeeper를 사용해서 애플리케이션 레벨에서 락을 관리하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 부하를 줄일 수 있고 세밀한 제어가 가능하지만, 구현 복잡도가 높고 분산락 자체가 장애 포인트가 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각자 다른 방법을 선호했는데, 3시간 동안 토론해도 명확한 결론은 나지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 시스템의 규모, 예상 트래픽 패턴, 팀의 기술 역량 등을 종합적으로 고려해서 결정해야 할 문제라는 걸 깨달았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스터디 회고와 개선점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇보다&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;nbsp;이 주간에 계속 야근+이사+남은 채용 과정들을 치르느라 시간이 너무 없어서 준비를 많이 못해서 아쉽다,,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음부턴 아키텍처 다이어그램이나 전체적인 타임라인을 도식화해 봐야겠다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 이번이 스터디 첫 주제라 어떻게 결론을 내려야 할지 명확하지 않아서 서로 트레이드오프를 고려하면서 토론하는 정도로 끝났는데, 앞으로는 좀 더 구체적인 결과물을 만들어보면 좋을 것 같다  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 각자의 설계를 다이어그램으로 그려서 비교해 보거나, 토론 과정에서 하나의 통합된 아키텍처로 의견을 맞춰본다거나?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참 많이 느끼는 거지만, 대학생/신입분들의 역량이 정말 많이 올라갔다는 걸 많이 느꼈고, 좋은 자극으로 받아들이면서 앞으로 정진해야겠다는 동기를 얻었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>   Dev/System Design</category>
      <category>F-Lab 블로그 챌린지</category>
      <author>현주먹</author>
      <guid isPermaLink="true">https://javacatcher.tistory.com/233</guid>
      <comments>https://javacatcher.tistory.com/233#entry233comment</comments>
      <pubDate>Sun, 31 Aug 2025 16:51:44 +0900</pubDate>
    </item>
    <item>
      <title>8주간의 개발자 스터디를 운영하며</title>
      <link>https://javacatcher.tistory.com/232</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 &lt;a href=&quot;https://github.com/grow-together-study/system-design-interview&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;&quot;가상 면접 사례로 배우는 대규모 시스템 설계 기초&quot; 스터디&lt;/span&gt;&lt;/a&gt;를 마무리했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;227&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HqNsM/btsPEOWXO5W/1AOILzTfChkhH9lKCJwOX0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HqNsM/btsPEOWXO5W/1AOILzTfChkhH9lKCJwOX0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HqNsM/btsPEOWXO5W/1AOILzTfChkhH9lKCJwOX0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHqNsM%2FbtsPEOWXO5W%2F1AOILzTfChkhH9lKCJwOX0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;227&quot; height=&quot;222&quot; data-origin-width=&quot;227&quot; data-origin-height=&quot;222&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;사내 JPA 스터디 이후로 오랜만에 운영했던 스터디인데, 성공적으로 마무리된 것 같아서 뿌듯하다!&lt;br /&gt;무엇보다 적극적으로 참석해 주고, 유종의 미를 거두게 해 준 스터디원분들께 정말 감사하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안그래도 요즘 &lt;b&gt;F-Lab에서 열린 블로그 챌린지 2기&lt;/b&gt;에 참여하고 있기 때문에, 무슨 글을 써볼까 고민하고 있던 참... &lt;br /&gt;스터디를 2번째 운영하면서 느낀 점들과 앞으로 개발자 스터디(도서)를 효과적으로 운영하는 방법에 대해 정리해보고자 한다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스터디는 어떻게 시작하면 좋을까?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;730&quot; data-origin-height=&quot;136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GFXpO/btsPF9eN2KH/k43hlSk49LkHI1kgNfooTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GFXpO/btsPF9eN2KH/k43hlSk49LkHI1kgNfooTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GFXpO/btsPF9eN2KH/k43hlSk49LkHI1kgNfooTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGFXpO%2FbtsPF9eN2KH%2Fk43hlSk49LkHI1kgNfooTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;344&quot; height=&quot;64&quot; data-origin-width=&quot;730&quot; data-origin-height=&quot;136&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;계속 읽고 싶었던 책이라, 혹시 계실까 해서 활동하는 개발자 단톡에 이렇게 꺼냈는데...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UoWU9/btsPDE8zYBu/3sOjrG760lpwkSeSsBzIRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UoWU9/btsPDE8zYBu/3sOjrG760lpwkSeSsBzIRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UoWU9/btsPDE8zYBu/3sOjrG760lpwkSeSsBzIRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUoWU9%2FbtsPDE8zYBu%2F3sOjrG760lpwkSeSsBzIRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;362&quot; height=&quot;128&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;202&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;생각보다 반응이 너무 뜨거워서 살짝 놀랐다...! 허거덩스&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;몇 명이 모여야 할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;몇 명이서 하냐&quot;도 스터디에서 굉장히 중요한 요소다.&lt;br /&gt;너무 많으면 관리가 어렵고, 너무 적으면 다양한 관점을 얻기 힘들다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;내 경험상 도서 스터디에서 가장 적절한 인원은 &lt;b&gt;3~5명&lt;/b&gt;이다.&lt;br /&gt;보통 디스코드 같은 음성 채널에서 스터디를 할 텐데, 6명 이상이면 목소리가 겹치거나 각각의 참여도가 떨어진다고 생각한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;실제로 우리는 8명으로 시작했지만 시작 전에 2분이 회사 사정상 하차하시고, 나머지 1분은 1회만 하고 하차하셨다.&lt;br /&gt;결과적으로 5명이 완주했는데, 이 정도 규모가 딱 적절했다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;무엇을 위한 스터디인지 명확히 하자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 도서 스터디지만, 발표로 진행할 수도 있고 회고로 할 수도 있고 방법은 다양하다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;발표도 고려했었지만, 이 책 자체가 난이도가 있고 또 재직자들이 많기 때문에 발표 자료까지 준비하는 건 타이트하다고 생각했다.&lt;br /&gt;무엇보다 이 책은 2~3회독 해야 하는 책이기 때문에 &lt;b&gt;빠르게 1회독을 해보자&lt;/b&gt;라는 목표로 설정했다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;우리가 정한 진행 방식&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;발표는 없이 개인 노트 정리 (선택)&lt;/li&gt;
&lt;li&gt;깃허브 이슈에 각 장을 읽으며 궁금했던 질문을 1~3개씩 등록 (필수)&lt;/li&gt;
&lt;li&gt;질문 중심으로 회고 진행&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;910&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buMOTm/btsPELlDy8Q/klzS42ompHfZgCFMHBauX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buMOTm/btsPELlDy8Q/klzS42ompHfZgCFMHBauX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buMOTm/btsPELlDy8Q/klzS42ompHfZgCFMHBauX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuMOTm%2FbtsPELlDy8Q%2FklzS42ompHfZgCFMHBauX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;542&quot; height=&quot;297&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;910&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하니 부담은 줄이면서도 깊이 있는 토론이 가능했다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이건 잘했다&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;벌금 제도를 도입하지 않은 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 보증금을 거두거나, 지각/불참에 대해 벌금을 거두는 규칙을 많이 세우는데, 난 그렇게 하기 싫었다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;왜냐!&lt;br /&gt;스터디라는 건 본인이 자발적으로 신청한 거고, 공부 안 하면 본인 손해라고 생각한다.&lt;br /&gt;무엇보다 강제성을 부여하는 건 좋지만, 오히려 &lt;b&gt;&quot;이런 장치가 없어도 적극적으로 참여하는 사람들이 모이는 게 낫지 않을까?&quot;&lt;/b&gt; 생각했다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;실제로 우리는 아무런 강제 장치 없이도 높은 참여율을 유지했다.&lt;br /&gt;0~1회 차에 회사가 바빠져 하차하신 분들 말고는 중도 하차한 스터디원이 전혀 없었다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;251&quot; data-origin-height=&quot;201&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QE531/btsPDsUHlGJ/kZIBJmh0f1g9Cn37DV0s61/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QE531/btsPDsUHlGJ/kZIBJmh0f1g9Cn37DV0s61/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QE531/btsPDsUHlGJ/kZIBJmh0f1g9Cn37DV0s61/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQE531%2FbtsPDsUHlGJ%2FkZIBJmh0f1g9Cn37DV0s61%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;251&quot; height=&quot;201&quot; data-origin-width=&quot;251&quot; data-origin-height=&quot;201&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스터디 일정을 유동적으로 변경하지 않은 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;야근하는 스터디원들이 1~2분씩 빠지는 경우가 있어서, 항상 풀 멤버로 진행되지는 않았다.&lt;br /&gt;3명이서 할 때도 많았는데, &quot;그럼 내일 하면 더 많이 오지 않을까?&quot; 해서 변경도 생각했었다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;하지만 유동적으로 시작하면 다음 진도를 위한 시간이 줄어들 뿐만 아니라,&lt;br /&gt;&quot;유동적으로 되니까 빠져도 되겠지?&quot;라는 여지를 주는 것 같다고 생각했다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;결과적으로 &quot;무슨 일이 있어도 진행한다&quot;는 생각이 있으니 무조건 그전에는 읽어야지.. 하게 돼서 좋았다.&lt;br /&gt;그리고 기간을 너무 러프하게 잡으면 흐지부지될 가능성이 높아서 &quot;무조건 8주만에 완주해야지&quot;라는 생각이 확고했다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;질문을 기반으로 회고한 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 킥오프 미팅 때는 &quot;각자 인상 깊었던 부분을 공유할까?&quot; 싶었다.&lt;br /&gt;하지만 개발 서적을 읽다 보면 &quot;왜?&quot;, &quot;뭔 말이지?&quot;라는 의문이 꼭 생긴다.&lt;br /&gt;그리고 이 부분을 찾아보면서 알아내야 이 책에서 배우는 점이 생긴다고 생각한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그래서 이슈에 각자 이 책을 읽으면서 생긴 질문들을 올리고,&lt;br /&gt;&quot;나는 이 부분에서 이게 뭔 말인지 모르겠더라, 근데 찾아보니 ~라고 한다&quot;&lt;br /&gt;&quot;이 부분에서 ~는 어떻게 되는걸까? 했는데 이런 거라고 하더라&quot; 이런 식으로 공유했다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이런 식으로 진행하니 같은 책을 읽어도 다른 관점에서 접근할 수 있다는 걸 알게 되어 좋았고, 다양한 사례들도 접할 수 있어서 좋았다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모든 회고록을 녹음하고, 기록한 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스코드로 회고록을 녹음하고 &lt;b&gt;클로바노트&lt;/b&gt;를 사용해서 스크립트로 만들었다.&lt;br /&gt;그리고 AI를 활용해서 우리가 이 질문들에서 어떤 내용을 나눴고, 어떤 걸 개선하면 좋은지? 회고록을 작성했다. &lt;a href=&quot;https://github.com/grow-together-study/system-design-interview/blob/main/15%EC%9E%A5_%EA%B5%AC%EA%B8%80_%EB%93%9C%EB%9D%BC%EC%9D%B4%EB%B8%8C_%EC%84%A4%EA%B3%84/%5B15%EC%9E%A5%5D%EB%8B%A8%EC%B2%B4_%ED%9A%8C%EA%B3%A0%EB%A1%9D.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;[예시]&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;그리고 각 스크립트는 따로 스터디원들한테 공유했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kzEhe/btsPFfmgiuI/AJmdDeKXKlKJWhkd6sJEmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kzEhe/btsPFfmgiuI/AJmdDeKXKlKJWhkd6sJEmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kzEhe/btsPFfmgiuI/AJmdDeKXKlKJWhkd6sJEmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkzEhe%2FbtsPFfmgiuI%2FAJmdDeKXKlKJWhkd6sJEmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;374&quot; height=&quot;255&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 당장 필요하지 않더라도 회독하면서 &quot;아 이때 이런 얘기를 했었구나, 이게 맞았네&quot; 이렇게 할 수만 있어도 좋다고 생각한다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아쉬웠던 부분들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이 정보가 정확한 정보일까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 각자 질문을 기반으로 회고를 진행했기 때문에, 그 질문에 대해 본인이 알아낸 정보를 공유하는 식이었다.&lt;br /&gt;근데 이 내용이 대규모 시스템 기반이고, 난이도가 있다 보니 실무에서 설계를 해보지 않은 사람이면 잘 몰라서 AI의 도움을 많이 받게 됐다.&lt;br /&gt;때문에 &lt;b&gt;이 정보가 확실한 건지 정확히 알기 어렵다는 점&lt;/b&gt;이 공통적인 아쉬운 점이었다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그래서 카프카같은 분야가 나오면 데이터 엔지니어 친구에게 물어서, 그 내용을 스터디원들에게 공유하거나,&lt;br /&gt;&quot;어느 회사에 다니는 분이 이런 말씀을 하시더라&quot; 식으로 현업 경험자들의 사례를 들었다.&lt;br /&gt;하지만 모든 내용을 검증하기에는 한계가 있었다.  &lt;br /&gt;&amp;nbsp;&lt;br /&gt;지금 생각하면 질문에 대해 좀 더 자세히 알아보고 했으면 좋았겠지만,&lt;br /&gt;재직자분들이 있으니 그렇게 하면 완주하기가 힘들 것 같아서 포기했다.&lt;br /&gt;애초에 이 책은 최소 2회독을 목표로 잡았기 때문에 러프하게 진행한 면도 있다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음에는 이렇게 해보고 싶다&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 경험자 멘토 섭외&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 스터디에서는 해당 분야 실무 경험자를 멘토로 섭외해서 정기적으로 질문을 받아보는 시간을 가져보고 싶다.&lt;br /&gt;여기서 뭔가 사이드 프로젝트 아이디어가 떠오르기도 했다...(뭔지는  )&lt;br /&gt;이론만으로는 한계가 있는 부분들을 실무 관점에서 물어볼 수 있을 것 같다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;간단한 실습 시간 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 스터디를 하면서, 뭔가 한 챕터를 골라서 직접 작은 트래픽을 고려해서 구현한 다음에,&lt;br /&gt;책의 내용을 적용해 보면 확실히 기억에 남겠다는 생각을 했다.&lt;br /&gt;이 부분은 시간이 정말 오래 걸릴 거고, 1장만 구현해도 n달이 걸릴 수도 있다고 생각해서.. 어떻게 진행할지 고민 중이다.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8주간의 스터디를 마무리하며 가장 큰 깨달음은 &lt;b&gt;역시 혼자 하는 것보다 같이 하는 게 낫다&lt;/b&gt;는 것이었다.&lt;br /&gt;또, 벌금이나 보증금 같은 장치 없이도 충분히 성공적인 스터디를 운영할 수 있다고 느꼈다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;또한 &lt;b&gt;질문 중심의 토론&lt;/b&gt;이 발표 형식보다 오히려 나을 수도 있겠다는 생각을 했다.&lt;br /&gt;발표는 내가 맡은 부분만 기억에 남을 수도 있을 수도 있기 때문에,&lt;br /&gt;각자의 궁금증에서 시작된 회고가 오히려 더 다양하고 실용적인 인사이트를 줬다.(나한테는)&lt;br /&gt;&amp;nbsp;&lt;br /&gt;무엇보다 함께 공부할 수 있는 동료들이 있다는 것 자체가 큰 동기부여가 되었다.&lt;br /&gt;예전에 일할 때는 사내에 같이 스터디할 사람이 없으니 혼자서 공부하는 경우가 많았는데..&lt;br /&gt;그러니 자꾸 중간에 그만 읽고 다른 책을 읽거나 하는 경우가 왕왕 있었다.&lt;br /&gt;혼자서는 끝까지 읽기 어려웠을 책도 함께라면 완주할 수 있다는 것을 다시 한번 느꼈다.&lt;/p&gt;</description>
      <category>  끄적끄적/후기 및 회고</category>
      <author>현주먹</author>
      <guid isPermaLink="true">https://javacatcher.tistory.com/232</guid>
      <comments>https://javacatcher.tistory.com/232#entry232comment</comments>
      <pubDate>Sun, 3 Aug 2025 03:06:31 +0900</pubDate>
    </item>
    <item>
      <title>9장. 웹 크롤러 설계</title>
      <link>https://javacatcher.tistory.com/231</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001033116&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초&lt;/a&gt;를 읽고 정리한 글입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 챕터를 읽으면서 가볍게 크롤링만 적용해 봤는데, 생각보다 고려할 부분이 정말 많구나 라는 걸 깨달았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크롤러는 &amp;lsquo;로봇&amp;rsquo;이나 &amp;lsquo;스파이더&amp;rsquo;라는 이름으로도 불리며, 검색 엔진을 비롯해 다양한 목적으로 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주 목적은 웹에 새롭게 올라오거나 갱신된 콘텐츠를 빠르고 정확하게 수집하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크롤러는 특정 웹 페이지들에서 시작해, 그 안의 하이퍼링크를 따라가며 콘텐츠를 순차적으로 수집해 나간다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;크롤러 활용 예시&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;활용 예시&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;검색 엔진 인덱싱&lt;/td&gt;
&lt;td&gt;검색 엔진의 로컬 인덱스를 구축하기 위해 크롤러를 사용한다. 대표적인 예시가 구글의 google bot이다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;웹 아카이빙&lt;/td&gt;
&lt;td&gt;웹페이지들을 주기적으로 수집, 저장하여 아카이빙하는 용도다. 각국 국립 도서관 등에서 많이 사용한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;웹 마이닝&lt;/td&gt;
&lt;td&gt;대량의 웹 데이터를 분석해 의미 있는 지식이나 패턴을 도출한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;웹 모니터링&lt;/td&gt;
&lt;td&gt;저작권, 상표권 침해나 특정 이슈의 모니터링 등에도 활용된다. (예: Digimarc의 웹 크롤러)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 크롤러의 복잡도는 &lt;b&gt;처리해야 하는 데이터의 규모&lt;/b&gt;에 따라 천차만별이다.&lt;br /&gt;학급 과제 수준으로 짧게 끝낼 수도 있고, 별도의 엔지니어링 팀이 24시간 관리해야 하는 대형 프로젝트가 될 수도 있다.&lt;br /&gt;따라서 크롤러 설계에서 가장 중요한 첫 단계는 &lt;b&gt;목표로 하는 데이터의 규모와, 필요한 주요 기능&lt;/b&gt;을 정확하게 정의하는 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1단계. 문제 이해 및 설계 범위 확정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;웹크롤러의 기본 동작 원리&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;입력으로 URL 집합이 주어진다.&lt;/li&gt;
&lt;li&gt;해당 URL들이 가리키는 모든 웹 페이지를 다운로드한다.&lt;/li&gt;
&lt;li&gt;각 페이지에서 새로운 URL을 추출한다.&lt;/li&gt;
&lt;li&gt;추출한 URL을 다시 수집할 URL 목록에 추가하며 이 과정을 반복한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉보기에는 단순해 보이지만, 대규모 시스템으로 확장할 때 고려해야 할 변수들이 매우 많다. 따라서 설계 전, 아래와 같은 질문을 통해 요구사항을 확정하고, 설계 범위를 정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;질문 예시&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;크롤러의 주된 용도는 무엇인가요? 검색 엔진 인덱스 생성용인가요? 아니면 데이터 마이닝? 아니면 그 외의 다른 용도가 있나요?&lt;/li&gt;
&lt;li&gt;매달 얼마나 많은 웹 페이지를 수집해야 하나요?&lt;/li&gt;
&lt;li&gt;새로 만들어진 웹 페이지나 수정된 웹 페이지도 고려해야 하나요?&lt;/li&gt;
&lt;li&gt;수집한 웹 페이지는 저장해야 합니까? 저장 기간은 어떻게 되나요?&lt;/li&gt;
&lt;li&gt;중복된 콘텐츠는 어떻게 해야 하나요?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;웹 크롤러는 아래와 같은 속성을 가져야 한다.&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;속성&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;규모 확장성&lt;/td&gt;
&lt;td&gt;웹은 방대하기 때문에, 병렬 처리 등으로 대량의 데이터를 빠르게 처리해야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;안정성&lt;/td&gt;
&lt;td&gt;비정상적 입력(잘못된 HTML, 장애 서버, 악성코드 등)에도 견딜 수 있어야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;예절(Politeness)&lt;/td&gt;
&lt;td&gt;수집 대상 서버에 부담을 주지 않도록 요청 빈도를 조절해야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;확장성&lt;/td&gt;
&lt;td&gt;새로운 타입의 데이터(예: 이미지) 등도 쉽게 수집할 수 있어야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개략적 규모 추정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접관과의 질문/답변을 통해서 아래와 같은 규모 추정이 되었다고 가정한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;값 및 산출 근거&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;월별 다운로드 웹페이지 수&lt;/td&gt;
&lt;td&gt;10억 (1,000,000,000) 개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QPS (초당 요청수)&lt;/td&gt;
&lt;td&gt;10억 &amp;divide; 30 &amp;divide; 24 &amp;divide; 3600 ≒ 400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;피크 QPS&lt;/td&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;평균 웹페이지 크기&lt;/td&gt;
&lt;td&gt;500KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;월별 전체 데이터&lt;/td&gt;
&lt;td&gt;10억 x 500KB = 500TB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5년치 데이터&lt;/td&gt;
&lt;td&gt;500TB x 12 x 5 = 30PB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2단계 개략적 설계안 제시 및 동의 구하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 다이어그램에서 각 컴포넌트가 어떤 역할을 하는지, 그리고 전체 작업의 흐름을 함께 살펴본다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1224&quot; data-origin-height=&quot;802&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HmjOL/btsPf3Hikz4/fC1wIkePxeINGCKgHlYfC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HmjOL/btsPf3Hikz4/fC1wIkePxeINGCKgHlYfC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HmjOL/btsPf3Hikz4/fC1wIkePxeINGCKgHlYfC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHmjOL%2FbtsPf3Hikz4%2FfC1wIkePxeINGCKgHlYfC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;590&quot; height=&quot;387&quot; data-origin-width=&quot;1224&quot; data-origin-height=&quot;802&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시작 URL 집합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 크롤러는 &amp;lsquo;시작 URL 집합&amp;rsquo;을 출발점으로 삼는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순하게는 특정 대학 사이트처럼 한정된 도메인의 모든 페이지를 대상으로 시작 URL을 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;만약 전 세계의 웹을 대상으로 한다면, &lt;b&gt;주제별로 다른 시작 URL을 선택&lt;/b&gt;하거나, URL 공간을 여러 부분집합으로 나누어 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 어떤 URL을 시작점으로 사용할지에 대한 정답은 없으며, &amp;ldquo;왜 이런 방식으로 시작 URL을 뽑았는지&amp;rdquo; 그 의도를 설계 단계에서 잘 설명할 수 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;미수집 URL 저장소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 현대 웹 크롤러는 크롤링 상태를 2가지로 나눠 관리한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;다운로드할 URL&lt;/li&gt;
&lt;li&gt;다운로드된 URL&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 첫 번째 상태를 저장하는 컴포넌트를 미수집 URL 저장소 (URL frontier)라고 부른다.&lt;br /&gt;FIFO 큐라고 생각하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML 다운로더&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹페이지를 실제로 다운로드하는 컴포넌트. 다운로드 대상 URL은 앞서 말한 URL Frontier에서 받아온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도메인 이름 변환기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL로 접근하려면 먼저 &lt;b&gt;도메인 이름 &amp;rarr; IP 주소&lt;/b&gt; 변환이 필요하다.&lt;br /&gt;HTML 다운로더는 도메인 이름 변환기를 호출해 해당 URL의 IP를 얻어온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;콘텐츠 파서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운로드한 HTML을 파싱(구문 해석)하고, 올바른 형식인지 검증한다.&lt;br /&gt;&lt;b&gt;콘텐츠 파서를 별도 컴포넌트로 분리&lt;/b&gt;하는 이유는, 파싱 과정이 느려질 경우 크롤링 전체 성능에 영향을 줄 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중복 콘텐츠인가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹의 29%가량이 중복 콘텐츠라는 연구 결과가 있다.&lt;br /&gt;중복 저장을 막으려면, 수집한 HTML이 이미 저장된 적이 있는지 빠르게 판단해야 한다.&lt;br /&gt;이를 위해 &lt;b&gt;해시 함수&lt;/b&gt; 등을 활용해 콘텐츠 중복 여부를 빠르게 검사한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;콘텐츠 저장소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크롤러가 수집한 HTML, 혹은 기타 데이터를 저장하는 공간이다.&lt;br /&gt;설계에서는 데이터의 양, 유형, 접근 빈도, 유효 기간 등을 고려해&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;대부분은 디스크 저장&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;인기 콘텐츠는 메모리에 보관&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등의 계층형 저장 구조를 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;URL 추출기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장한 HTML 문서들을 파싱해 링크들을 골라내는 역할.&lt;br /&gt;상대경로는 전부 절대경로로 변환해 준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;662&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buQ89m/btsPhysiNce/I6HtIZcBTSLrBygt5cJcnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buQ89m/btsPhysiNce/I6HtIZcBTSLrBygt5cJcnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buQ89m/btsPhysiNce/I6HtIZcBTSLrBygt5cJcnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuQ89m%2FbtsPhysiNce%2FI6HtIZcBTSLrBygt5cJcnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;540&quot; height=&quot;306&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;662&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;URL 필터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 추출한 URL을 아래 기준에 따라 걸러낸다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 파일 타입/확장자(예:. jpg,. zip 등)는 제외&lt;/li&gt;
&lt;li&gt;접근이 불가능한(오류 발생) URL은 제외&lt;/li&gt;
&lt;li&gt;접근 제한 목록(deny list)에 포함된 URL은 제외&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이미 방문한 URL?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일 URL을 여러 번 수집하지 않으려면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;이미 방문한 URL 저장소&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;미수집 URL 저장소&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 자료구조를 잘 구현해 추적한다.&lt;br /&gt;블룸필터나 해시 테이블 등 메모리 효율적인 구조가 많이 쓰인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;URL 저장소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL 저장소는 이미 방문한 URL을 보관하는 저장소다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 작업 흐름&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;844&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJsnXV/btsPf2BCuwA/sXC0iisa7cLwgy1JqaHROK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJsnXV/btsPf2BCuwA/sXC0iisa7cLwgy1JqaHROK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJsnXV/btsPf2BCuwA/sXC0iisa7cLwgy1JqaHROK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJsnXV%2FbtsPf2BCuwA%2FsXC0iisa7cLwgy1JqaHROK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;584&quot; height=&quot;401&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;844&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;시작 URL들을 미수집 URL 저장소에 저장한다.&lt;/li&gt;
&lt;li&gt;HTML 다운로더는 미수집 URL 저장소에서 URL 목록을 가져온다.&lt;/li&gt;
&lt;li&gt;HTML 다운로더는 도메인 이름 변환기를 사용하여 URL의 IP 주소를 알아내고, 해당 IP 주소로 접속하여 웹 페이지를 다운받는다.&lt;/li&gt;
&lt;li&gt;콘텐츠 파서는 다운된 HTML 페이지를 파싱 하여 올바른 형식을 갖춘 페이지인지 검증한다.&lt;/li&gt;
&lt;li&gt;콘텐츠 파싱과 검증이 끝나면 중복 콘텐츠인지 확인하는 절차를 개시한다.&lt;/li&gt;
&lt;li&gt;중복 콘텐츠인지 확인하기 위해서, 해당 페이지가 이미 저장소에 있는지 본다. 이미 저장소에 있는 콘텐츠인 경우에는 처리하지 않고 버린다. 저장소에 없는 콘텐츠인 경우에는 저장소에 저장한 뒤 URL 추출기로 전달한다.&lt;/li&gt;
&lt;li&gt;URL 추출기는 해당 HTML 페이지에서 링크를 골라낸다.&lt;/li&gt;
&lt;li&gt;골라낸 링크를 URL 필터로 전달한다.&lt;/li&gt;
&lt;li&gt;필터링이 끝나고 남은 URL만 중복 URL 판별 단계로 전달한다.&lt;/li&gt;
&lt;li&gt;이미 처리한 URL인지 확인하기 위하여, URL 저장소에 보관된 URL인지 살핀다. 이미 저장소에 있는 URL은 버린다.&lt;/li&gt;
&lt;li&gt;저장소에 없는 URL은 URL 저장소에 저장할 뿐 아니라 미수집 URL 저장소에도 전달한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3단계. 상세 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 중요한 컴포넌트 &amp;amp; 구현 기술을 상세하게 보자&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DFS vs BFS&lt;/li&gt;
&lt;li&gt;미수집 URL 저장소&lt;/li&gt;
&lt;li&gt;HTML 다운로더&lt;/li&gt;
&lt;li&gt;안정성 확보 전략&lt;/li&gt;
&lt;li&gt;확장성 확보 전략&lt;/li&gt;
&lt;li&gt;문제 있는 콘텐츠 감지 및 회피 전략&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DFS를 쓸 것인가, BFS를 쓸 것인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹은 유향 그래프나 같다. 페이지는 노드, 하이퍼링크는 엣지라고 보면 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 크롤링 == 그래프를 탐색하는 과정이다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DFS, BFS&lt;/code&gt;는 바로 이 그래프 탐색에 널리 사용되는 두 가지 알고리즘이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만 &lt;code&gt;DFS&lt;/code&gt;는 좋은 선택이 아닐 가능성이 높다. 그래프 크기가 클 경우 어느 정도로 깊숙이 가게 될지 가늠하기 어려워서다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 웹 크롤러는 보통 &lt;code&gt;BFS&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐를 사용하는 알고리즘으로, 한쪽으로는 탐색할 URL을 집어넣고, 다른 한쪽으로는 꺼내기만 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 구현법에는 두 가지 문제점이 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;한 페이지에서 추출한 링크 대부분이 같은 호스트(서버)로 향하는 경우가 많아, 특정 서버에 짧은 시간에 여러 요청이 몰릴 수 있다. 이것은 크롤러 예절(politeness)에 위배된다.&lt;/li&gt;
&lt;li&gt;기본 BFS 큐는 우선순위를 다루지 못한다. 실제로는 페이지의 중요도(예: PageRank), 트래픽 양, 업데이트 빈도 등 여러 기준을 반영해 탐색 순서를 조절해야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;미수집 URL 저장소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미수집 URL 저장소를 활용하면 위의 문제를 좀 쉽게 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 살펴본 대로 URL 저장소는 다운로드할 URL을 보관하는 장소다.&lt;br /&gt;이 저장소를 잘 구현하면 politeness를 갖추면서, 우선순위를 구별하는 크롤러를 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예의&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일 웹사이트에 대해서는 한 번에 한 페이지만 요청한다. 같은 페이지를 다운로드하는 태스크는 시간차를 두고 실행한다.&lt;br /&gt;구현 방법   웹사이트의 호스트명과 다운로드를 수행하는 작업 스레드 사이의 관계를 유지하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 다운로드 스레드는 별도 FIFO 큐를 가지고 있어서 해당 큐에서 꺼낸 URL 만 다운로드한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호스트별로 들어가는 큐를 지정해 주면 됨. 같은 큐에서 나온 것은 동시에 접속하지 않는다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;queue router: 같은 호스트에 속한 URL은 언제나 같은 큐로 가도록 보장하는 역할&lt;/li&gt;
&lt;li&gt;mapping table: 호스트 이름과 큐 사이의 관계를 보관하는 테이블&lt;/li&gt;
&lt;li&gt;FIFO 큐: 같은 호스트에 속한 URL은 언제나 같은 큐에 보관된다.&lt;/li&gt;
&lt;li&gt;queue selector: 큐 선택기는 큐들을 순회하면서 큐에서 URL을 꺼내서 해당 큐에서 나온 URL을 다운로드하도록 지정된 작업스레드에 전달하는 역할을 한다.&lt;/li&gt;
&lt;li&gt;작업스레드(worker thread): 작업 스레드는 전달된 URL을 다운로드하는 작업을 수행한다. &lt;br /&gt;전달된 URL은 순차적으로 처리될 것이며, 작업들 사이에는 일정한 지연시간을 둘 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;우선순위&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유용성에 따라 URL의 우선순위를 나눌 때는 페이지 랭크, 트래픽 양, 갱신 빈도 등 다양한 척도를 사용할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;본 절에서 설명할 &lt;b&gt;prioritizer&lt;/b&gt;는 URL 우선순위를 정하는 컴포넌트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐에 URL을 저장하기 전에 prioritizer(순위 결정 장치)를 거치도록 설계를 변경한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;순위 결정 장치: URL을 입력으로 받아 우선순위를 계산.&lt;/li&gt;
&lt;li&gt;우선순위 별로 큐가 하나씩 할당된다. 우선순위가 높으면 &lt;b&gt;선택될 확률&lt;/b&gt;도 올라간다.&lt;/li&gt;
&lt;li&gt;큐 선택기: 임의 큐에서 처리할 URL을 꺼낸다. 순위가 높은 큐에서 더 자주 꺼내도록 프로그램되어있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 둘을 반영한 전체 설계는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전면 큐 (front queue): 우선순위 결정 과정을 처리한다.&lt;/li&gt;
&lt;li&gt;후면 큐 (back queue): 크롤러의 politeness를 보장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;1412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dTvZcR/btsPgALjwZn/CFZ3BpRZ05aK0e4wQJYqD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dTvZcR/btsPgALjwZn/CFZ3BpRZ05aK0e4wQJYqD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTvZcR/btsPgALjwZn/CFZ3BpRZ05aK0e4wQJYqD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdTvZcR%2FbtsPgALjwZn%2FCFZ3BpRZ05aK0e4wQJYqD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;395&quot; height=&quot;630&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;1412&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;신선도&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹페이지는 수시로 추가되고, 삭제되고, 변경되므로 데이터의 신선함(freshness)를 유지하기 위해서 이미 다운로드한 페이지라고 해도 재수집할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 작업을 최적화하기 위한 전략으로는 다음과 같은 것들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 페이지의 변경 이력 활용&lt;/li&gt;
&lt;li&gt;우선순위가 높은 페이지는 더 자주 재수집&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;미수집 URL 저장소를 위한 지속성 저장장치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 시스템은 수억 개의 URL을 다루므로,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;모든 URL을 메모리에 저장&lt;/b&gt;: 안정성&amp;middot;확장성에 문제&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모든 URL을 디스크에 저장&lt;/b&gt;: 느려서 성능 저하&lt;/li&gt;
&lt;li&gt;&lt;b&gt;절충안&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디스크에 대부분의 URL을 저장&lt;/li&gt;
&lt;li&gt;메모리에는 &amp;lsquo;버퍼 큐&amp;rsquo;를 두어, 자주 쓰는 URL만 관리&lt;/li&gt;
&lt;li&gt;버퍼에서 데이터가 일정량 쌓이면 디스크로 flush&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML 다운로더&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML 다운로더는 실제로 HTTP 요청을 보내 페이지를 내려받는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Robots.txt&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로봇 제외 프로토콜이라고도 불린다. 웹사이트가 크롤러와 소통하는 표준.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;웹사이트마다 &lt;b&gt;robots.txt&lt;/b&gt; 파일을 통해 &amp;ldquo;크롤러가 어디까지 접근해도 되는가&amp;rdquo;를 명시한다.&lt;br /&gt;따라서 크롤러는 사이트를 다운로드하기 전에 해당 파일에 나열된 규칙을 먼저 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 호스트에서 Robots.txt 파일을 중복으로 다운로드하는 것을 피하기 위해, 이 파일은 주기적으로 다시 다운로드하여 캐싱한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;성능 최적화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML 다운로더에 사용할 수 있는 성능 최적화 기법들&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 분산 크롤링 &lt;br /&gt;성능을 높이기 위해 크롤링 작업을 여러 서버에 분산.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 도메인 이름 변환 결과 캐시 &lt;br /&gt;DNS resolver는 크롤러 성능의 병목 중 하나인데, 이는 DNS 요청을 보내고 결과를 받는 작업의 동기적 특성 때문이다.&lt;br /&gt;스레드는 DNS 요청의 결과를 받기 전까지는 다음 작업을 진행할 수 없다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크롤러 스레드 가운데 어느 하나라도 DNS resolver에 요청을 보내면, 이 요청이 완료될 때까지 다른 스레드의 요청이 모두 block 된다.&lt;br /&gt;따라서 DNS 조회 결과로 얻어진 도메인 이름과 그에 상응하는 IP 주소를 캐시에 보관, 주기적으로 갱신하도록 구현하여 성능을 높일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 지역성&lt;br /&gt;크롤링 작업을 수행하는 서버를 지역별로 분산하는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크롤링 서버가 크롤링 대상 서버와 지역적으로 가까우면 페이지 다운로드 시간이 줄어들 것이다. 지역성을 활용하는 전략은 크롤서버, 캐시, 큐, 저장소 등 대부분의 컴포넌트에 적용 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 짧은 타임아웃&lt;br /&gt;어떤 웹서버는 응답이 느리거나 아예 응답하지 않는데, 이런 경우를 대비해&lt;br /&gt;크롤러가 최대 얼마나 기다릴지를 미리 정해두는 것이다. 타임아웃의 경우 해당 페이지 다운로드를 중단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;안정성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안정성은 성능 최적화만큼 중요하게 고려해야 할 부분이다.&lt;br /&gt;시스템 안정성을 향상하기 위한 접근법 중 몇 가지를 보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;안정 해시(consistent hashing): 다운로더 서버(분산 HTML 다운로더)들에 부하를 고르게 분산하기 위해 적용가능한 기술.&lt;/li&gt;
&lt;li&gt;크롤링 상태 및 수집 데이터 저장: 장애가 발생한 경우에도 쉽게 복구할 수 있도록
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;크롤링 상태&lt;/li&gt;
&lt;li&gt;수집된 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 지속적 저장장치에 기록해 두는 것이 바람직하다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;예외처리: 예외가 발생해도 전체 시스템이 중단되지 않도록 미리 처리&lt;/li&gt;
&lt;li&gt;데이터 검증: 시스템 오류를 방지하기 위한 중요 수단. (비정상정 입력이나 환경을 대비)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;확장성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 요구사항에서 이미지를 예로 든 것처럼, 이런 시스템을 설계할 때는 새로운 형태의 콘텐츠를 쉽게 지원할 수 있도록 신경써야한다. 본 예제의 경우에는 새로운 모듈을 끼워 넣음으로써 새로운 형태의 컨텐츠를 지원할 수 있도록 설계하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/U3IPT/btsPhrz7Cod/VKL4Gc3RhTqdfcAijfHEaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/U3IPT/btsPhrz7Cod/VKL4Gc3RhTqdfcAijfHEaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/U3IPT/btsPhrz7Cod/VKL4Gc3RhTqdfcAijfHEaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FU3IPT%2FbtsPhrz7Cod%2FVKL4Gc3RhTqdfcAijfHEaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;598&quot; height=&quot;334&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PNG 다운로더는 PNG 파일을 다운로드하는 plug-in 모듈이다&lt;/li&gt;
&lt;li&gt;웹 모니터는 웹을 모니터링하여 저작권이나 상표권이 침해되는 일을 막는 모듈이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 있는 콘텐츠 감지 및 회피&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;중복 콘텐츠(웹 컨텐트의 30%가량은 중복)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해시&lt;/li&gt;
&lt;li&gt;체크섬&lt;br /&gt;등의 방법을 사용해 중복 콘텐츠를 보다 쉽게 탐지할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;거미 덫(spider trap)&lt;br /&gt;크롤러를 무한루프에 빠뜨리도록 설계한 웹 페이지다.&lt;br /&gt;&lt;code&gt;spidertrapexample.com/foo/bar/foo/bar/foo/bar/...&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;URL의 최대 길이를 제한 (모든 경우를 다 피할 수는 없음..)&lt;/li&gt;
&lt;li&gt;그래서 사람이 수작업으로 찾아낸 후 이런 사이트를 크롤러 탐색 대상에서 제외하거나 URL 필터 목록에 걸어둔다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;데이터 노이즈&lt;br /&gt;어떤 콘텐츠는 거의 가치가 없다.&lt;br /&gt;(광고, 스크립트 코드 등) 이런 콘텐츠를 가능한 제외 해야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4단계. 마무리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;좋은 크롤러가 갖추어야 하는 특성&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;특성&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;규모 확장성&lt;/b&gt; (Scalability)&lt;/td&gt;
&lt;td&gt;수억~수십억 개의 웹 페이지를 수집&amp;middot;처리할 수 있도록 분산 시스템 구조와 확장 가능한 저장소 설계가 필수다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;예의&lt;/b&gt; (Politeness)&lt;/td&gt;
&lt;td&gt;크롤러는 대상 서버에 과도한 부하를 주지 않도록, 한 호스트에 일정 시간 이상 간격을 두고 접근한다. robots.txt 등 업계 표준을 반드시 지킨다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;확장성&lt;/b&gt; (Extensibility)&lt;/td&gt;
&lt;td&gt;새로운 콘텐츠 유형(이미지, PDF, 동영상 등)이 추가될 때 시스템 전체를 뜯어고치지 않고도, 플러그인 모듈 등으로 손쉽게 기능을 확장할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;안정성&lt;/b&gt; (Robustness)&lt;/td&gt;
&lt;td&gt;예외 상황(네트워크 장애, 데이터 중복, 크롤러 자체 장애 등)에서도 서비스가 멈추지 않도록 장애 대응과 데이터 무결성, 자동 복구 기능을 갖춘다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규모 확장성이 뛰어난 웹 크롤러 설계 작업은 단순하지 않다. 웹이 워낙 방대한 데다, 수없이 많은 덫이 도사리고 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가로 논의하면 좋은 설계 이슈&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 서버 측 렌더링(Server-side Rendering)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 많은 웹사이트가 자바스크립트, AJAX 등 클라이언트 기술을 활용해 &lt;b&gt;페이지를 &amp;lsquo;실시간&amp;rsquo;으로 조립&lt;/b&gt;해서 보여준다.&lt;br /&gt;즉, HTML을 그대로 받아와 파싱 해봤자 실제로 사용자가 보는 콘텐츠나 링크(동적 내비게이션, 무한 스크롤, 팝업 등)는 나타나지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하려면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 측에서 실제로 브라우저를 실행시키듯 &lt;b&gt;JS 실행 환경&lt;/b&gt;을 만들어&lt;/li&gt;
&lt;li&gt;해당 페이지를 렌더링(rendering) 한 후&lt;/li&gt;
&lt;li&gt;그 결과 HTML에서 링크를 추출하는 전략을 쓴다.&lt;br /&gt;대표적으로 Selenium, Puppeteer, Playwright 같은 &lt;b&gt;Headless Browser&lt;/b&gt;를 활용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 원치 않는 페이지 필터링&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;품질이 낮거나 스팸성 페이지(광고, 미끼 사이트 등)는 데이터베이스에 저장하기 전에 &lt;br /&gt;&lt;b&gt;스팸 필터 컴포넌트&lt;/b&gt;를 둬서 자동 감지&amp;middot;제거하도록 구현하면 데이터 품질을 높일 수 있다.&lt;/li&gt;
&lt;li&gt;머신러닝이나 규칙 기반 필터 등 다양한 방법을 활용할 수 있다.&lt;/li&gt;
&lt;li&gt;이 컴포넌트는 보통 &lt;b&gt;콘텐츠 파서 &amp;rarr; 데이터 저장소&lt;/b&gt; 단계에서 삽입한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 데이터베이스 다중화 및 샤딩&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크롤러가 수집한 데이터의 양이 방대하다면 &lt;b&gt;DB 다중화(Replication)와&lt;/b&gt; &lt;b&gt;샤딩(Sharding)을&lt;/b&gt; 활용해&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 저장소의 &lt;b&gt;가용성&lt;/b&gt;(장애 대비)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;규모 확장성&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안정성&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 확보할 수 있다.&lt;br /&gt;예를 들어 인기 많은 도메인별로 데이터를 분산 저장하거나, 해시 기반 샤딩으로 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 수평적 규모 확장(Stateless &amp;amp; Scale-out)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 크롤러의 각 컴포넌트(다운로더, 파서 등)를 &lt;b&gt;무상태(stateless) 서버&lt;/b&gt;로 설계하면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버를 추가하는 것만으로도 처리량을 쉽게 늘릴 수 있다.&lt;/li&gt;
&lt;li&gt;분산 큐, 분산 파일시스템, 메시지 중개인 등을 활용하면 서버 간 상태 동기화도 용이해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 가용성, 일관성, 안정성(ACID vs. BASE)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;가용성(Availability)&lt;/b&gt;: 일부 노드에 장애가 생겨도 시스템이 중단되지 않도록 설계한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관성(Consistency)&lt;/b&gt;: 데이터가 여러 저장소에 분산되어 있을 때, 각 노드 간 데이터 불일치가 생기지 않도록 동기화한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안정성(Reliability)&lt;/b&gt;: 장애 발생 시 데이터 유실을 막고, 빠른 복구가 가능하도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6. 데이터 분석 솔루션(Analytics)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크롤러의 데이터를 &lt;b&gt;분석&lt;/b&gt;하여&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유용한 정보 추출(트렌드, 인기도, 스팸 탐지 등)&lt;/li&gt;
&lt;li&gt;크롤링 정책/전략 자동화&lt;/li&gt;
&lt;li&gt;데이터 정제/요약&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등에 활용할 수 있다.&lt;br /&gt;대표적으로 대시보드, 로그 분석, 실시간 모니터링 시스템과의 연동이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;+ 스터디원과 나눴던 내용들&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  URL의 우선순위를 나눌 때 트래픽의 양을 알아낼 수 있는 방법이 뭐가 있을까요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유튜브 같은 경우는 조회수나, 좋아요 수..? 구글 해서 나오는 검색 결과 수?&lt;br /&gt;그 외에는 잘 모르겠다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  다른 언어들도 많은데 주로 크롤링하는데 왜 파이썬을 사용할까요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬이 크롤러 개발에 가장 널리 사용되는 이유는 풍부한 라이브러리 생태계와 간결한 문법 때문이다.&lt;br /&gt;BeautifulSoup, Scrapy, Requests, Selenium 등 강력한 크롤링/파싱 라이브러리를 손쉽게 사용할 수 있고, 데이터 처리와 머신러닝까지 연동이 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 짧고 생산성이 높아서 빠른 프로토타이핑과 유지보수가 유리하며, 대규모 분산 크롤링도 Celery, asyncio 같은 프레임워크를 활용해 효율적으로 구현할 수 있다.&lt;br /&gt;또한, 파이썬은 커뮤니티와 자료가 많아 에러 해결이나 샘플 코드 확보가 쉽고, 웹 자동화/테스트와도 자연스럽게 연동된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  요즘은 SPA 프레임워크를 사용해서 동적 페이지를 주로 구성하는 데 그럼 크롤링은 어떻게 하는지?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Selenium 같은 툴을 활용해 동적 콘텐츠를 렌더링 해서 크롤링한다.&lt;br /&gt;서버에서 API 형태로 데이터를 내려주는 숨은 엔드포인트를 분석해 직접 호출하는 방식도 병행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;br /&gt;&lt;a href=&quot;https://velog.io/@kyy00n/%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%88-9%EC%9E%A5.-%EC%9B%B9-%ED%81%AC%EB%A1%A4%EB%9F%AC-%EC%84%A4%EA%B3%84&quot;&gt;https://velog.io/@kyy00n/%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%88-9%EC%9E%A5.-%EC%9B%B9-%ED%81%AC%EB%A1%A4%EB%9F%AC-%EC%84%A4%EA%B3%84&lt;/a&gt;&lt;/p&gt;</description>
      <category>   Dev/System Design</category>
      <author>현주먹</author>
      <guid isPermaLink="true">https://javacatcher.tistory.com/231</guid>
      <comments>https://javacatcher.tistory.com/231#entry231comment</comments>
      <pubDate>Mon, 14 Jul 2025 01:03:20 +0900</pubDate>
    </item>
    <item>
      <title>8장. URL 단축기 설계</title>
      <link>https://javacatcher.tistory.com/230</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001033116&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초&lt;/a&gt;를 읽고 정리한 글입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 책 내용을 들어가기에 앞서 왜 대규모 시스템에서 URL을 단축해야 할까? 의문이 들어 찾아봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 표는 단축 URL의 주요 장점과 설명을 정리한 것이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;공간 절약 및 가독성 향상&lt;/td&gt;
&lt;td&gt;SNS(예: 트위터 280자 제한)나 문자메시지(SMS)처럼 짧은 메시지 환경에서는 긴 URL이 불편을 초래한다. 단축 URL은 글자 수를 절약하고, 깔끔하게 링크를 전달할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;클릭 추적 및 분석&lt;/td&gt;
&lt;td&gt;언제, 어디서, 누가 클릭했는지 등 실시간 데이터 추적이 가능하다. 국가/도시별, 시간대별, 디바이스&amp;middot;브라우저별, 유입경로까지 세분화된 분석이 가능하다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;마케팅 효과 측정&lt;/td&gt;
&lt;td&gt;다양한 채널에 뿌려진 링크별로 성과를 비교할 수 있고, 메시지별 A/B 테스트나 ROI(투자대비수익률) 계산이 가능하다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;링크 관리 및 업데이트&lt;/td&gt;
&lt;td&gt;일부 서비스는 리디렉션 대상 URL을 변경할 수 있어서 웹사이트 구조 변경, 오타 수정, 시즌별 캠페인 등에 유용하게 쓸 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;주소 마스킹 기능&lt;/td&gt;
&lt;td&gt;원래 URL을 감춰 보안이나 브랜딩 관점에서 유리하다. 민감한 정보 노출을 막거나, 짧고 인상적인 링크를 쓸 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스팸 및 봇 클릭 방지&lt;/td&gt;
&lt;td&gt;비정상 트래픽(봇 등)을 필터링해 광고비 낭비를 막고, 통계의 정확성을 높일 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 여러 장점이 있지만, 내가 생각하기에 단축 URL의 가장 큰 강점은 &lt;b&gt;가독성을 높여주는 점&lt;/b&gt;과 &lt;b&gt;클릭 추적을 통해 마케팅 효과를 극대화할 수 있다는 점&lt;/b&gt;이라고 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 요즘 토스, 무신사, 당근, 지그재그 같은 서비스들에서 사용자들끼리 링크를 공유해 유입률을 높이는 마케팅 전략을 많이 사용하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250713222708.png&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;1524&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVTpk6/btsPf04Sb5P/sNN923rd7SUGngjQMMQTK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVTpk6/btsPf04Sb5P/sNN923rd7SUGngjQMMQTK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVTpk6/btsPf04Sb5P/sNN923rd7SUGngjQMMQTK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVTpk6%2FbtsPf04Sb5P%2FsNN923rd7SUGngjQMMQTK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;333&quot; height=&quot;425&quot; data-filename=&quot;Pasted image 20250713222708.png&quot; data-origin-width=&quot;1194&quot; data-origin-height=&quot;1524&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분에서 단축 URL을 사용하면 큰 이점이 있지 않을까? 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;개발 커뮤니티 단톡방에서 본 이야기인데,&lt;br /&gt;요즘 이런 이벤트를 진행하는 이유는 사용자가 이벤트에 참여할 때 약관 동의 과정에서 데이터를 수집하는 것이 목적이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시에 DAU를 증가시켜 광고 단가를 올리기 위한 목적도 있다고 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhgeM5/btsPfJI147b/K8m80ktiKpCxrqdiGPqKu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhgeM5/btsPfJI147b/K8m80ktiKpCxrqdiGPqKu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhgeM5/btsPfJI147b/K8m80ktiKpCxrqdiGPqKu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhgeM5%2FbtsPfJI147b%2FK8m80ktiKpCxrqdiGPqKu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;307&quot; height=&quot;107&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;실제로 수집하는 항목이 꽤 많다.(이벤트 참가할 때 유의 깊게 봐야 할 듯..)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;참고한 글&lt;br /&gt;&lt;a href=&quot;https://vivoldi.com/blog/url-shortener/why-use-long-links-to-be-shortened&quot;&gt;왜 긴 링크를 짧게 줄여서 사용할까? 단축URL을 사용하는 이유!&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://vivoldi.com/blog/url-shortener/track-realtime-clicks&quot;&gt;단축 URL을 사용하여 실시간 클릭 수를 추적하는 방법&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1단계 문제 이해 및 설계 범위 확정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 설계 면접 문제는 의도적으로 어떤 정해진 결말을 갖지 않도록 만들어진다 따라서 면접장에서 시스템을 성공적으로 설계해 내려면 질문을 통해 모호함을 줄이고 요구사항을 알아내야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;질문 예시&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떻게 동작해야하는지 예제를 보여주실 수 있을까요?&lt;/li&gt;
&lt;li&gt;트래픽 규모는 어느정도 일까요?&lt;/li&gt;
&lt;li&gt;단축 URL의 길이는 어느 정도여야 하나요?&lt;/li&gt;
&lt;li&gt;단축 URL에 포함될 문자에 제한이 있습니까?&lt;/li&gt;
&lt;li&gt;단축 URL을 시스템에서 지우거나 갱신할 수 있습니까?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시스템의 기본적 기능은 아래와 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;URL 단축 : 주어진 긴 URL을 훨씬 짧게 줄인다.&lt;/li&gt;
&lt;li&gt;URL 리다이렉션(redirection): 축약된 URL로 HTTP 요청이 오면 원래 URL로 안내&lt;/li&gt;
&lt;li&gt;높은 가용성과 규모 확장성, 그리고 장애 감내가 요구됨&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개략적 추정&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쓰기 연산: 매일 1억 개의 단축 URL 생성&lt;/li&gt;
&lt;li&gt;초당 쓰기 연산: 1억 / 24 / 3600 = 1160&lt;/li&gt;
&lt;li&gt;읽기 연산: 읽기 연산과 쓰기 연산 비율은 10:1이라고 하자. 그 경우 읽기 연산은 초당 11,600회 발생한다(1160 x 10 = 11,600)&lt;/li&gt;
&lt;li&gt;URL 단축 서비스를 10년간 운영한다고 가정하면 1억 x 365 x 10 = 3650억 개의 레코드를 보관해야 한다.&lt;/li&gt;
&lt;li&gt;축약 전 URL의 평균 길이는 100이라고 하자.&lt;/li&gt;
&lt;li&gt;따라서 10년 동안 필요한 저장 용량 3650억 x 100바이트 = 36.5TB이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위 개략적 추정은 질문을 통해 답변받은 것을 통해서 계산하는 것이며 계산이 끝나면 결과를 면접관과 점검하여 합의한 후에 진행하도록 하자.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2단계 개략적 설계안 제시 및 동의 구하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API 엔드포인트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST API로 설계하는 걸 가정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL 단축기는 기본적으로 두 개의 엔드포인트를 필요로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. URL 단축용 엔드포인트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POST &lt;code&gt;/api/v1/data/shorten&lt;/code&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인자: { longUrl: longURLstring }&lt;/li&gt;
&lt;li&gt;반환: 단축 URL&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. URL 리다이렉션용 엔드포인트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GET &lt;code&gt;/api/v1/shortUrl&lt;/code&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반환: HTTP 리다이렉션 목적지가 될 원래 URL&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;URL 리다이렉션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음의 그림은 브라우저에 단축 URL을 입력하면 무슨 일이 생기는지 보여준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1192&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wX70L/btsPfxIQGGt/KjTklXnefhp2WK1NWrslK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wX70L/btsPfxIQGGt/KjTklXnefhp2WK1NWrslK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wX70L/btsPfxIQGGt/KjTklXnefhp2WK1NWrslK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwX70L%2FbtsPfxIQGGt%2FKjTklXnefhp2WK1NWrslK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;501&quot; height=&quot;248&quot; data-origin-width=&quot;1192&quot; data-origin-height=&quot;590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;단축 URL을 받은 서버는 그 URL을 원래 URL로 바꾸어서 301 응답의 Location 헤더에 넣어 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음의 그림은 클라이언트와 서버 사이의 통신 절차를 좀 더 자세히 보여준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;764&quot; data-origin-height=&quot;756&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rpKO4/btsPfJPPb2N/N3GkIjksbXcDUCqAp537uK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rpKO4/btsPfJPPb2N/N3GkIjksbXcDUCqAp537uK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rpKO4/btsPfJPPb2N/N3GkIjksbXcDUCqAp537uK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrpKO4%2FbtsPfJPPb2N%2FN3GkIjksbXcDUCqAp537uK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;407&quot; height=&quot;403&quot; data-origin-width=&quot;764&quot; data-origin-height=&quot;756&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;301 응답 VS 302 응답&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;301 응답과 302 응답 둘 다 리다이렉션 응답이지만, 차이가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;301 Permanetly Moved&lt;/code&gt;&lt;br /&gt;이 응답은 해당 URL에 대한 HTTP 요청의 처리 책임이 영구적으로 Location 헤더에 반환된 URL로 이전되었다는 응답이다.&lt;br /&gt;영구적으로 이전되었으므로, 브라우저는 이 응답을 캐시 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 추후 같은 단축 URL에 요청을 보낼 필요가 있을 때 브라우저는 캐시 된 원래 URL로 요청을 보내게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;302 Found&lt;/code&gt;&lt;br /&gt;이 응답은 주어진 URL로의 요청이 &lt;b&gt;일시적으로 Location 헤더가 지정하는 URL에 의해 처리되어야 한다는 응답이다.&lt;/b&gt;&lt;br /&gt;따라서 클라이언트의 요청은 언제나 단축 URL 서버에 먼저 보내진 후에 원래 URL로 리다이렉션 되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 방법은 각기 다른 장단점을 갖고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 부하를 줄이는 것이 중요하다면 `301 Permanent Moved`를 사용하는 것이 좋은데 첫 번째 요청만 단축 URL 서버로 전송될 것이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 트래픽 분석이 중요할 때는 `302 Found`를 쓰는 쪽이 클릭 발생률이나 발생 위치를 추적하는 데 좀 더 유리할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL 리다이렉션을 구현하는 가장 직관적인 방법은 해시 테이블을 사용하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 테이블에 &amp;lt;단축 URL, 원래 URL&amp;gt;의 쌍을 저장한다고 가정한다면, URL 리다이렉션은 다음과 같이 구현될 수 있을 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원래 URL = hashTable.get(단축 URL)&lt;/li&gt;
&lt;li&gt;301 또는 302 응답 Location 헤더에 원래 URL을 넣은 후 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;URL 단축&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단축 URL이 `&lt;a href=&quot;http://www.tinyurl.com/%7BhashValue%7D&quot;&gt;www.tinyurl.com/{hashValue}&lt;/a&gt;` 같은 형태라고 해 보자.&lt;br /&gt;결국 중요한 것은 긴 URL을 이 해시 값으로 대응시킬 해시 함수 fx를 찾는 일이 될 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;520&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tMRiL/btsPgAdu7LT/VVuVNS68c0hs6KY3Utsm50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tMRiL/btsPgAdu7LT/VVuVNS68c0hs6KY3Utsm50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tMRiL/btsPgAdu7LT/VVuVNS68c0hs6KY3Utsm50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtMRiL%2FbtsPgAdu7LT%2FVVuVNS68c0hs6KY3Utsm50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;313&quot; height=&quot;238&quot; data-origin-width=&quot;520&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 해시 함수는 다음 요구사항을 만족해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력으로 주어지는 긴 URL이 다른 값이면 해시 값도 달라야 한다.&lt;/li&gt;
&lt;li&gt;계산된 해시 값은 원래 입력으로 주어졌던 긴 URL로 복원될 수 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3단계 상세설계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 모델&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급했듯이 모든 것을 해시 테이블에 두었었다. 이 접근법은 초기 전략으로는 괜찮지만 실제 시스템에 쓰기에는 곤란한데, 메모리는 유한한 데다 비싸기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 나은 방법은 `&amp;lt;단축 URL, 원래 URL&amp;gt;`의 순서쌍을 관계형 데이터베이스에 저장하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 그림이 테이블의 간단한 설계 사례다.&lt;br /&gt;이 테이블은 단순화된 것으로 `id`, `shortUrl`, `longURL`의 세 개 칼럼을 갖는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfENXS/btsPgi5bJ9p/Gk2D4VGvkFqY3m1ejlmI50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfENXS/btsPgi5bJ9p/Gk2D4VGvkFqY3m1ejlmI50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfENXS/btsPgi5bJ9p/Gk2D4VGvkFqY3m1ejlmI50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfENXS%2FbtsPgi5bJ9p%2FGk2D4VGvkFqY3m1ejlmI50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;193&quot; height=&quot;157&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;456&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해시 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 함수는 원래 URL을 단축 URL로 변환하는데 쓰인다. 편의상 해시 함수가 계산하는 단축 URL값을 `hashValue`라고 지칭한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;자릿수 정하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL에 담길 수 있는 영역은 숫자와 영문자이다. `([0-9, a-z, A-Z])`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 사용할 수 있는 문자의 개수는 10 + 26 + 26 = 62개이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 3650억 개의 레코드를 보관할 수 있도록 하는 것을 고려해야 하기 때문에 몇 자리의 문자가 필요한지 계산해 보면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x25uJ/btsPhN3PUbD/I1AuFSHCM2LH9nzv7PybK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x25uJ/btsPhN3PUbD/I1AuFSHCM2LH9nzv7PybK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x25uJ/btsPhN3PUbD/I1AuFSHCM2LH9nzv7PybK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx25uJ%2FbtsPhN3PUbD%2FI1AuFSHCM2LH9nzv7PybK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;404&quot; height=&quot;219&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;따라서 7자리로 3.5조 개의 URL을 커버할 수 있다.&lt;br /&gt;hashValue의 길이는 7로 하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;해시 함수 구현에 쓰일 기술로는 두 가지 방법을 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나는 해시 후 충돌 해소 방법이고, 다른 하나는 base-62 변환 법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;해시 후 충돌 해소&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 URL을 줄이려면, 원래 URL을 7글자 문자열로 줄이는 해시 함수가 필요하다.&lt;br /&gt;손쉬운 방법은 CRC32, MD5, SHA-1같이 잘 알려진 해시 함수를 이용하는 것이다.&lt;/p&gt;
&lt;table style=&quot;height: 116px;&quot; width=&quot;487&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;th style=&quot;height: 21px; width: 163px;&quot;&gt;해시 함수&lt;/th&gt;
&lt;th style=&quot;height: 21px; width: 672px;&quot;&gt;해시 결과 (16진수)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 163px;&quot;&gt;CRC32&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 672px;&quot;&gt;5cb54054&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 163px;&quot;&gt;MD5&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 672px;&quot;&gt;5a62509a84df9ee03fe1230b9dfb84e&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 163px;&quot;&gt;SHA-1&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 672px;&quot;&gt;0eeae7916c06853901d9ccbefbfcaf4de57ed85b&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 위의 표와 같이, CRC32가 계산한 가장 짧은 해시값조차도 7보다는 길다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 하면 줄일 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결할 첫 번째 방법은 계산된 해시 값에서 처음 7개 글자만 이용한느 것이다.&lt;br /&gt;&lt;b&gt;하지만 이렇게 하면 해시 결과가 서로 충돌할 확률이 높아진다.&lt;/b&gt;&lt;br /&gt;충돌이 실제로 발생했을 때는, 충돌이 해소될 때까지 사전에 정한 문자열을 해시값에 덧붙인다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;772&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GXuC1/btsPg6Qwk7t/HkJUTyEtTiZnnFktNS8EM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GXuC1/btsPg6Qwk7t/HkJUTyEtTiZnnFktNS8EM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GXuC1/btsPg6Qwk7t/HkJUTyEtTiZnnFktNS8EM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGXuC1%2FbtsPg6Qwk7t%2FHkJUTyEtTiZnnFktNS8EM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;466&quot; height=&quot;358&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;772&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 방법을 쓰면 충돌은 해소할 수 있지만 단축 URL을 생성할 때 한 번 이상 데이터베이스 질의를 해야 하므로 오버헤드가 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 대신 블룸 필터를 사용하면 성능을 높일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;base-62 변환&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진법 변환은 URL 단축기를 구현할 때 흔히 사용되는 접근법 중 하나다.&lt;br /&gt;이 기법은 수의 표현 방식이 다른 두 시스템이 같은 수를 공유하여야 하는 경우에 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;62진법을 쓰는 이유는 hashValue에 사용할 수 있는 문자 개수가 62개이기 때문이다.&lt;br /&gt;`1115710`을 62진수로 변환해 보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;62진법은 수를 표현하기 위해 62개의 문자를 사용하는 진법이다. &lt;br /&gt;따라서 0은 0으로 9는 9로, 10은 a로 61은 Z로 대응시켜 표현하도록 할 것이다. 따라서 62진법에서 &amp;lsquo;a&amp;rsquo;는 10을 나타내고 &amp;lsquo;Z&amp;rsquo;는 61을 나타낸다.&lt;/li&gt;
&lt;li&gt;1115710 = 2 x 622 + 55 X 621 + 59 x 620 = [2, 55, 59] =&amp;gt; [2, T, X] =&amp;gt; 2 TX62이다.&lt;/li&gt;
&lt;li&gt;따라서 단축 URL은 `&lt;a href=&quot;https://tinyurl.com/2TX%EA%B0%80&quot;&gt;https://tinyurl.com/2TX`가&lt;/a&gt; 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두 접근법 비교&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;해시 후 충돌 해소 전략&lt;/th&gt;
&lt;th&gt;&lt;b&gt;base-62 변환&lt;/b&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;단축 URL의 길이가 고정됨&lt;/td&gt;
&lt;td&gt;단축 URL의 길이가 가변적. ID 값이 커지면 같이 길어짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;유일성이 보장되는 ID 생성기가 필요치 않음&lt;/td&gt;
&lt;td&gt;유일성 보장 ID 생성기가 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;충돌이 가능해서 해소 전략이 필요&lt;/td&gt;
&lt;td&gt;ID의 유일성이 보장된 후에야 적용 가능한 전략이라 충돌은 아예 불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ID로부터 단축 URL을 계산하는 방식이 아니라서 다음에 쓸 수 있는 URL을 알아내는 것이 불가능&lt;/td&gt;
&lt;td&gt;ID가 1씩 증가하는 값이라고 가정하면 다음에 쓸 수 있는 단축 URL이 무엇인지 쉽게 알아낼 수 있어서 보안상 문제가 될 소지가 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;URL 단축기 상세 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL 단축기는 시스템의 핵심 컴포넌트이므로, 그 처리 흐름이 논리적으로는 단순해야 하고 기능적으로는 언제나 동작하는 상태로 유지되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 책에서는 62진법(base62) 변환 기법을 사용해 설계한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;758&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pOe9B/btsPhRd5MGj/06UPVSqP7JqG7qxekb1NW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pOe9B/btsPhRd5MGj/06UPVSqP7JqG7qxekb1NW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pOe9B/btsPhRd5MGj/06UPVSqP7JqG7qxekb1NW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpOe9B%2FbtsPhRd5MGj%2F06UPVSqP7JqG7qxekb1NW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;455&quot; height=&quot;342&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;758&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;입력으로 긴 URL을 받는다.&lt;/li&gt;
&lt;li&gt;데이터베이스에 해당 URL이 있는지 검사한다.&lt;/li&gt;
&lt;li&gt;데이터베이스에 있다면 해당 URL에 대한 단축 URL을 만든 적이 있다는 것이다. 따라서 데이터베이스에서 해당 단축 URL을 가져와서 클라이언트에게 반환한다.&lt;/li&gt;
&lt;li&gt;데이터베이스에 없는 경우에는 해당 URL은 새로 접수된 것이므로 유일한 ID를 생성한다. 이 ID는 데이터베이스의 기본 키로 사용된다.&lt;/li&gt;
&lt;li&gt;62진법 변환을 적용, ID를 단축 URL로 만든다.&lt;/li&gt;
&lt;li&gt;ID, 단축 URL, 원래 URL로 새 데이터베이스 레코드를 만든 후 단축 URL을 클라이언트에 전달한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;URL 리다이렉션 상세 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기보다 읽기를 더 자주 하는 시스템이라, 캐시에 저장하여 성능을 높였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPxvUj/btsPfOcuV5y/AGz4lPPYzi1zCT5tGEWbAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPxvUj/btsPfOcuV5y/AGz4lPPYzi1zCT5tGEWbAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPxvUj/btsPfOcuV5y/AGz4lPPYzi1zCT5tGEWbAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPxvUj%2FbtsPfOcuV5y%2FAGz4lPPYzi1zCT5tGEWbAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;444&quot; height=&quot;225&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;로드밸런서의 동작 흐름은 다음과 같이 요약할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 단축 URL을 클릭한다.&lt;/li&gt;
&lt;li&gt;로드밸런서가 해당 클릭으로 발생한 요청을 웹 서버에 전달한다.&lt;/li&gt;
&lt;li&gt;단축 URL이 이미 캐시에 있는 경우에는 원래 URL을 바로 꺼내서 클라이언트에게 전달한다.&lt;/li&gt;
&lt;li&gt;캐시에 해당 단축 URL이 없는 경우에는 데이터베이스에서 꺼낸다. 데이터베이스에 없다면 아마 사용자가 잘못된 단축 URL을 입력한 경우일 것이다.&lt;/li&gt;
&lt;li&gt;데이터베이스에서 꺼낸 URL을 캐시에 넣은 후 사용자에게 반환한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4단계 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설계를 마친 후에도 시간이 좀 남는다면 다음과 같은 것을 면접관과 이야기할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;처리율 제한 장치(rate limiter)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지금까지 살펴본 시스템은 엄청난 양의 URL 단축 요청이 있을 경우 무력화될 수 있다는 잠재적 보안 결함을 갖고 있다. 처리율 제한 장치를 두면, IP 주소를 비롯한 필터링 규칙들을 이용해 요청을 걸러낼 수 있을 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;웹 서버의 규모 확장&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;본 설계에 포함된 웹 계층은 무상태 계층이므로, 웹 서버를 자유롭게 증설하거나 삭제할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터베이스의 규모 확장&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스를 다중화하거나 샤딩(sharding)하여 규모 확장성을 달성할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터 분석 솔루션(analytics)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성공적인 비즈니스를 위해서는 데이터가 중요하다. URL 단축기에 데이터 분석 솔루션을 통합해 두면 어떤 링크를 얼마나 많은 사용자가 클릭했는지, 언제 주로 클릭했는지 등 중요한 정보를 알아낼 수 있을 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;가용성, 데이터 일관성, 안정성&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대규모 시스템이 성공적으로 운영되기 위해서는 반드시 갖추어야 할 속성들이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;+ 회고 시 스터디원들과 나눴던 내용들&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;단축 URL을 빠르게 찾기 위해서 캐시를 사용하지만 캐시에 없다면 RDB에서 찾아야 하는데 읽기 성능이 중요할 거 같은데 mongodb 같은 nosql 사용은 어떻게 생각하시나요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단축 URL 시스템에서 대부분의 요청은 캐시(예: Redis, Memcached)를 통해 빠르게 처리되지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시에 없는 경우에는 결국 데이터베이스에서 원본 URL을 찾아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;초당만 건 이상의 읽기 요청과 수십~수백억 건의 레코드를 고려할 때, 데이터베이스의 읽기 성능과 확장성이 굉장히 중요한 요소가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDB도 인덱싱과 리드 레플리카를 통해 어느 정도까지는 성능을 낼 수 있지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 수십 테라바이트 규모로 커지면 샤딩 및 분산 처리가 필요해지고, 운영 복잡성이 급격히 높아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 키-값 단순 조회 패턴에서는 관계형의 고도 기능(조인, 트랜잭션 등)이 거의 필요하지 않기 때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때는 MongoDB 같은 NoSQL이 훨씬 적합한 선택이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.reddit.com/r/aws/?f=flair_name%3A%22technical%20question%22&quot;&gt;What database to use for URL Shortener project&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;유명한 서비스나 오픈소스가 있는지&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://zapier.com/blog/best-url-shorteners/&quot;&gt;The 7 best URL shortener services&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;왜 대규모 시스템에서 URL을 단축해야 할까요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서두에 서술!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;br /&gt;&lt;a href=&quot;https://jonghoonpark.com/2023/06/15/url-shortener&quot;&gt;https://jonghoonpark.com/2023/06/15/url-shortener&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://haril.dev/blog/2023/08/16/URL-Shortener&quot;&gt;직접 구현해 보는 URL 단축기&lt;/a&gt;&lt;/p&gt;</description>
      <category>   Dev/System Design</category>
      <author>현주먹</author>
      <guid isPermaLink="true">https://javacatcher.tistory.com/230</guid>
      <comments>https://javacatcher.tistory.com/230#entry230comment</comments>
      <pubDate>Mon, 14 Jul 2025 00:54:04 +0900</pubDate>
    </item>
    <item>
      <title>'면접을 위한 CS 전공지식 노트'를 읽고</title>
      <link>https://javacatcher.tistory.com/228</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;588&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001834833&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBT6N4/btsO5GebCb4/eMdRp209cQKrERsTcC6wTK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBT6N4%2FbtsO5GebCb4%2FeMdRp209cQKrERsTcC6wTK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;287&quot; height=&quot;368&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;588&quot;/&gt;&lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1;&quot; href=&quot;https://product.kyobobook.co.kr/detail/S000001834833&quot;&gt;[책 링크]&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;  목차&lt;/b&gt;&lt;br /&gt;1장. 디자인 패턴과 프로그래밍 패러다임&lt;br /&gt;2장. 네트워크&lt;br /&gt;3장. 운영체제&lt;br /&gt;4장. 데이터베이스&lt;br /&gt;5장. 자료 구조&lt;br /&gt;6장. 포트폴리오와 면접&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;192&quot; data-start=&quot;165&quot; data-ke-size=&quot;size26&quot;&gt;면접 일주일 전 복기하기 좋다.&lt;/h2&gt;
&lt;p data-end=&quot;309&quot; data-start=&quot;193&quot; data-ke-size=&quot;size16&quot;&gt;딱 책 소개에 적혀있듯이, 면접 일주일 전에 가볍게 읽으면서 CS 복기하기 좋은 책이다.&lt;/p&gt;
&lt;p data-end=&quot;309&quot; data-start=&quot;193&quot; data-ke-size=&quot;size16&quot;&gt;또는 CS를 공부하고 싶은 입문자들에게 좋을 것 같다.&lt;/p&gt;
&lt;p data-end=&quot;309&quot; data-start=&quot;193&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;309&quot; data-start=&quot;193&quot; data-ke-size=&quot;size16&quot;&gt;디자인 패턴, 네트워크, 운영체제, 데이터베이스, 자료구조, 그리고 마지막에 포트폴리오와 면접 팁까지 핵심이 딱 담겨있다.&lt;/p&gt;
&lt;p data-end=&quot;309&quot; data-start=&quot;193&quot; data-ke-size=&quot;size16&quot;&gt;그리고 마지막에 &quot;이건 이렇게 물어볼 수 있다 &amp;rarr; 이렇게 답하면 된다&quot;이런 흐름으로 알려주는데, CS보다 이 부분이 도움이 됐다.&lt;/p&gt;
&lt;p data-end=&quot;309&quot; data-start=&quot;193&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;309&quot; data-start=&quot;193&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;693&quot; data-start=&quot;665&quot; data-ke-size=&quot;size26&quot;&gt;깊이 있는 공부에는 부족할 수 있다&lt;/h2&gt;
&lt;p data-end=&quot;857&quot; data-start=&quot;694&quot; data-ke-size=&quot;size16&quot;&gt;나는 이미 알고 있던 내용을 다시 정리하고 싶어서 이 책을 읽었는데,&lt;br /&gt;오히려 어디가 약한지, 뭐는 설명할 수 있고 뭐는 막히는지를 점검하기에 딱 좋았다.&lt;/p&gt;
&lt;p data-end=&quot;857&quot; data-start=&quot;694&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span&gt;입문자에게 좋지만, &quot;이걸로 면접 대비를 할 수 있다?&quot; 는 잘 모르겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;857&quot; data-start=&quot;694&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;깊이보다는 폭을 넓히는 데 초점이 맞춰져 있기 때문에, 실제 면접에서 꼬리질문이 이어졌을 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;깊게 파고들기엔 부족할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;857&quot; data-start=&quot;694&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;요즘 신입도 허들이 너무 높아져서 이 책만 읽어서는 기술 면접 대비 못한다고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;857&quot; data-start=&quot;694&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;857&quot; data-start=&quot;694&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1453&quot; data-start=&quot;1437&quot; data-ke-size=&quot;size26&quot;&gt;  이런 사람에게 추천&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1595&quot; data-start=&quot;1455&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1484&quot; data-start=&quot;1455&quot;&gt;전공 지식을 정리하고 싶은 신입/주니어 개발자&lt;/li&gt;
&lt;li data-end=&quot;1520&quot; data-start=&quot;1485&quot;&gt;CS 면접을 앞두고 빠르게 전 범위를 복습하고 싶은 사람&lt;/li&gt;
&lt;li data-end=&quot;1549&quot; data-start=&quot;1521&quot;&gt;CS 공부도 하면서, 포폴/면접 팁도 챙기고 싶은 사람&lt;/li&gt;
&lt;li data-end=&quot;1595&quot; data-start=&quot;1550&quot;&gt;깊이 있는 개념은 다른 책에서 다지고, 요약/정리는 이 책으로 하고 싶은 사람&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  끄적끄적/후기 및 회고</category>
      <author>현주먹</author>
      <guid isPermaLink="true">https://javacatcher.tistory.com/228</guid>
      <comments>https://javacatcher.tistory.com/228#entry228comment</comments>
      <pubDate>Sat, 5 Jul 2025 02:54:42 +0900</pubDate>
    </item>
    <item>
      <title>7장. 분산 시스템을 위한 유일 ID 생성기 설계</title>
      <link>https://javacatcher.tistory.com/227</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;분산 시스템에서 유일한 ID를 생성하는 일은 생각보다 복잡하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 RDBMS의 &lt;code&gt;auto_increment&lt;/code&gt;를 사용하면 될 것 같지만, 서버가 분산되기 시작하면 곧 병목과 충돌의 문제가 나타난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 그러한 문제를 해결하기 위한 다양한 접근법을 소개하고, 그중에서도 트위터의 Snowflake 알고리즘을 중심으로 설계를 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 서버 한 대로는 그 요구를 감당할 수 없을뿐더러, 여러 데이터베이스 서버를 쓰는 경우에는 지연시간(delay)을 낮추기가 무척 힘들 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  여러 데이터베이스 서버를 쓰는 경우에 지연시간을 낮추기가 힘들다는 게 &lt;span data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;무슨말일까?&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;무슨 말일까?&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot; data-grammar-id=&quot;grammar1&quot; data-grammar-focus=&quot;false&quot;&gt;무슨말일까?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID 충돌을 피하려면 각 DB 서버가 생성하는 ID 범위를 미리 조정하거나, 중앙에서 ID를 발급받는 &lt;b&gt;티켓 서버 같은 구조&lt;/b&gt;를 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우에도 결국 ID 생성 요청은 &lt;b&gt;특정 서버 또는 중앙 로직을 반드시 거쳐야&lt;/b&gt; 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서 발생하는 문제는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;네트워크 통신 비용 증가&lt;/b&gt;: 모든 노드가 중앙 서버에 ID를 요청해야 하므로 요청/응답 과정에서 지연이 발생한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동기화 이슈&lt;/b&gt;: 서버 간 상태를 맞추기 위해 추가적인 동기화 작업이 필요하고, 이 또한 지연과 복잡성을 증가시킨다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장성 한계&lt;/b&gt;: 장애나 서버 확장 시 동기화 불일치 문제로 인해, 전체 시스템의 안정성과 응답 시간이 영향을 받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 지연시간을 낮추기 위해 각 서버가 &lt;b&gt;독립적으로 ID를 생성&lt;/b&gt;할 수 있는 구조가 이상적이지만, &lt;code&gt;auto_increment&lt;/code&gt; 방식은 애초에 &lt;b&gt;중앙 집중형 설계&lt;/b&gt;이기 때문에 이와 같은 분산 처리에는 적합하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 점에서 &lt;code&gt;auto_increment&lt;/code&gt;는 분산 시스템의 요구 사항과 충돌하게 된다는 말인 것 같다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1단계. 문제 이해 및 설계 범위 확정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 설계 면접 문제를 푸는 첫 단계는 적절한 질문을 통해 요구사항을 이해하고 모호함을 해소하여 설계 방향을 정하는 것이다.&lt;br /&gt;가령, 요구사항을 다음과 같이 &lt;span data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;정의해볼&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;정의해 볼&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot; data-grammar-id=&quot;grammar2&quot; data-grammar-focus=&quot;false&quot;&gt;정의해볼&lt;/span&gt; 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ID는 유일해야 한다.&lt;/li&gt;
&lt;li&gt;ID는 숫자로만 구성되어야 한다.&lt;/li&gt;
&lt;li&gt;ID는 64비트로 표현될 수 있는 값이어야 한다.&lt;/li&gt;
&lt;li&gt;ID는 발급 날짜에 따라 정렬 가능해야 한다.&lt;/li&gt;
&lt;li&gt;초당 10,000개의 ID를 만들 수 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2단계. 개략적 설계안 제시 및 동의 구하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 시스템에서 유일성이 보장되는 ID를 만드는 방법은 여러 가지다. 살펴볼 몇 가지 방법들은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다중 마스터 복제(multi-master replication)&lt;/li&gt;
&lt;li&gt;UUID(Universally Unique Identifier)&lt;/li&gt;
&lt;li&gt;티켓 서버(ticket server)&lt;/li&gt;
&lt;li&gt;트위터 스노플레이크(twitter snowflake) 접근법&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 다중 마스터 복제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다중 마스터 복제는 대략 다음 그림과 같은 구성을 갖는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701182925.png&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rhp4y/btsOY3uvsrU/AVSyU53tCdao0mZxqsA9P0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rhp4y/btsOY3uvsrU/AVSyU53tCdao0mZxqsA9P0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rhp4y/btsOY3uvsrU/AVSyU53tCdao0mZxqsA9P0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frhp4y%2FbtsOY3uvsrU%2FAVSyU53tCdao0mZxqsA9P0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;412&quot; height=&quot;214&quot; data-filename=&quot;Pasted image 20250701182925.png&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;518&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 접근법은 데이터베이스의 &lt;code&gt;auto_increment&lt;/code&gt; 기능을 활용하는 것이다.&lt;br /&gt;다만 ID의 값을 구할 때 1만큼 증가시켜 얻는 것이 아니라, k만큼 증가시킨다.&lt;br /&gt;여기서 k는 현재 사용 중인 데이터베이스 서버의 수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 그림의 예제를 보자.&lt;br /&gt;어떤 서버가 만들어 낼 다음 아이디는, 해당 서버가 생성한 이전 ID 값에 전체 서버의 수 2를 더한 값이다.&lt;br /&gt;이렇게 하면 규모 확장성 문제를 어느 정도 해결할 수 있는데, 데이터베이스 수를 늘리면 초당 생산 ID 수도 늘릴 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 방법은 다음과 같은 단점이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 데이터 센터에 걸쳐 규모를 늘리기 어렵다.&lt;/li&gt;
&lt;li&gt;ID의 유일성은 보장되겠지만 그 값이 시간 흐름에 맞추어 커지도록 보장할 수는 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버를 추가하거나 삭제할 때도 잘 동작하도록 만들기 어렵다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.UUID&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;128비트 크기의 전역적으로 유일한 문자열 ID를 생성한다. Java에서는 &lt;code&gt;UUID.randomUUID()&lt;/code&gt;를 사용하여 간단히 생성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;import java.util.UUID;

public class UUIDExample {
    public static void main(String[] args) {
        UUID id = UUID.randomUUID();
        System.out.println(id); // e.g., 09c93e62-50b4-468d-bf8a-c07e1040bfb2
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;만들게되면&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;만들게 되면&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot; data-grammar-id=&quot;grammar3&quot; data-grammar-focus=&quot;false&quot;&gt;만들게되면&lt;/span&gt; &lt;span data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;09c93e62-50b4-468d-bf8a-c07e1040bfb2와&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;09c93e62-50b4-468d-bf8a-c07e1040 bfb2와&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot; data-grammar-id=&quot;grammar4&quot; data-grammar-focus=&quot;false&quot;&gt;&lt;code&gt;09c93e62-50b4-468d-bf8a-c07e1040bfb2&lt;/code&gt;와&lt;/span&gt; 같은 형태를 띤다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701182939.png&quot; data-origin-width=&quot;1442&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m42MG/btsO0hZkYzU/Qyu18i7cyi6A0iY2RkSHCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m42MG/btsO0hZkYzU/Qyu18i7cyi6A0iY2RkSHCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m42MG/btsO0hZkYzU/Qyu18i7cyi6A0iY2RkSHCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm42MG%2FbtsO0hZkYzU%2FQyu18i7cyi6A0iY2RkSHCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;483&quot; height=&quot;133&quot; data-filename=&quot;Pasted image 20250701182939.png&quot; data-origin-width=&quot;1442&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 구조에서 각 웹 서버는 별도의 ID 생성기를 사용해 독립적으로 ID를 만들어낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UUID를 만드는 것은 단순하다. 서버 사이의 조율이 필요 없으므로 동기화 이슈도 없다.&lt;/li&gt;
&lt;li&gt;충돌 가능성이 매우 낮다.&lt;/li&gt;
&lt;li&gt;각 서버가 자기가 쓸 ID를 알아서 만드는 구조이므로 규모 확장도 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;단점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ID가 128비트로 길다. 이전의 ID 요구사항은 64비트이다.&lt;/li&gt;
&lt;li&gt;ID를 시간순으로 정렬할 수 없다.&lt;/li&gt;
&lt;li&gt;ID에 숫자가 아닌 값이 포함될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 티켓 서버&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중앙 집중식으로 운영되는 &lt;b&gt;ID 발급 전용 서버&lt;/b&gt;를 두고, 각 노드는 이 서버에 ID 요청을 보내도록 구성한다.&lt;br /&gt;(플리커가 분산 기본 키를 만들어 내기 위해 이 기술을 이용했다고 한다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701184638.png&quot; data-origin-width=&quot;594&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chuU2p/btsOZTSb2bh/iik0XsF20HDFhkAKiNlWhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chuU2p/btsOZTSb2bh/iik0XsF20HDFhkAKiNlWhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chuU2p/btsOZTSb2bh/iik0XsF20HDFhkAKiNlWhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchuU2p%2FbtsOZTSb2bh%2Fiik0XsF20HDFhkAKiNlWhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;393&quot; height=&quot;184&quot; data-filename=&quot;Pasted image 20250701184638.png&quot; data-origin-width=&quot;594&quot; data-origin-height=&quot;278&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 아이디어의 핵심은 &lt;code&gt;auto_increment&lt;/code&gt; 기능을 갖춘 데이터베이스 서버, 즉 티켓 서버를 중앙 집중형으로 하나만 사용하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유일성이 보장되는 숫자로만 구성된 ID를 쉽게 만들 수 있다.&lt;/li&gt;
&lt;li&gt;구현하기 쉽고, 중소 규모 애플리케이션에 적합하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;단점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;티켓 서버가 SPOF(Single-Point-of-Failure)가 된다.&lt;br /&gt;이 서버에 장애가 발생하면, 해당 서버를 이용하는 모든 시스템이 영향을 받는다. 이 이슈를 피하려면 티켓 서버를 여러 대 준비해야 한다. 하지만 &lt;span data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;그렇게하면&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;그렇게 하면&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot; data-grammar-id=&quot;grammar5&quot; data-grammar-focus=&quot;false&quot;&gt;그렇게하면&lt;/span&gt; 데이터 동기화 같은 새로운 문제가 발생할 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 트위터 Snowflake 알고리즘&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701182956.png&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/by263S/btsO1iQHFH8/hlKCHgRHMWtMRffDB75pnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/by263S/btsO1iQHFH8/hlKCHgRHMWtMRffDB75pnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/by263S/btsO1iQHFH8/hlKCHgRHMWtMRffDB75pnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fby263S%2FbtsO1iQHFH8%2FhlKCHgRHMWtMRffDB75pnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;538&quot; height=&quot;100&quot; data-filename=&quot;Pasted image 20250701182956.png&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;298&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Twitter가 설계한 64비트 기반의 고성능 분산 ID 생성 시스템이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID를 비트 단위로 나눠 구성하고, 각 비트에 명확한 의미를 부여해 유일성과 시간순 정렬을 보장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;비트 수&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;사인 비트 (Sign Bit)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1비트&lt;/td&gt;
&lt;td&gt;항상 0으로 설정. 현재는 사용되지 않으며 미래 확장을 위해 유보됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;타임스탬프 (Timestamp)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;41비트&lt;/td&gt;
&lt;td&gt;Epoch(기준 시각, 2010-11-04 01:42:54 UTC) 이후 경과한 밀리초를 나타냄. 시간 순 정렬이 가능해짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;데이터센터 ID (Data Center ID)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;5비트&lt;/td&gt;
&lt;td&gt;최대 32개의 데이터센터 구분 가능 (&lt;code&gt;2^5 = 32&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;서버 ID (Machine ID)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;5비트&lt;/td&gt;
&lt;td&gt;각 데이터센터에서 최대 32대의 서버 식별 가능. 총 1024대 (&lt;code&gt;32 * 32&lt;/code&gt;) 서버까지 확장 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;일련번호 (Sequence Number)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;12비트&lt;/td&gt;
&lt;td&gt;동일한 밀리초 내에서 생성되는 ID의 구분자. 최대 4096개까지 생성 가능 (&lt;code&gt;2^12 = 4096&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Snowflake는 총 64비트로 구성되어 있으며, 시간순 정렬 가능하고, 충돌 없이 초당 약 409만 개의 ID를 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3단계. 상세 설계&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;트위터 Snowflake 알고리즘을 기준으로 &lt;span data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;설계해보자.&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;설계해 보자.&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot; data-grammar-id=&quot;grammar6&quot; data-grammar-focus=&quot;false&quot;&gt;설계해보자.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터센터 ID와 서버 ID는 시스템이 시작할 때 고정적으로 설정&lt;/b&gt;된다.&lt;/li&gt;
&lt;li&gt;이 값들은 &lt;b&gt;운영 중에 바뀌지 않는 것을 전제로 설계&lt;/b&gt;되어야 한다.&lt;/li&gt;
&lt;li&gt;만약 데이터센터 ID나 서버 ID를 실수로 바꾸게 되면, &lt;b&gt;ID 충돌&lt;/b&gt;이 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;따라서 서버 증설이나 설정 변경 시에는 반드시 &lt;b&gt;ID의 중복 여부를 철저히 검증&lt;/b&gt;해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타임스탬프&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;타임스탬프는 ID 생성기가 작동할 때마다 &lt;b&gt;실시간으로 계산되는 값&lt;/b&gt;이며, 스노플레이크 ID에서 &lt;b&gt;가장 큰 비중인 41비트&lt;/b&gt;를 차지한다.&lt;/li&gt;
&lt;li&gt;이 값은 특정 기준 시점(Epoch)부터 &lt;b&gt;현재까지 경과한 밀리초 단위 시간&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;시간이 흐를수록 값이 커지므로, &lt;b&gt;생성된 ID를 시간순으로 정렬할 수 있는 특징&lt;/b&gt;이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음의 그림은 앞서 살펴본 ID 구조를 따르는 값의 이진 표현 형태로부터 UTC 시각을 추출하는 예제다.&lt;br /&gt;이 방법을 역으로 적용하면 어떤 UTC 시각도 상술한 타임스탬프 값으로 변환할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701183010.png&quot; data-origin-width=&quot;1524&quot; data-origin-height=&quot;824&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Pt7qf/btsOZjjv6kf/8NqPPNDj9q3IkVkiJnq331/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Pt7qf/btsOZjjv6kf/8NqPPNDj9q3IkVkiJnq331/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Pt7qf/btsOZjjv6kf/8NqPPNDj9q3IkVkiJnq331/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPt7qf%2FbtsOZjjv6kf%2F8NqPPNDj9q3IkVkiJnq331%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;498&quot; height=&quot;269&quot; data-filename=&quot;Pasted image 20250701183010.png&quot; data-origin-width=&quot;1524&quot; data-origin-height=&quot;824&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;타임스탬프 최대 표현 범위&lt;/h4&gt;
&lt;table style=&quot;height: 141px;&quot; width=&quot;502&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;width: 177px; height: 20px;&quot;&gt;항목&lt;/th&gt;
&lt;th style=&quot;width: 677px; height: 20px;&quot;&gt;값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 177px; height: 19px;&quot;&gt;비트 수&lt;/td&gt;
&lt;td style=&quot;width: 677px; height: 19px;&quot;&gt;41비트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 177px; height: 22px;&quot;&gt;최대값&lt;/td&gt;
&lt;td style=&quot;width: 677px; height: 22px;&quot;&gt;2&lt;sup&gt;41&lt;/sup&gt; - 1 = 2,199,023,255,551 (밀리초)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 177px; height: 19px;&quot;&gt;환산 시간&lt;/td&gt;
&lt;td style=&quot;width: 677px; height: 19px;&quot;&gt;약 69년&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 177px; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 677px; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;예를 들어, 2,199,023,255,551 밀리초는 약&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;69년에 해당하며, 이는 다음과 같은 계산으로 도출된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&lt;span data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;2199023255551ms&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;2199023255551 ms&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot; data-grammar-id=&quot;grammar7&quot; data-grammar-focus=&quot;false&quot;&gt;2199023255551ms&lt;/span&gt; &amp;divide; 1000(초) &amp;divide; 60 &amp;divide; 60 &amp;divide; 24 &amp;divide; 365 ≒ 69년&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 기원 시각을 현재 기준으로 설정한다면, &lt;b&gt;앞으로 약 69년간 오버플로 없이 안전하게 ID를 생성할 수 있다&lt;/b&gt;.&lt;br /&gt;단, 69년이 지나면 ID 생성이 불가능해지므로, 그 이전에 &lt;b&gt;기원 시각(epoch)을 조정하거나 새로운 ID 체계로 이관&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일련번호&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일련번호는 &lt;b&gt;동일한 밀리초 내에서 생성되는 ID들을 구분하기 위한 값&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;총 12비트가 할당되어 있으며, 표현 가능한 값의 범위는 &lt;code&gt;0 ~ 4095&lt;/code&gt; &amp;rarr; 즉 &lt;b&gt;&lt;span data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;1밀리초&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;1밀리 초&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot; data-grammar-id=&quot;grammar8&quot; data-grammar-focus=&quot;false&quot;&gt;1밀리초&lt;/span&gt; 안에 최대 4096개의 ID 생성 가능&lt;/b&gt;하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 상황에서는 타임스탬프만으로도 유일성이 확보되지만, &lt;b&gt;같은 밀리초 내에 다수의 ID를 만들어야 할 경우&lt;/b&gt; 이 일련번호가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 밀리초가 바뀔 때마다 이 값을 &lt;b&gt;0으로 초기화&lt;/b&gt;하고 다시 증가시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;약 어떤 밀리초 내에서 4096개 이상의 ID를 요청받게 되면, 다음 &lt;span data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;밀리초까지&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;밀리 초까지&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot; data-grammar-id=&quot;grammar9&quot; data-grammar-focus=&quot;false&quot;&gt;밀리초까지&lt;/span&gt; 대기(wait)하여 새로운 타임스탬프가 나오기를 기다린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4단계 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 장에서는 유일성이 보장되는 ID 생성기 구현에 쓰일 수 있는 다양한 전략을 알아보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 선택한 방식은 트위터 스노플레이크인데, 모든 요구사항을 만족하면서도 분산 환경에서 규모 확장이 가능했기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로, 이러한 설계를 &lt;span data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;진행시&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;진행 시&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot; data-grammar-id=&quot;grammar10&quot; data-grammar-focus=&quot;false&quot;&gt;진행시&lt;/span&gt; 다음과 같은 것들을 추가적으로 생각해봐야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시계 동기화(clock synchronization)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이번 스노플레이크 설계에서는 ID 생성 서버들이 전부 같은 시계를 사용한다고 가정하였다. 하지만 이런 가정은 하나의 서버가 여러 코어에서 실행될 경우 유효하지 않을 수 있다. 여러 서버가 물리적으로 독립된 여러 장비에서 실행되는 경우에도 마찬가지다. 이러한 문제점을 해결하는 보편적인 방법으로는 &lt;a href=&quot;https://en.wikipedia.org/wiki/Network_Time_Protocol&quot;&gt;NTP(Network Time Protocol)&lt;/a&gt;이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;각 section의 길이 최적화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 동시성(concurrency)이 낮고 수명이 긴 application이라면 일련번호 절의 길이를 줄이고 타임스탬프 절의 길이를 &lt;span data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;늘리는&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;늘이는&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;spell&amp;quot;}&quot; data-grammar-id=&quot;grammar11&quot; data-grammar-focus=&quot;false&quot;&gt;늘리는&lt;/span&gt; 것이 효과적일 수도 있을 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;고가용성(high availability)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ID 생성기는 필수 불가결(mission critical) 컴포넌트이므로 아주 높은 가용성을 제공해야 할 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;br /&gt;&lt;span data-grammar=&quot;{&amp;quot;input&amp;quot;:&amp;quot;https://donghyeon.dev//%EC%9D%B8%ED%94%84%EB%9D%BC/2022/04/02/%EB%B6%84%EC%82%B0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%97%90%EC%84%9C-%EC%9C%A0%EC%9D%BC-ID-%EC%83%9D%EC%84%B1%EA%B8%B0-%EC%84%A4%EA%B3%84/&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;https://donghyeon.dev//%EC% 9D% B8% ED%94%84% EB% 9D% BC/2022/04/02/%EB% B6%84% EC%82% B0-%EC% 8B% 9C% EC% 8A% A4% ED%85% 9C% EC%97%90% EC%84%9C-%EC% 9C% A0% EC% 9D% BC-ID-%EC%83% 9D% EC%84% B1% EA% B8% B0-%EC%84% A4% EA% B3%84/&amp;quot;,&amp;quot;etype&amp;quot;:&amp;quot;space&amp;quot;}&quot; data-grammar-id=&quot;grammar12&quot; data-grammar-focus=&quot;false&quot;&gt;&lt;a href=&quot;https://donghyeon.dev//%EC%9D%B8%ED%94%84%EB%9D%BC/2022/04/02/%EB%B6%84%EC%82%B0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%97%90%EC%84%9C-%EC%9C%A0%EC%9D%BC-ID-%EC%83%9D%EC%84%B1%EA%B8%B0-%EC%84%A4%EA%B3%84/&quot;&gt;https://donghyeon.dev//%EC%9D%B8%ED%94%84%EB%9D%BC/2022/04/02/%EB%B6%84%EC%82%B0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%97%90%EC%84%9C-%EC%9C%A0%EC%9D%BC-ID-%EC%83%9D%EC%84%B1%EA%B8%B0-%EC%84%A4%EA%B3%84/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>   Dev/System Design</category>
      <author>현주먹</author>
      <guid isPermaLink="true">https://javacatcher.tistory.com/227</guid>
      <comments>https://javacatcher.tistory.com/227#entry227comment</comments>
      <pubDate>Tue, 1 Jul 2025 19:27:53 +0900</pubDate>
    </item>
    <item>
      <title>6장. 키-값 저장소 설계</title>
      <link>https://javacatcher.tistory.com/226</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;키-값 저장소는 키-값 데이터베이스라고도 불리는 &lt;b&gt;비관계형 데이터베이스&lt;/b&gt;이며, 각 값은 고유 식별자인 키를 통해 접근한다.&lt;br /&gt;키는 일반 텍스트 혹은 해시 값일 수 있으며, &lt;b&gt;짧을수록 성능에 유리&lt;/b&gt;하다.&lt;br /&gt;값은 문자열, 리스트, 객체 등 어떤 것이든 가능하며, 저장소는 값의 형태를 따로 제한하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 예시로는 &lt;b&gt;Amazon Dynamo, Memcached, Redis&lt;/b&gt; 등이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요구사항 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 장에서는 다음 특성을 갖는 키-값 저장소를 설계해 볼 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;키-값 쌍의 크기는 10KB 이하&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;큰 데이터를 저장할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;높은 가용성을 제공&lt;/b&gt;해야 한다. 따라서 시스템은 설사 장애가 있더라도 빨리 응답해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;높은 규모 확장성을 제공&lt;/b&gt;해야 한다. 따라서 트래픽 양에 따라 자동적으로 서버 증설/삭제가 이루어져야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 일관성 수준은 조정이 가능&lt;/b&gt;해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;응답 지연시간(latency)이 짧아야&lt;/b&gt; 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;단일 서버 기반 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 대 서버만 사용하는 키-값 저장소를 설계하는 것은 쉽다.&lt;br /&gt;가장 직관적인 방법은 &lt;b&gt;키-값 쌍 전부를 메모리에 해시 테이블로 저장하는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 이 접근법은 &lt;b&gt;빠른 속도를 보장&lt;/b&gt;하지만 &lt;b&gt;모든 데이터를 메모리 안에 두는 것이 불가능할 수도 있다&lt;/b&gt;는 한계가 있다.&lt;br /&gt;이 문제를 해결하기 위한 개선책으로는 다음과 같은 것들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 압축(compression)&lt;/li&gt;
&lt;li&gt;자주 쓰이는 데이터만 메모리에 두고 나머지는 디스크에 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 이렇게 개선한다고 해도, 단일 서버로는 한계가 있어 결국 &lt;b&gt;분산 키-값 저장소&lt;/b&gt;로의 확장이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;분산 키-값 저장소&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 키-값 저장소는 분산 해시 테이블이라고도 한다. 키-값 쌍을 여러 서버에 분산시키기 때문이다.&lt;br /&gt;분산 시스템을 설계할 때는 `CAP 이론`을 이해하고 있어야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701155333.png&quot; data-origin-width=&quot;955&quot; data-origin-height=&quot;897&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgG2s3/btsO0vDg5Wa/BBNqKbCc6JQtdCgaJVMqk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgG2s3/btsO0vDg5Wa/BBNqKbCc6JQtdCgaJVMqk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgG2s3/btsO0vDg5Wa/BBNqKbCc6JQtdCgaJVMqk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgG2s3%2FbtsO0vDg5Wa%2FBBNqKbCc6JQtdCgaJVMqk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;267&quot; height=&quot;251&quot; data-filename=&quot;Pasted image 20250701155333.png&quot; data-origin-width=&quot;955&quot; data-origin-height=&quot;897&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 일관성, 가용성, 파티션 감내라는 세 가지 요구사항을 동시에 만족하는 분산 시스템을 설계하는 것은 불가능하다는 정리&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CAP 이론 세 가지 요소&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Consistency (일관성)&lt;/b&gt;: 모든 노드에서 같은 데이터를 조회 가능해야 함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Availability (가용성)&lt;/b&gt;: 일부 노드 장애에도 시스템이 응답을 제공해야 함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Partition Tolerance (파티션 감내)&lt;/b&gt;: 네트워크 단절이 생겨도 시스템이 계속 동작해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 가지 모두를 동시에 만족할 수는 없으며, 일반적으로 &lt;b&gt;파티션 감내(P)&lt;/b&gt;는 반드시 필요하므로 실제 구현에서는 CP 또는 AP 시스템 중 하나를 선택한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;시스템 유형 분류&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CP (일관성 + 파티션 감내)&lt;/b&gt;: 은행 시스템 등 강한 일관성이 중요한 곳 (&amp;rarr; 가용성 희생)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AP (가용성 + 파티션 감내)&lt;/b&gt;: SNS, 캐시 등에서 사용 (&amp;rarr; 일관성 희생)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CA&lt;/b&gt;: 이론적으로 가능하지만, 실전에서는 분산 환경에서 파티션 감내가 필요하므로 사실상 존재하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 시스템에서 데이터는 보통 여러 노드에 복제되어 보관된다. 세 대의 복제(replica) 노드 n1, n2, n3에 데이터를 복제하여 보관하는 상황을 가정해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이상적 상태&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상적 환경이라면 네트워크가 파티션 되는 상황은 절대로 일어나지 않을 것이고, n1에 기록된 데이터는 자동적으로 n2, n3에 복제되므로 일관성과 가용성도 만족한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701155555.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1068&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/li9DJ/btsO0UCw3eo/pkry6TpTJzMkSabXSL2g8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/li9DJ/btsO0UCw3eo/pkry6TpTJzMkSabXSL2g8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/li9DJ/btsO0UCw3eo/pkry6TpTJzMkSabXSL2g8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fli9DJ%2FbtsO0UCw3eo%2Fpkry6TpTJzMkSabXSL2g8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;240&quot; height=&quot;200&quot; data-filename=&quot;Pasted image 20250701155555.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1068&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 분산 시스템&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701155730.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1078&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sMOU2/btsOZQVsdc6/9s7R5sdjkkvZYxdO2tmemk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sMOU2/btsOZQVsdc6/9s7R5sdjkkvZYxdO2tmemk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sMOU2/btsOZQVsdc6/9s7R5sdjkkvZYxdO2tmemk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsMOU2%2FbtsOZQVsdc6%2F9s7R5sdjkkvZYxdO2tmemk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;244&quot; height=&quot;205&quot; data-filename=&quot;Pasted image 20250701155730.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1078&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;분산 시스템은 파티션 문제를 피할 수 없다. 그리고 파티션 문제가 발생하면 우리는 일관성과 가용성 사이에서 하나를 선택해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CP 시스템 (일관성, 파티션)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가용성 대신 일관성을 선택한다면 세 서버 사이에 생길 수 있는 데이터 불일치 문제를 피하기 위해 n1과 n2에 대해 &lt;b&gt;쓰기 연산을 중단시켜야 하는데, 그렇게 하면 가용성이 깨진다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;은행권 시스템은 보통 데이터 일관성을 양보하지 않는다.&lt;br /&gt;예를 들어, 온라인 뱅킹 시스템이 계좌 최신 정보를 출력할 수 있는 상황이 발생하면 이런 시스템은 상황이 해결될 때까지는 오류를 반환해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;AP 시스템(가용성, 파티션)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 일관성 대신 가용성을 선택한 시스템은 &lt;b&gt;설사 낡은 데이터를 반환할 위험이 있더라도 계속 읽기 연산을 허용해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아울러 n1과 n2는 계속 쓰기 연산을 허용할 것이고, 파티션 문제가 해결된 뒤에 새 데이터를 n3에 전송할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CA 시스템(일관성, 가용성)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 말했듯이, &lt;b&gt;CA 시스템은 분산 시스템에서 존재하지 않는다&lt;/b&gt;&lt;br /&gt;분산 시스템이 아닌 단일 시스템에서는 존재할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;키-값 저장소 핵심 컴포넌트 및 기술&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 파티션&lt;/li&gt;
&lt;li&gt;데이터 다중화(replication)&lt;/li&gt;
&lt;li&gt;일관성(consistency)&lt;/li&gt;
&lt;li&gt;일관성 불일치 해소(inconsistency resolution)&lt;/li&gt;
&lt;li&gt;장애 처리&lt;/li&gt;
&lt;li&gt;시스템 아키텍처 다이어그램&lt;/li&gt;
&lt;li&gt;쓰기 경로&lt;/li&gt;
&lt;li&gt;읽기 경로&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 파티션(Partitioning)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 데이터를 한 서버에 담는 건 비효율적이므로 데이터를 나눠 여러 서버에 저장한다. 다음 조건을 만족해야 한다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 &lt;b&gt;고르게 분산&lt;/b&gt;할 수 있을 것&lt;/li&gt;
&lt;li&gt;서버 추가/삭제 시 데이터 이동 최소화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5장에서 다룬 &lt;a href=&quot;https://javacatcher.tistory.com/225&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;안정 해시 설계&lt;/a&gt;는 이 문제를 푸는 데 적합한 기술이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 키를 같은 링 위에 배치하고, 시계 방향으로 순회하다 만나는 첫 번째 서버가 바로 해당 키-값 쌍을 저장할 서버다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;안정 해시를 사용하여 데이터를 파티션 하면 좋은 점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;규모 확장 자동화(automatic scaling) : 트래픽에 따라 서버 자동 증설/축소 가능&lt;/li&gt;
&lt;li&gt;다양성(heterogeneity) : 서버 성능에 따라 가상 노드 수를 다르게 설정 가능 다시 말해, 고성능서버는 더 많은 가상 노드를 갖도록 설정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 다중화(Replication)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;높은 가용성과 안정성을 확보하기 위해 데이터를 N개의 서버에 비동기적으로 복제한다.&lt;/li&gt;
&lt;li&gt;일반적으로 해시 링에서 키 위치를 기준으로 시계 방향으로 N개의 노드를 선택하여 사본을 저장한다. 지점으로부터 시계 방향으로 링을 순회하면서 만나는 첫 N개 서버에 데이터 사본을 보관하는 것이다.&lt;br /&gt;따라서 N=3으로 설정한 아래 그림에서 key0은 s1, s2, s3에 저장된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701160354.png&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;1034&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/55vQ2/btsO0IbfeUn/PpuP8rVXqCrAxGMQK1fBNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/55vQ2/btsO0IbfeUn/PpuP8rVXqCrAxGMQK1fBNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/55vQ2/btsO0IbfeUn/PpuP8rVXqCrAxGMQK1fBNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F55vQ2%2FbtsO0IbfeUn%2FPpuP8rVXqCrAxGMQK1fBNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;301&quot; height=&quot;299&quot; data-filename=&quot;Pasted image 20250701160354.png&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;1034&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 가상 노드를 사용한다면 위와 같이 선택한 N개의 노드가 대응될 실제 물리 서버의 개수가 N보다 작아질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 문제를 피하려면 노드를 선택할 때 같은 물리 서버를 중복 선택하지 않도록 해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  이게 무슨 말이지?&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, s0, s1, s2가 실제로는 모두 &lt;b&gt;서버 A 한 대에 존재하는 가상 노드들&lt;/b&gt;일 수 있다.&lt;br /&gt;이 경우 key0의 3개 복제본이 &lt;b&gt;물리적으로 같은 서버 A에 저장&lt;/b&gt;된다.&lt;br /&gt;즉, 논리적으로는 3개의 서로 다른 노드에 저장했지만 실제로는 1개의 물리 서버에만 저장될 수 있다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 되면 다음과 같은 문제가 발생한다&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;문제 상황&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;서버 장애&lt;/td&gt;
&lt;td&gt;서버 A 하나에 장애가 발생하면 key0의 모든 복제본이 함께 손실됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;복제 무효&lt;/td&gt;
&lt;td&gt;논리적으로는 3개의 노드에 저장했지만, 실제로는 1개 서버에 몰려 있어 &lt;b&gt;복제의 의미가 없어짐&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;고가용성 실패&lt;/td&gt;
&lt;td&gt;정전, 네트워크 단절, 자연재해 등 물리적 장애에 대응할 수 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 데이터 센터에 속한 노드는 정전, 네트워크 이슈, 자연재해 등의 문제를 동시에 겪을 가능성이 있다.&lt;br /&gt;따라서 안정성을 담보하기 위해 데이터의 사본은 다른 센터의 서버에 보관하고, 센터들은 고속 네트워크로 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 일관성(Consistency)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다중화된 데이터를 적절히 동기화하기 위해 &lt;b&gt;정족수 합의(Quorum Consensus)&lt;/b&gt;를 사용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;N&lt;/code&gt;: 복제본 수&lt;/li&gt;
&lt;li&gt;&lt;code&gt;W&lt;/code&gt;: 쓰기 성공으로 간주되기 위한 응답 수&lt;/li&gt;
&lt;li&gt;&lt;code&gt;R&lt;/code&gt;: 읽기 성공으로 간주되기 위한 응답 수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N = 3인 경우에 대한 그림을 살펴보자.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701160446.png&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;820&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pfP5s/btsO0EGJeEa/9rd5GfnmhH3jXjdX8kxsj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pfP5s/btsO0EGJeEa/9rd5GfnmhH3jXjdX8kxsj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pfP5s/btsO0EGJeEa/9rd5GfnmhH3jXjdX8kxsj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpfP5s%2FbtsO0EGJeEa%2F9rd5GfnmhH3jXjdX8kxsj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;388&quot; height=&quot;374&quot; data-filename=&quot;Pasted image 20250701160446.png&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;820&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. W = 1 일 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;W = 1은 데이터가 한 대 서버에만 기록된다는 뜻이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위의 그림과 같이 데이터가 서버 0, 서버 1 서버 2에 다중화되는 상황을 예를 들어 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;W = 1의 의미는, 쓰기 연산이 성공했다고 판단하기 위해 중재자(coordinator)는 최소 한 대 서버로부터 쓰기 성공 응답을 받아야 한다는 뜻이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;따라서 서버 1로부터 성공 응답을 받았다면 서버 0, 서버 2로부터의 응답을 기다릴 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중재자는 클라이언트 노드 사이에서 프락시(proxy) 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. W = 1 또는 R =1인 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;W = 1 또는 R = 1인 구성의 경우 중재자는 한 대 서버로부터의 응답만 받으면 되니 응답속도는 빠를 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;W나 R의 값이 1보다 큰 경우에는 시스템이 보여주는 데이터 일관성의 수준은 향상될 테지만 중재자의 응답 속도는 가장 느린 서버로부터의 응답을 기다려야 하므로 느려질 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. W + R &amp;gt; N&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;W + R &amp;gt; N인 경우에는 강한 일관성이 보장된다.&lt;br /&gt;일관성을 보증할 최신 데이터를 가진 노드가 최소 하나는 겹칠 것 이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;b&gt;  &lt;/b&gt;그럼 어떻게 정해야 할까?&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책에서는 가능한 몇 가지 구성을 제시한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;R = 1, W = N : 빠른 읽기 연산에 최적화된 시스템 (쓰기 응답은 복제본 수만큼 받기)&lt;/li&gt;
&lt;li&gt;W = 1, R = N : 빠른 쓰기 연산에 최적화된 시스템 (읽기 응답은 복제본 수 만큼 받기)&lt;/li&gt;
&lt;li&gt;W + R &amp;gt; N : 강한 일관성이 보장됨(보통 N=3, W=R=2) (읽기 응답과 쓰기 응답은 복제본 수 보다 크도록 설정)&lt;/li&gt;
&lt;li&gt;W + R &amp;lt;= N : 강한 일관성이 보장되지 않음 (읽기 응답과 쓰기 응답은 복제본 수 보다 작도록 설정)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구되는 일관성 수준에 따라 W, R, N의 값을 조정하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일관성 모델&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일관성 모델은 키 값 저장소를 설계할 때 고려해야 할 또 하나의 중요한 요소다.&lt;br /&gt;일관성 모델은 데이터 일관성의 수준을 결정하는데, 종류가 다양하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;강한 일관성(strong consistency) : 모든 읽기 연산은 항상 최신 데이터를 읽음.&lt;br /&gt;다시 말해서 클라이언트는 절대로 낡은 데이터를 보지 못한다.&lt;/li&gt;
&lt;li&gt;약한 일관성(weak consistency) : 읽기 연산은 최신이 아닐 수 있음.&lt;/li&gt;
&lt;li&gt;최종 일관성(eventual consistency) : 약한 일관성의 한 형태로, 시간이 지나면 모든 사본이 동기화됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강한 일관성을 달성하는 일반적인 방법은, 모든 사본에 현재 쓰기 연산의 결과가 반영될 때까지 해당 데이터에 대한 읽기/쓰기를 금지하는 것이다.&lt;br /&gt;&lt;b&gt;이 방법은 고가용성 시스템에는 적합하지 않다. 새로운 요청의 처리가 중단되기 때문이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다이나모 또는 카산드라 같은 저장소는 최종 일관성 모델을 택하고 있기 때문에 더 자세히 알아보자.&lt;br /&gt;최종 일관성 모델을 따를 경우 쓰기 연산이 병렬적으로 발생하면 시스템에 저장된 값의 일관성이 깨어질 수 있는데, 이 문제는 클라이언트가 해결해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 측에서 데이터 버전 정보를 활용해 일관성이 깨진 데이터를 읽지 않도록 하는 기법에 대해서는 아래에서 살펴볼 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비 일관성 해소 기법 : 데이터 버저닝&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 다중화하면 가용성은 높아지지만 사본 간 일관성이 깨질 가능성은 높아진다.&lt;br /&gt;&lt;b&gt;버저닝(versioning)과 벡터 시계(vector clock)는 그 문제를 해소하기 위해 등장한 기술이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버저닝은 데이터를 변경할 때마다 해당 데이터의 새로운 버전을 만드는 것을 의미한다.&lt;br /&gt;따라서 각 버전의 데이터는 변경 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버저닝에 대해 알아보기 전에 우선 데이터 일관성이 어떻게 깨지는지 예제를 통해 알아보자.&lt;br /&gt;아래의 그림과 같이 어떤 데이터의 사본이 노드 n1과 n2에 보관되어 있다고 해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701160851.png&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;716&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIqxzg/btsOY74H6kG/bLkHFEoLVhk11hGLS0bkP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIqxzg/btsOY74H6kG/bLkHFEoLVhk11hGLS0bkP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIqxzg/btsOY74H6kG/bLkHFEoLVhk11hGLS0bkP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIqxzg%2FbtsOY74H6kG%2FbLkHFEoLVhk11hGLS0bkP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;389&quot; height=&quot;217&quot; data-filename=&quot;Pasted image 20250701160851.png&quot; data-origin-width=&quot;1284&quot; data-origin-height=&quot;716&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 데이터를 가져오려는 서버 1과 서버 2는 get(&amp;ldquo;name&amp;rdquo;) 연산의 결과로 같은 값을 얻는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701160922.png&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4FHgA/btsO0WUDBbs/LNHjLrbMiNVlwppCn11k00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4FHgA/btsO0WUDBbs/LNHjLrbMiNVlwppCn11k00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4FHgA/btsO0WUDBbs/LNHjLrbMiNVlwppCn11k00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4FHgA%2FbtsO0WUDBbs%2FLNHjLrbMiNVlwppCn11k00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;415&quot; height=&quot;189&quot; data-filename=&quot;Pasted image 20250701160922.png&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;684&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이제 위 그림과 같이 &lt;code&gt;서버 1&lt;/code&gt;은 &amp;ldquo;name&amp;rdquo;에 매달린 값을 &amp;ldquo;johnSanFrancisco&amp;rdquo;로 바꾸고, &lt;code&gt;서버 2&lt;/code&gt;는 &amp;ldquo;johnNewYork&amp;rdquo;으로 바꾼다고 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 두 연산은 동시에 이뤄진다고 하자. 이제 우리는 충돌하는 두 값을 갖게 되었다.&lt;br /&gt;그 각각을 버전 v1, v2라고 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 변경이 이루어진 이후에, 원래 값은 무시할 수 있다. 변경이 끝난 옛날 값이기 때문이다.&lt;br /&gt;하지만 마지막 두 버전 v1과 v2 사이의 충돌은 해소하기 어려워 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;벡터 시계는 이 문제를 푸는데 보편적으로 사용되는 기술이다.&lt;br /&gt;동작 원리를 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;벡터 시계(vector clock)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 데이터에 &lt;code&gt;[서버, 버전]&lt;/code&gt; 쌍을 매단다&lt;/li&gt;
&lt;li&gt;쓰기 시 해당 서버의 버전 카운터 증가&lt;/li&gt;
&lt;li&gt;이를 통해 어떤 버전이 선행/후행인지 판단 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 시계는 D([S1, v1], [S2, v2], &amp;hellip;, [Sn, vn]와 같이 표현한다고 가정하자.&lt;br /&gt;여기서 D는 데이터이고, vi는 버전 카운터, Si는 서버 번호이다.&lt;br /&gt;만일 데이터 D를 서버 Si에 기록하면, 시스템은 아래 작업 가운데 하나를 수행하여야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;`[Si, vi]`가 있으면 vi를 증가시킨다.&lt;/li&gt;
&lt;li&gt;그렇지 않으면 새 항목 `[Si, 1]`를 만든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 추상적 로직이 실제로 어떻게 수행되는지를 아래의 그림으로 살펴보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701161020.png&quot; data-origin-width=&quot;984&quot; data-origin-height=&quot;1022&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1jV4U/btsOZYFKZmo/dnlJb9B19kYYAKOi2lBBC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1jV4U/btsOZYFKZmo/dnlJb9B19kYYAKOi2lBBC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1jV4U/btsOZYFKZmo/dnlJb9B19kYYAKOi2lBBC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1jV4U%2FbtsOZYFKZmo%2FdnlJb9B19kYYAKOi2lBBC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;425&quot; height=&quot;441&quot; data-filename=&quot;Pasted image 20250701161020.png&quot; data-origin-width=&quot;984&quot; data-origin-height=&quot;1022&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트가 데이터 D1을 시스템에 기록한다. 이 쓰기 연산을 처리한 서버는 Sx이다. 따라서 벡터 시계는 `D1([Sx, 1])`으로 변한다.&lt;/li&gt;
&lt;li&gt;다른 클라이언트가 데이터 D1을 읽고 D2로 업데이트한 다음 기록한다. D2는 D1에 대한 변경이므로 D1을 덮어쓴다. 이때 쓰기 연산은 같은 서버 Sx가 처리한다고 가정하자. 벡터 시계는 `D2([Sx, 2])`로 바뀔 것이다.&lt;/li&gt;
&lt;li&gt;다른 클라이언트가 D2를 읽어 D3로 갱신한 다음 기록한다. 이 쓰기 연산은 Sy가 처리한다고 가정하자. 벡터 시계 상태는 `D3([Sx, 2], [Sy 1])`로 바뀐다.&lt;/li&gt;
&lt;li&gt;또 다른 클라이언트가 D2를 읽고 D4로 갱신한 다음 기록한다 (3번과 병렬처리) 이때 쓰기 연산은 서버 Sz가 처리한다고 가정하자. 벡터 시계는 `D4([Sx, 2], [Sz, 1])`일 것이다.&lt;/li&gt;
&lt;li&gt;어떤 클라이언트가 D3과 D4를 읽으면 데이터 간 충돌이 있다는 것을 알게 된다. D2를 Sy와 Sz가 각기 다른 값으로 바꾸었기 때문이다. 이 충돌은 클라이언트가 해소한 후에 서버에 기록한다. 이 쓰기 연산을 처리한 서버는 Sx였다고 하자. 벡터 시계는 `D5([Sx, 3], [Sy, 1], [Sz, 1])`로 바뀐다. 충돌이 일어났다는 것을 어떻게 감지하는지는 잠시 후에 더 자세히 살펴볼 것이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 시계를 사용하면 어떤 버전 X가 버전 Y의 이전 버전인지(따라서 충돌이 없는지) 쉽게 판단할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;버전 Y에 포함된 모든 구성요소의 값이 X에 포함된 모든 구성요소 값보다 같거나 큰지만 보면 된다.&lt;/b&gt;&lt;br /&gt;예를 들어 벡터 시계 `D([s0, 1], [s1, 1])`은 `D([s0, 1], [s1, 2])`의 이전 버전이다. 따라서 두 데이터 사이에 충돌은 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 버전 X와 Y 사이에 충돌이 있는지 보려면 (다시 말해 그 두 버전이 같은 이전 버전에서 파생된 다른 버전들인지 보려면) Y의 벡터 시계 구성요소 가운데 X의 벡터 시계 동일 서버 구성요소보다 작은 값을 갖는 것이 있는지 보면 된다.&lt;br /&gt;예를 들어, `D([s0, 1], [s1, 2])와 D([s0, 2], [s1, 1])`는 서로 충돌한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 벡터 시계를 사용해 충돌을 감지하고 해소하는 방법에는 두 가지 분명한 &lt;b&gt;단점&lt;/b&gt;이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째로는 충돌 감지 및 해소 로직이 클라이언트에 들어가야 하므로, &lt;b&gt;클라이언트 구현이 복잡&lt;/b&gt;해진다.&lt;br /&gt;두 번째는 &lt;b&gt;[서버: 버전]의 순서쌍 개수가 굉장히 빨리 늘어난다는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하려면 그 길이에 어떤 임계치(threshold)를 설정하고, 임계치 이상으로 길이가 길어지면 오래된 순서쌍을 벡터 시계에서 제거하도록 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 이렇게 하면 버전 간 선후 관계가 정확하게 결정될 수 없기 때문에 충돌 해소 과정의 효율성이 낮아지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만 다이나모 데이터베이스에 &lt;a href=&quot;https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf&quot;&gt;관계된 문헌에&lt;/a&gt; 따르면 아마존은 실제 서비스에서 그런 문제가 벌어지는 것을 발견한 적이 없다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장애 감지 기법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 환경에서는 단일 노드의 판단만으로 장애 여부를 결정하지 않는다.&lt;br /&gt;보통 두 대 이상의 서버가 똑같이 서버 A의 장애를 보고해야 해당 서버에 실제로 장애가 발생했다고 간주하게 된다.&lt;br /&gt;다음의 그림과 같이 모든 노드 사이에 &lt;b&gt;멀티캐스팅 채널&lt;/b&gt;을 구축하는 것이 서버 장애를 감지하는 가장 손쉬운 방법이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701161140.png&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;854&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rheRN/btsO0WmNk0W/YaWAxSuIjKYRtvAItkDy4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rheRN/btsO0WmNk0W/YaWAxSuIjKYRtvAItkDy4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rheRN/btsO0WmNk0W/YaWAxSuIjKYRtvAItkDy4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrheRN%2FbtsO0WmNk0W%2FYaWAxSuIjKYRtvAItkDy4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;210&quot; height=&quot;195&quot; data-filename=&quot;Pasted image 20250701161140.png&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;854&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만 이 방법은 서버가 많을 때는 분명 비효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;가십 프로토콜(gossip protocol)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가십 프로토콜 같은 분산형 장애 감지 솔루션(decentralized failure detection) 솔루션을 채택하는 편이 보다 효율 적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가십 프로토콜의 &lt;code&gt;동작 원리&lt;/code&gt;는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 노드는 멤버십 목록을 유지한다. 멤버십 목록은 각 멤버 ID와 그 박동 카운터 쌍의 목록이다.&lt;/li&gt;
&lt;li&gt;각 노드는 주기적으로 자신의 박동 카운터를 증가시킨다.&lt;/li&gt;
&lt;li&gt;각 노드는 무작위로 선정된 노드들에게 주기적으로 자기 박동 카운터 목록을 보낸다.&lt;/li&gt;
&lt;li&gt;박동 카운터 목록을 받은 노드는 멤버십 목록을 최신 값으로 갱신한다.&lt;/li&gt;
&lt;li&gt;어떤 멤버의 박동 카운터 값이 지정된 시간 동안 갱신되지 않으면 해당 멤버는 장애 상태인 것으로 간주한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701161224.png&quot; data-origin-width=&quot;1578&quot; data-origin-height=&quot;862&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xY0Sy/btsO0DgLNi0/SGhIKaDJvUuTxMjJxiuYwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xY0Sy/btsO0DgLNi0/SGhIKaDJvUuTxMjJxiuYwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xY0Sy/btsO0DgLNi0/SGhIKaDJvUuTxMjJxiuYwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxY0Sy%2FbtsO0DgLNi0%2FSGhIKaDJvUuTxMjJxiuYwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;668&quot; height=&quot;365&quot; data-filename=&quot;Pasted image 20250701161224.png&quot; data-origin-width=&quot;1578&quot; data-origin-height=&quot;862&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;노드 서버 0은 그림 좌측의 테이블과 같은 멤버십 목록을 가진 상태다.&lt;/li&gt;
&lt;li&gt;노드 서버 0은 노드 서버2(멤버 ID=2)의 박동 카운터가 오랫동안 증가되지 않았다는 걸 발견한다.&lt;/li&gt;
&lt;li&gt;노드 서버0은 노드 서버 2를 포함하는 박동 카운터 목록을 무작위로 선택된 다른 노드에게 전달한다.&lt;/li&gt;
&lt;li&gt;노드 서버 2의 박동 카운터가 오랫동안 증가되지 않았음을 발견한 모든 노드는 해당 노드를 장애 노드로 표시한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장애 처리&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 일시적 장애 처리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가십 프로토콜을 통해 장애를 감지한 시스템은 &lt;b&gt;가용성을 유지&lt;/b&gt;하기 위해 다양한 조치를 취해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;엄격한 정족수(strict quorum)&lt;/b&gt; 방식에서는 일관성을 보장하기 위해 &lt;b&gt;읽기/쓰기 연산을 중단&lt;/b&gt;해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;느슨한 정족수(sloppy quorum)&lt;/b&gt; 방식은 가용성을 우선시하여, 장애가 발생한 서버를 제외하고 &lt;b&gt;W개의 건강한 서버와 R개의 건강한 서버&lt;/b&gt;를 해시 링에서 선택하여 연산을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  정족수(定足數)란?&lt;/b&gt;&lt;br /&gt;: 회의나 의결을 진행하기 위해 필요한 최소한의 참석 인원수를 의미한다.&lt;br /&gt;여기서는 분산 시스템에서 어떤 연산(읽기 또는 쓰기)을 &quot;성공&quot;으로 간주하기 위해 필요한 최소한의 응답 수를 의미한다고 이해했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 장애 상태의 서버 대신 요청을 처리한 서버는 &lt;b&gt;hinted handoff(단서 후 임시 위탁)&lt;/b&gt; 기법을 통해 데이터를 보관하고, 장애 서버가 복구되면 변경사항을 넘겨준다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시: 서버 2가 장애인 동안, 서버 3이 대신 읽기/쓰기 요청을 처리하고 복구 시 데이터를 전달&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701161323.png&quot; data-origin-width=&quot;882&quot; data-origin-height=&quot;826&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GevMB/btsOYZ6H84j/7ooCmHceTUqrJUu3KCzIPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GevMB/btsOYZ6H84j/7ooCmHceTUqrJUu3KCzIPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GevMB/btsOYZ6H84j/7ooCmHceTUqrJUu3KCzIPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGevMB%2FbtsOYZ6H84j%2F7ooCmHceTUqrJUu3KCzIPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;410&quot; height=&quot;384&quot; data-filename=&quot;Pasted image 20250701161323.png&quot; data-origin-width=&quot;882&quot; data-origin-height=&quot;826&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위 그림을 보면, 장애 상태인 노드 서버 2에 대한 읽기 및 쓰기 연산은 일시적으로 노드 서버 3이 처리한다.&lt;br /&gt;서버 2가 복구되면, 서버 3은 갱신된 데이터를 서버 2로 인계할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 영구 장애 처리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일시적 장애와 달리 &lt;b&gt;영구적인 서버 장애&lt;/b&gt;를 처리하기 위해서는 &lt;b&gt;반-엔트로피(anti-entropy) 프로토콜&lt;/b&gt;을 사용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반-엔트로피는 복제된 사본들 간의 데이터를 비교하여 최신 상태로 동기화한다.&lt;/li&gt;
&lt;li&gt;이 과정에서 &lt;b&gt;머클 트리(Merkle Tree)를&lt;/b&gt; 활용하여 일관성 체크 및 데이터 전송량을 최소화한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 데이터 센터 장애 처리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정전, 네트워크 장애, 자연재해 등으로 인한 &lt;b&gt;데이터 센터 단위 장애&lt;/b&gt;를 대비하기 위해,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터는 &lt;b&gt;여러 데이터 센터에 다중화&lt;/b&gt;되어야 한다.&lt;/li&gt;
&lt;li&gt;하나의 센터가 완전히 중단되더라도 &lt;b&gt;다른 센터에서 데이터 접근이 가능&lt;/b&gt;하도록 구성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시스템 아키텍처&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 아키텍처 다이어그램을 그려보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701161617.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7fPoa/btsOZB5kKlf/Esc5uxRi6PhOEn0UFqt0Q0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7fPoa/btsOZB5kKlf/Esc5uxRi6PhOEn0UFqt0Q0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7fPoa/btsOZB5kKlf/Esc5uxRi6PhOEn0UFqt0Q0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7fPoa%2FbtsOZB5kKlf%2FEsc5uxRi6PhOEn0UFqt0Q0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;487&quot; height=&quot;266&quot; data-filename=&quot;Pasted image 20250701161617.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트는 API(get, put)를 통해 시스템에 접근&lt;/li&gt;
&lt;li&gt;중재자(Coordinator)는 클라이언트와 노드 사이에서 &lt;b&gt;프록시 역할&lt;/b&gt; 수행&lt;/li&gt;
&lt;li&gt;노드들은 &lt;b&gt;안정 해시 링(Consistent Hashing Ring)&lt;/b&gt; 위에 배치되어 있음&lt;/li&gt;
&lt;li&gt;데이터는 여러 노드에 다중화되어 저장&lt;/li&gt;
&lt;li&gt;시스템은 &lt;b&gt;자동 확장/축소&lt;/b&gt;가 가능하도록 설계되어 있으며,&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단일 장애점(SPOF)&lt;/b&gt; 없는 완전한 분산 아키텍처로 구성됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;쓰기 경로에서 발생하는 일&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701161717.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7dwU4/btsOZ89nCB2/OkuSd7VXfv2tJsfzTE8y21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7dwU4/btsOZ89nCB2/OkuSd7VXfv2tJsfzTE8y21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7dwU4/btsOZ89nCB2/OkuSd7VXfv2tJsfzTE8y21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7dwU4%2FbtsOZ89nCB2%2FOkuSd7VXfv2tJsfzTE8y21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;479&quot; height=&quot;256&quot; data-filename=&quot;Pasted image 20250701161717.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;684&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;쓰기 요청이 &lt;b&gt;커밋 로그(commit log)에&lt;/b&gt; 먼저 기록됨&lt;/li&gt;
&lt;li&gt;이후 &lt;b&gt;메모리 캐시(MemTable)에&lt;/b&gt; 저장됨&lt;/li&gt;
&lt;li&gt;캐시가 가득 차거나 설정한 임계치에 도달하면, 디스크의 &lt;b&gt;SSTable로&lt;/b&gt; 영속 저장됨&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  SStable이란?&lt;/b&gt;&lt;br /&gt;: Sorted-String Table의 약어로 키-값의 순서쌍을 정렬된 리스트 형태로 관리하는 테이블&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;읽기 경로에서 발생하는 일&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20250701161752.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EOKEg/btsOY7wQenz/WcxVxmUdLixnkX8kaGF7y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EOKEg/btsOY7wQenz/WcxVxmUdLixnkX8kaGF7y0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EOKEg/btsOY7wQenz/WcxVxmUdLixnkX8kaGF7y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEOKEg%2FbtsOY7wQenz%2FWcxVxmUdLixnkX8kaGF7y0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;559&quot; height=&quot;249&quot; data-filename=&quot;Pasted image 20250701161752.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;570&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;먼저 &lt;b&gt;메모리 캐시&lt;/b&gt;에서 데이터를 조회&lt;/li&gt;
&lt;li&gt;없을 경우, &lt;b&gt;블룸 필터(Bloom Filter)를&lt;/b&gt; 통해 존재 여부를 확인&lt;/li&gt;
&lt;li&gt;블룸 필터를 통해 어떤 SSTable에 키가 보관되어 있는지 판단&lt;/li&gt;
&lt;li&gt;SSTable에서 실제 데이터를 조회&lt;/li&gt;
&lt;li&gt;클라이언트에 응답 반환&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  Bloom Filter란?&lt;/b&gt;&lt;br /&gt;: 키가 존재할 가능성을 빠르게 판단하는 확률적 자료구조로, 존재하지 않는 경우에는 100% 정확하지만, 존재할 경우 오탐(False Positive)이 가능함&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 키-값 저장소의 각 컴포넌트는 서로 긴밀하게 연결되어 작동하며, 전체 시스템의 성능, 확장성, 신뢰성을 보장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안정 해시, 데이터 다중화, 일관성 모델, 버저닝, 장애 처리 등의 기술을 적절히 조합함으로써, 대규모 데이터를 효율적으로 관리하고 높은 가용성을 제공하는 강력한 분산 시스템을 구축할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 키-값 저장소 설계에 사용되는 주요 기술과 그 목적을 정리하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;대규모 데이터 저장&lt;/th&gt;
&lt;th&gt;안정 해시를 사용하여 서버에 부하 분산&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;읽기 연산에 대한 가용성 보장&lt;/td&gt;
&lt;td&gt;데이터를 여러 데이터센터에 다중화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;쓰기 연산에 대한 가용성 보장&lt;/td&gt;
&lt;td&gt;버저닝 및 벡터 시계를 사용하여 충돌 해소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 파티션&lt;/td&gt;
&lt;td&gt;안정 해시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;점진적 규모 확장성&lt;/td&gt;
&lt;td&gt;안정 해시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;다양성(heterogeneity)&lt;/td&gt;
&lt;td&gt;안정 해시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;조절 가능한 데이터 일관성&lt;/td&gt;
&lt;td&gt;정족수 합의(quorum consensus)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일시적 장애 처리&lt;/td&gt;
&lt;td&gt;느슨한 정족수 프로토콜 및 단서 후 임시 위탁&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;영구적 장애 처리&lt;/td&gt;
&lt;td&gt;머클 트리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 센터 장애 대응&lt;/td&gt;
&lt;td&gt;여러 데이터센터에 다중화&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처&lt;br /&gt;&lt;a href=&quot;https://blog.naver.com/wavescats/222751548888?viewType=pc&quot;&gt;https://blog.naver.com/wavescats/222751548888?viewType=pc&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://donghyeon.dev/%EC%9D%B8%ED%94%84%EB%9D%BC/2022/03/26/%ED%82%A4-%EA%B0%92-%EC%A0%80%EC%9E%A5%EC%86%8C-%EC%84%A4%EA%B3%84/&quot;&gt;https://donghyeon.dev/%EC%9D%B8%ED%94%84%EB%9D%BC/2022/03/26/%ED%82%A4-%EA%B0%92-%EC%A0%80%EC%9E%A5%EC%86%8C-%EC%84%A4%EA%B3%84/&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://dewble.tistory.com/entry/system-design-distributed-key-value-store&quot;&gt;https://dewble.tistory.com/entry/system-design-distributed-key-value-store&lt;/a&gt;&lt;/p&gt;</description>
      <category>   Dev/System Design</category>
      <author>현주먹</author>
      <guid isPermaLink="true">https://javacatcher.tistory.com/226</guid>
      <comments>https://javacatcher.tistory.com/226#entry226comment</comments>
      <pubDate>Tue, 1 Jul 2025 19:23:07 +0900</pubDate>
    </item>
    <item>
      <title>5장. 안정 해시 설계</title>
      <link>https://javacatcher.tistory.com/225</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;수평적 규모 확장성(scale out)을 달성하기 위해서는 클라이언트로부터의 요청이나 데이터를 서버에 균등하게 나누는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;code&gt;안정 해시&lt;/code&gt;는 이 목표를 달성하기 위해 보편적으로 사용하는 기술이다.&lt;br /&gt;안정 해시에 대해서 알기 전에 우선 이러한 해시 기술이 어떠한 문제를 해결하려고 하는지 좀 더 자세히 살펴보자&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;먼저, 해시란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안정 해시에 대해서 알아보기 전에 간략하게 해시에 대해서 짚어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시의 사전적 의미는 '임의 길이의 데이터 문자열을 입력으로 받아서 고정 크기의 출력, 일반적으로는 숫자와 문자열로 이루어진 해시 값 또는 해시 코드를 생성하는 수학적 함수'이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 풀어 이야기하면 같은 문자열 입력은 항상 같은 해시 코드를 반환한다는 것이다. 해시의 이런 특성을 이용하여 암호화나 파일의 위변조 판정 등 다양한 용도로 사용된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해시 키 재배치(rehash) 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N개의 캐시 서버가 있다고 하자. 이 서버들에 부하를 균등하게 나누는 보편적 방법은 아래의 해시 함수를 사용하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;severIndex = hash(key) % N&lt;/code&gt; (N은 서버의 개수)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 4대의 서버를 사용하는 예제를 통해 동작 원리를 알아보자.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;키&lt;/th&gt;
&lt;th&gt;해시&lt;/th&gt;
&lt;th&gt;해시%4(서버 인덱스)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;key0&lt;/td&gt;
&lt;td&gt;18358617&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key1&lt;/td&gt;
&lt;td&gt;26143584&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key2&lt;/td&gt;
&lt;td&gt;18131146&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key3&lt;/td&gt;
&lt;td&gt;35863496&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key4&lt;/td&gt;
&lt;td&gt;34085809&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key5&lt;/td&gt;
&lt;td&gt;27581703&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key6&lt;/td&gt;
&lt;td&gt;38164978&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key7&lt;/td&gt;
&lt;td&gt;22530351&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정한 키가 보관된 서버를 알아내기 위해, 나머지(modular) 연산을 &lt;code&gt;f(key)%4&lt;/code&gt;와 같이 적용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 hash(key0) % 4 = 1이면, 클라이언트는 캐시에 보관된 데이터를 가져오기 위해 서버 1에 접속하여야 한다.&lt;br /&gt;다음 그림은 본 예제에서 키 값이 서버에 어떻게 분산되는지 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xm9fH/btsOYHKz3vh/6hfb8PHsgyFkkkAW2i7wmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xm9fH/btsOYHKz3vh/6hfb8PHsgyFkkkAW2i7wmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xm9fH/btsOYHKz3vh/6hfb8PHsgyFkkkAW2i7wmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxm9fH%2FbtsOYHKz3vh%2F6hfb8PHsgyFkkkAW2i7wmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;402&quot; height=&quot;206&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 방법은 서버 풀(server pool)의 크기가 고정되어 있을 때, 그리고 데이터 분포가 균등할 때는 잘 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 서버가 추가되거나 기존 서버가 삭제되면 문제가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 1번 서버가 장애를 일으켜 동작을 중단했다고 하자.&lt;br /&gt;그러면 서버 풀의 크기는 3으로 변한다.&lt;br /&gt;그 결과로, 키에 대한 해시 값은 변하지 않지만 modular 연산을 적용하여 계산한 서버 인덱스 값은 달라질 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 수가 1만큼 줄어들어서이다.&lt;br /&gt;따라서 다음과 같은 결과를 얻는다. &lt;code&gt;해시 % 3&lt;/code&gt;의 결과 값이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;키&lt;/th&gt;
&lt;th&gt;해시&lt;/th&gt;
&lt;th&gt;해시%4(서버 인덱스)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;key0&lt;/td&gt;
&lt;td&gt;18358617&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key1&lt;/td&gt;
&lt;td&gt;26143584&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key2&lt;/td&gt;
&lt;td&gt;18131146&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key3&lt;/td&gt;
&lt;td&gt;35863496&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key4&lt;/td&gt;
&lt;td&gt;34085809&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key5&lt;/td&gt;
&lt;td&gt;27581703&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key6&lt;/td&gt;
&lt;td&gt;38164978&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key7&lt;/td&gt;
&lt;td&gt;22530351&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이어서 변화된 키 분포(distribution)이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lfGKP/btsOY0pmnZ2/tA8ikDm9ZnOl6H5wGRSzhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lfGKP/btsOY0pmnZ2/tA8ikDm9ZnOl6H5wGRSzhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lfGKP/btsOY0pmnZ2/tA8ikDm9ZnOl6H5wGRSzhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlfGKP%2FbtsOY0pmnZ2%2FtA8ikDm9ZnOl6H5wGRSzhk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;256&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림에 보인 대로, 장애가 발생한 1번 서버에 보관되어 있는 키뿐만 아닌 &lt;b&gt;대부분의 키가 재분배되었다.&lt;/b&gt;&lt;br /&gt;1번 서버가 죽으면 대부분 캐시 클라이언트가 데이터가 없는 엉뚱한 서버에 접속하게 된다는 뜻이다.&lt;br /&gt;그 결과로 대규모 캐시 미스(cache miss)가 발생하게 될 것이다.&lt;br /&gt;안정 해시는 이 문제를 효과적으로 해결하는 기술이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;안정 해시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안정 해시(Consistent Hashing)란 분산되어 있는 서버 혹은 서비스에 데이터를 균등하게 나누기 위한 기술이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안정 해시는 해시 테이블 크기가 조정될 때 평균적으로 오직 &lt;code&gt;k/n&lt;/code&gt;개의 키만 재배치하는 해시 기술이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k는 키의 개수이고, n은 슬롯의 개수이다.&lt;br /&gt;이와는 달리 대부분의 전통적 해시 테이블은 슬롯의 수가 바뀌면 거의 대부분 키를 재배치한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해시 공간과 해시 링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안정 해시의 동작 원리를 살펴보자. 해시 함수 f로는 &lt;a href=&quot;https://ko.wikipedia.org/wiki/SHA-1&quot;&gt;SHA-1&lt;/a&gt;을 사용한다고 하고, 그 함수의 출렬 값 범위는 x0, x1,... xn과 같다고 하자. SHA-1의 해시 공간 범위는 0부터 (2^160) - 1까지라고 알려져 있다. 따라서 x0는 0, xn은 (2^160) - 1이며, 나머지 x1부터 xn-1까지는 그 사이의 값을 갖게 될 것이다. 다음 그림은 해시 공간을 그림으로 표현한 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cj8Vpb/btsOX9f6Iq5/kM6vyzogprZLKKLWS96Kwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cj8Vpb/btsOX9f6Iq5/kM6vyzogprZLKKLWS96Kwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cj8Vpb/btsOX9f6Iq5/kM6vyzogprZLKKLWS96Kwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcj8Vpb%2FbtsOX9f6Iq5%2FkM6vyzogprZLKKLWS96Kwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;408&quot; height=&quot;66&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;132&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 해시 공간의 양쪽을 구부려 잡으면 다음과 같은 해시 링이 만들어진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7frfE/btsOYe9KaKk/k87mHM4zCzlYPun6kQihyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7frfE/btsOYe9KaKk/k87mHM4zCzlYPun6kQihyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7frfE/btsOYe9KaKk/k87mHM4zCzlYPun6kQihyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7frfE%2FbtsOYe9KaKk%2Fk87mHM4zCzlYPun6kQihyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;187&quot; height=&quot;216&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해시 서버&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 해시 함수 f를 사용하면 서버 IP나 이름을 이 링 위의 어떤 위치에 대응시킬 수 있다.&lt;br /&gt;다음 그림은 4개의 서버를 이 해시 링 위에 배치한 결과이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u9abH/btsOXIDq0tr/t9OasdZJ80KcWceXsVB6Lk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u9abH/btsOXIDq0tr/t9OasdZJ80KcWceXsVB6Lk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u9abH/btsOXIDq0tr/t9OasdZJ80KcWceXsVB6Lk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu9abH%2FbtsOXIDq0tr%2Ft9OasdZJ80KcWceXsVB6Lk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;340&quot; height=&quot;232&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;560&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해시 키&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 사용된 해시 함수는 &quot;해시 키 재배치 문제&quot;에 언급된 함수와는 다르며, &lt;code&gt;modular 연산 %&lt;/code&gt;는 사용하지 않고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 그림과 같이 캐시할 키 key0, key1, key2, key3 또한 해시 링 위의 어느 지점에 배치할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brHeUC/btsOY0JFzDE/iwMkKHth3JAcZjAI3ZdCX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brHeUC/btsOY0JFzDE/iwMkKHth3JAcZjAI3ZdCX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brHeUC/btsOY0JFzDE/iwMkKHth3JAcZjAI3ZdCX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrHeUC%2FbtsOY0JFzDE%2FiwMkKHth3JAcZjAI3ZdCX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;383&quot; height=&quot;234&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서버 조회&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 키가 저장되는 서버는, 해당 키의 위치로부터 시계 방향으로 링을 탐색해 나가면서 만나는 첫 번째 서버다. 다음 그림을 살펴보자&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf1CjO/btsOYeWb4b2/MofRT4XKxGjkJh2vIAM7a0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf1CjO/btsOYeWb4b2/MofRT4XKxGjkJh2vIAM7a0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf1CjO/btsOYeWb4b2/MofRT4XKxGjkJh2vIAM7a0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbf1CjO%2FbtsOYeWb4b2%2FMofRT4XKxGjkJh2vIAM7a0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;365&quot; height=&quot;250&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;558&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;key0은 서버 0에 저장되고, key1은 서버 1에 저장되며, key2는 서버 2, key3는 서버 3에 저장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서버 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;윗 내용에 이어서 서버를 추가하더라도 키 가운데 일부만 재배치하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3wqxI/btsOWGeCtQs/iMscgqExQrh88aYdjGlcHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3wqxI/btsOWGeCtQs/iMscgqExQrh88aYdjGlcHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3wqxI/btsOWGeCtQs/iMscgqExQrh88aYdjGlcHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3wqxI%2FbtsOWGeCtQs%2FiMscgqExQrh88aYdjGlcHk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;393&quot; height=&quot;264&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;542&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위의 그림을 살펴보자.&lt;br /&gt;새로운 서버 4가 추가된 뒤에 key0 만 재배치됨을 알 수 있다. k1, k2, k3은 같은 서버에 남는다.&lt;br /&gt;이유는 다음과 같다.&lt;br /&gt;서버 4가 추가되기 전, key0은 서버 0에 저장되어 있었다.&lt;br /&gt;하지만 서버 4가 추가된 뒤에 key0의 위치에서 시계 방향으로 순회했을 때 처음으로 만나게 되는 서버가 서버 4이기 때문에 key0 만 재배치된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서버 제거&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 서버가 제거되면 키 가운데 일부만 재배치된다.&lt;br /&gt;다음 그림을 보면 서버 1이 삭제되었을 때 key1만이 서버 2로 재배치됨을 알 수 있다.&lt;br /&gt;나머지 키는 영향이 없다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WTP66/btsOYwWvjzV/SPaeYKZsFXf0BLL9JeDay0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WTP66/btsOYwWvjzV/SPaeYKZsFXf0BLL9JeDay0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WTP66/btsOYwWvjzV/SPaeYKZsFXf0BLL9JeDay0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWTP66%2FbtsOYwWvjzV%2FSPaeYKZsFXf0BLL9JeDay0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;377&quot; height=&quot;263&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;568&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 구현법의 두 가지 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안정 해시 알고리즘은 MIT에서 처음 제안되었다고 한다. 그 기본 절차는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버와 키를 균등 분포(uniform distribution) 해시 함수를 사용해 해시 링에 배치한다.&lt;/li&gt;
&lt;li&gt;키의 위치에서 링을 시계 방향으로 탐색하다 만나는 최초의 서버가 키가 저장될 서버다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 접근법에는 두 가지 문제가 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서버가 추가되거나 삭제되는 상황을 감안하면 &lt;code&gt;파티션의 크기를 균등하게 유지하는 게 불가능하다.&lt;/code&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 파티션은 인접한 서버 사이의 해시 공간이다. 어떤 서버는 굉장히 작은 해시 공간을 할당받고, 어떤 서버는 굉장히 큰 해시 공간을 할당받는 상황이 가능하다는 것이다.&lt;/li&gt;
&lt;li&gt;다음 그림은 서버 1이 삭제되는 바람에 서버 2의 파티션이 다른 파티션 대비 거의 두 배로 커지는 상황을 보여준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSMPyZ/btsOWIDBLqN/xYeZQM1RItmKrlGH4sxVV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSMPyZ/btsOWIDBLqN/xYeZQM1RItmKrlGH4sxVV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSMPyZ/btsOWIDBLqN/xYeZQM1RItmKrlGH4sxVV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSMPyZ%2FbtsOWIDBLqN%2FxYeZQM1RItmKrlGH4sxVV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;376&quot; height=&quot;243&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;520&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;키의 균등 분포(uniform distribution)를 달성하기가 어렵다는 것이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7LexT/btsOYGZeYDz/OTKtElBw8DW1gy4NKWErn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7LexT/btsOYGZeYDz/OTKtElBw8DW1gy4NKWErn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7LexT/btsOYGZeYDz/OTKtElBw8DW1gy4NKWErn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7LexT%2FbtsOYGZeYDz%2FOTKtElBw8DW1gy4NKWErn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;387&quot; height=&quot;248&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;506&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 그림에서, 서버 1과 서버 3은 아무 데이터도 갖지 않는 반면, 대부분의 키는 서버 2에 보관될 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해 제안된 기법이 가상 노드(virtual node) 또는 복제(replica)라 불리는 기법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가상 노드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 노드는 실제 노드 또는 서버를 가리키는 노드로서, 하나의 서버는 링 위에 여러 개의 가상 노드를 가질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 공간은 유한하다. 그러므로 해시 공간에 배치된 노드 수가 굉장히 많다면 표준편차가 감소하여 노드 하나가 없어진다 하더라도 다음 노드에 큰 부하는 가지 않을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 현실 세계에서 &lt;b&gt;물리 노드의 수는 곧 비용&lt;/b&gt;이라는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 물리 노드(Physical node)를 모방하는 가상 노드(Virtual node)를 구현하여 이를 영리하게 해결한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;586&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clktDZ/btsOYa7cXcs/A87hYY0FLqT3tHp9Y01CN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clktDZ/btsOYa7cXcs/A87hYY0FLqT3tHp9Y01CN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clktDZ/btsOYa7cXcs/A87hYY0FLqT3tHp9Y01CN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclktDZ%2FbtsOYa7cXcs%2FA87hYY0FLqT3tHp9Y01CN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;363&quot; height=&quot;261&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;586&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림을 살펴보면, 서버 0과 서버 1은 3개의 가상 노드를 갖는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 숫자 3은 임의로 정한 것이며, 실제 시스템에서는 그보다 훨씬 큰 값이 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 0을 링에 배치하기 위해 s0 하나만 쓰는 대신에 s0_0, s0_1, s0_2의 세 개 가상 노드를 사용했다. 서버 1도 마찬가지다. 따라서 각 서버는 하나가 아닌 여러 개 파티션을 관리해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키의 위치로부터 시계방향으로 링을 탐색하다 만나는 최초의 가상 노드가 해당 키가 저장될 서버가 된다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cky0y/btsOZhqIYMW/6PKuOJGpvJhJeFcigbVoE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cky0y/btsOZhqIYMW/6PKuOJGpvJhJeFcigbVoE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cky0y/btsOZhqIYMW/6PKuOJGpvJhJeFcigbVoE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCky0y%2FbtsOZhqIYMW%2F6PKuOJGpvJhJeFcigbVoE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;428&quot; height=&quot;283&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k0이 저장되는 서버는 k0의 위치로부터 링을 시계방향으로 탐색하다 만나는 최초의 가상 노드 s1_1가 나타내는 서버, 즉 서버 1이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 노드의 개수를 늘리면 키의 분포는 점점 더 균등해진다. 표준 편차(standard deviation)가 작아져서 데이터가 고르게 분포되기 때문이다. 표준 편차는 데이터가 어떻게 퍼져 나갔는지를 보이는 척도다.&lt;br /&gt;가상 노드의 개수를 더 늘리면 표준 편차의 값은 떨어지지만 가상 노드 데이터를 저장할 공간은 더 많이 필요하게 될 것이다. 그러니 시스템 요구사항에 맞도록 가상 노드 개수를 적절히 조정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;재배치할 키 결정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 추가되거나 제거되면 데이터 일부는 재배치해야 한다. 어느 범위의 키들이 재배치되어야 할까?&lt;br /&gt;그림으로 예를 들어보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKn92U/btsOWqiZXFQ/x2mvJNVzEkSMVpMGK81tA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKn92U/btsOWqiZXFQ/x2mvJNVzEkSMVpMGK81tA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKn92U/btsOWqiZXFQ/x2mvJNVzEkSMVpMGK81tA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKn92U%2FbtsOWqiZXFQ%2Fx2mvJNVzEkSMVpMGK81tA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;369&quot; height=&quot;262&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;564&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;서버 4가 추가되었다고 해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 영향 받은 범위는 s4(새로 추가된 노드)부터 그 반시계 방향에 있는 첫 번째 서버 s3까지이다.&lt;br /&gt;즉 s3부터 s4 사이에 있는 키들을 s4로 재배치하여야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZpfJ8/btsOXQOE7wq/bHwuwUJsG9hlvsqFWOS1wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZpfJ8/btsOXQOE7wq/bHwuwUJsG9hlvsqFWOS1wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZpfJ8/btsOXQOE7wq/bHwuwUJsG9hlvsqFWOS1wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZpfJ8%2FbtsOXQOE7wq%2FbHwuwUJsG9hlvsqFWOS1wk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;406&quot; height=&quot;285&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안정 해시의 이점은 요약해 보자면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 추가되거나 삭제될 때 재배치되는 키의 수가 최소화된다.&lt;/li&gt;
&lt;li&gt;데이터가 보다 균등하게 분포하게 되므로 수평적 규모 확장성을 달성하기 쉽다.&lt;/li&gt;
&lt;li&gt;핫스폿(hotspot) 키 문제를 줄인다. 특정한 샤드에 대한 접근이 지나치게 빈번하면 서버 과부하 문제가 생기지만, 안정 해시는 데이터를 좀 더 균등하게 분배하므로 이런 문제가 생길 가능성을 줄인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안정 해시는 실제로 널리 쓰이는 기술이다. 그중 유명한 사례를 살펴보면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Amazon DynamoDB의 파티셔닝 관련 컴포넌트&lt;/li&gt;
&lt;li&gt;Apache Cassandra 클러스터에서의 데이터 파티셔닝&lt;/li&gt;
&lt;li&gt;디스코드 채팅 어플리케이션&lt;/li&gt;
&lt;li&gt;아카마이 CDN&lt;/li&gt;
&lt;li&gt;meglev 네트워크 부하 분산기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림 및 참고&lt;br /&gt;&lt;a href=&quot;https://velog.io/@minu/5%EC%9E%A5.-%EC%95%88%EC%A0%95-%ED%95%B4%EC%8B%9C-%EC%84%A4%EA%B3%84&quot;&gt;https://velog.io/@minu/5%EC%9E%A5.-%EC%95%88%EC%A0%95-%ED%95%B4%EC%8B%9C-%EC%84%A4%EA%B3%84&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://haril.dev/blog/2023/06/04/Consistent-Hashing#user-content-fn-fn-nth-1-3009c3&quot;&gt;안정 해시 이해하기 굉장히 좋은 블로그&lt;/a&gt;&lt;/p&gt;</description>
      <category>   Dev/System Design</category>
      <author>현주먹</author>
      <guid isPermaLink="true">https://javacatcher.tistory.com/225</guid>
      <comments>https://javacatcher.tistory.com/225#entry225comment</comments>
      <pubDate>Mon, 30 Jun 2025 17:10:03 +0900</pubDate>
    </item>
    <item>
      <title>4장. 처리율 제한 장치의 설계</title>
      <link>https://javacatcher.tistory.com/224</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001033116&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초&lt;/a&gt;를 읽고 정리한 글입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;처리율 제한 장치란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리율 제한 장치란 네트워크 시스템에서 &lt;code&gt;클라이언트 또는 서비스가 보내는 트래픽의 처리율(rate)을 제어하기 위한 장치&lt;/code&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP를 예로 들면 이 장치는 특정 기간 내에 전송되는 클라이언트의 요청 횟수를 제한한다. API 요청 횟수가 제한 장치에 정의된 임계치를 넘어서면 추가로 도달한 모든 호출은 처리가 중단된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자는 초당 2회 이상 새 글을 올릴 수 없다.&lt;/li&gt;
&lt;li&gt;같은 IP 주소로는 하루에 10개 이상의 계정을 생성할 수 없다.&lt;/li&gt;
&lt;li&gt;같은 디바이스로는 주당 5회 이상 리워드를 요청할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API에 rate limiter를 두면 좋은 점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DoS(Denial of Service) 공격에 의한 자원 고갈(resource starvation)을 방지할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대형 IT 기업들이 공개한 대부분의 API는 어떤 형태로든 처리율 제한 장치를 갖고 있다. 예를 들어 트위터는 3시간 동안 300개의 트윗만 올릴 수 있도록 제한하고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비용을 절감한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추가 요청에 대한 처리를 제한하면 서버를 많이 두지 않아도 되고, 우선순위가 높은 API에 더 높은 자원을 할당할 수 있다. 제3자 API에 사용료를 지불하고 있는 회사들에게는 아주 중요하다. 예를 들어, 신용을 확인하거나, 신용카드 결제를 하거나 등 API에 대한 과금이 횟수에 따라 이루어진다면, 그 횟수를 제한할 수 있어야 비용을 절감할 수 있을 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;서버 과부하를 막는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;봇(bot)에서 오는 트래픽이나 사용자의 잘못된 이용 패턴으로 유발된 트래픽을 걸러내는데 처리율 제한 장치를 활용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1단계: 문제 이해 및 설계 범위 확정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리율 제한 장치는 여러가지 알고리즘을 사용해서 구현할 수 있기 때문에, 어떤 알고리즘을 사용해야 할지 면접관과 소통하며 결정을 내려야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책의 예시에서는 다음과 같은 질문을 해봤다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 측 제한 장치 or 서버 측 제한 장치?
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;서버&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;API 호출 제어 기준은? IP주소? 사용자 ID?
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;다양한 형태를 정의할 수 있도록 하는 유연한 시스템 이어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;시스템의 규모
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;대규모&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;분산 환경에서 동작해야 하는지
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;그렇다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;처리율 제한 장치가 독립된 서비스 인지 애플리케이션 코드에 포함될 수 있는지
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;본인이 결정하세요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;요청이 거부당한 사용자에게 그 사실을 알려야 하는지.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;네&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예제 요구사항 요약&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정된 처리율을 초과하는 요청은 정확하게 제한한다.&lt;/li&gt;
&lt;li&gt;낮은 응답시간: 이 처리율 제한 장치는 HTTP 응답시간에 나쁜 영향을 주어서는 곤란하다.&lt;/li&gt;
&lt;li&gt;가능한 한 적은 메모리를 써야 한다.&lt;/li&gt;
&lt;li&gt;분산형 처리율 제한(distributed rate limiting): 하나의 처리율 제한 장치를 여러 서버나 프로세스에서 공유할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;예외 처리 : 요청이 제한되었을 때는 그 사실을 사용자에게 분명하게 보여주어야 한다.&lt;/li&gt;
&lt;li&gt;높은 결함 감내성(fault tolerance): 제한 장치에 장애가 생기더라도 전체 시스템에 영향을 주어서는 안 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2단계: 개략적 설계안 제시 및 동의 구하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 기본적인 클라이언트-서버 통신 모델을 사용해 보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처리율 제한 장치는 어디에 둘 것인가?&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;클라이언트 vs 서버&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트 축에 둔다면 일반적으로 클라이언트는 처리율 제한을 안정적으로 걸 수 있는 장소가 못 된다. 클라이언트 요청은 쉽게 위변조가 가능해서 모든 클라이언트의 구현을 통제하는 것도 어려울 수 있다&lt;/li&gt;
&lt;li&gt;서버 측에 둔다면?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;818&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9667a/btsOWLAi4Ew/dAn8ziOQCgGzkdoGJeBJpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9667a/btsOWLAi4Ew/dAn8ziOQCgGzkdoGJeBJpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9667a/btsOWLAi4Ew/dAn8ziOQCgGzkdoGJeBJpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9667a%2FbtsOWLAi4Ew%2FdAn8ziOQCgGzkdoGJeBJpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;405&quot; height=&quot;94&quot; data-origin-width=&quot;818&quot; data-origin-height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. API 서버에 두는 대신, 미들웨어를 만들어 API 서버로 가는 요청들을 제어할 수 있다.&lt;br /&gt;만약 처리율 제한 미들웨어에 의해 요청이 가로막히면 클라이언트는 HTTP 상태코드 &lt;code&gt;429(Too many requests)를&lt;/code&gt; 받는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRNEsQ/btsOWpqILlv/EmlJJEoPOnkqkLfoAfLe30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRNEsQ/btsOWpqILlv/EmlJJEoPOnkqkLfoAfLe30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRNEsQ/btsOWpqILlv/EmlJJEoPOnkqkLfoAfLe30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRNEsQ%2FbtsOWpqILlv%2FEmlJJEoPOnkqkLfoAfLe30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;389&quot; height=&quot;115&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;236&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 API 서버의 처리율이 초당 2개의 요청으로 제한된 상황에서, 클라이언트가 3번째 요청을 앞의 두 요청과 같은 초 범위 내에서 전송했다고 해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;position: absolute;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 두 요청은 API 서버로 전송되지만, 세 번째 요청은 처리율 제한 미들웨어에 의해 가로막히고 클라이언트로는 HTTP 상태 코드 &lt;code&gt;429(Too many requests)&lt;/code&gt;를 반환할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;API 게이트웨이&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;폭넓게 채택된 기술로는 클라우드 마이크로서비스의 경우, 처리율 제한 장치는 보통 API 게이트웨이라 불리는 컴포넌트에 구현된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;API 게이트웨이는 처리율 제한, SSL 종단(termination), 사용자 인증(authentication), IP 허용 목록(whitelist) 관리 등을 지원하는 완전 위탁관리형 서비스(fully managed), 즉 클라우드 업체가 유지 보수를 담당하는 서비스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만 일단은 API 게이트웨이가 처리율 제한을 지원하는 미들웨어라는 점만 기억하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어디에 두어야 할지에 대한 몇 가지 지침&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리율 제한 기능을 설계할 때는 처리율 제한 장치를 어디에 두어야 하나를 중요하게 따져야 한다.&lt;br /&gt;회사의 현재 기술스택이나 엔지니어링 인력, 우선순위, 목표에 따라 서버에 둘 수도 있고, 게이트웨이에 둘 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 일반적으로 적용될 수 있는 몇 가지 지침을 살펴보면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로그래밍 언어, 캐시 서비스 등 현재 사용하고 있는 기술 스택을 점검하라.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 사용하는 프로그래밍 언어가 서버 측 구현을 지원하기 충분할 정도로 효율이 높은지 확인하라.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사업 필요에 맞는 처리율 제한 알고리즘을 찾아라.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 측에서 모든 것을 구현하기로 했다면, 알고리즘은 자유롭게 선택할 수 있다. 하지만 제3 사업자가 제공하는 게이트웨이를 사용하기로 했다면 선택지는 제한될 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;설계가 마이크로서비스에 기반하고 있고, 사용자 인증이나 IP 허용목록 관리 등을 처리하기 위해 API 게이트웨이를 이미 설계에 포함시켰다면 처리율 제한 기능 또한 게이트웨이에 포함시켜야 할 수도 있다.&lt;/li&gt;
&lt;li&gt;처리율 제한 서비스를 직접 만드는 데는 시간이 든다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처리율 제한 장치를 구현하기에 충분한 인력이 없다면 상용 API 게이트웨이를 쓰는 것이 바람직한 방법일 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처리율 제한 알고리즘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리율 제한을 실현하는 알고리즘은 여러 가지인데, 각기 다른 장단점을 갖고 있다.&lt;br /&gt;널리 알려진 인기 알고리즘 위주로 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 토큰 버킷 알고리즘&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 버킷 알고리즘은 처리율 제한 폭넓게 이용되고 있다.&lt;br /&gt;간단하고, 알고리즘에 대한 세간의 이해도도 높은 편이며 인터넷 기업들이 보편적으로 사용하고 있다. 아마존, 스트라이프가 API 요청을 통제(throttle) 하기 위해 이 알고리즘을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 버킷 알고리즘의 동작원리는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토큰 버킷은 지정된 용량을 갖는 컨테이너다. 이 버킷에는 사전 설정된 양의 토큰이 주기적으로 채워진다. 토큰이 꽉 찬 버킷에는 더 이상의 토큰은 추가되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvgrRX/btsOXNxyBNr/kmYlKmIH9QwpnmZKAcqlKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvgrRX/btsOXNxyBNr/kmYlKmIH9QwpnmZKAcqlKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvgrRX/btsOXNxyBNr/kmYlKmIH9QwpnmZKAcqlKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvgrRX%2FbtsOXNxyBNr%2FkmYlKmIH9QwpnmZKAcqlKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;215&quot; height=&quot;231&quot; data-origin-width=&quot;410&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 그림은 용량이 4인 버킷이다. 토큰 공급기(refiller)는 이 버킷에 매초 2개의 토큰을 추가한다. 버킷이 가득 차면 추가로 공급된 토큰은 버려진다(overflow).&lt;/li&gt;
&lt;li&gt;각 요청은 처리될 때마다 하나의 토큰을 사용한다. 요청이 도착하면 버킷에 충분한 토큰이 있는지 검사하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;724&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vR4X1/btsOWMFWQSJ/TN5Vxm9F6A1f2hWdHhE4y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vR4X1/btsOWMFWQSJ/TN5Vxm9F6A1f2hWdHhE4y0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vR4X1/btsOWMFWQSJ/TN5Vxm9F6A1f2hWdHhE4y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvR4X1%2FbtsOWMFWQSJ%2FTN5Vxm9F6A1f2hWdHhE4y0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;317&quot; height=&quot;291&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;724&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;충분한 토큰이 있는 경우, 버킷에서 토큰 하나를 꺼낸 후 요청을 시스템에 전달한다.&lt;/li&gt;
&lt;li&gt;충분한 토큰이 없는 경우, 해당 요청은 버려진다(dropped).&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 그림은 토큰을 어떻게 버킷에서 꺼내고, 토큰 공급기는 어떻게 동작하며, 처리 제한 로직은 어떻게 작동하는지를 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예에서 토큰 버킷의 크기는 4이다. 토큰 공급률(refill rate)은 분당 4이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;864&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RRRKx/btsOWkJHcCX/B2UVrluERENGfyUPItYd9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RRRKx/btsOWkJHcCX/B2UVrluERENGfyUPItYd9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RRRKx/btsOWkJHcCX/B2UVrluERENGfyUPItYd9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRRRKx%2FbtsOWkJHcCX%2FB2UVrluERENGfyUPItYd9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;310&quot; height=&quot;395&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;864&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 토큰 버킷 알고리즘은 2개 인자를 받는다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;버킷 크기 : 버킷에 담을 수 있는 토큰의 최대 개수&lt;/li&gt;
&lt;li&gt;토큰 공급률(refill rate): 초당 몇 개의 토큰이 버킷에 공급되는가&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버킷은 몇 개나 사용해야 할까? 공급 제한 규칙에 따라 달라진다. 다음 사례를 살펴보자&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;통상적으로, &lt;b&gt;API 엔드포인트마다 별도의 버킷을 둔다&lt;/b&gt;. 예를 들어, 사용자마다 하루에 한 번만 포스팅할 수 있고, 친구는 150명까지 추가할 수 있고, 좋아요 버튼은 다섯 번까지만 누를 수 있다면, 사용자마다 3개의 버킷을 두어야 할 것이다.&lt;/li&gt;
&lt;li&gt;IP 주소별로 처리율 제한을 적용해야 한다면 &lt;b&gt;IP 주소마다 버킷을 하나씩 할당&lt;/b&gt;해야 한다.&lt;/li&gt;
&lt;li&gt;시스템의 처리율을 초당 10,000개 요청으로 제한하고 싶다면, &lt;b&gt;모든 요청이 하나의 버킷을 공유&lt;/b&gt;하도록 해야 할 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;구현이 쉽다.&lt;/li&gt;
&lt;li&gt;메모리 사용 측면에서도 효율적이다.&lt;/li&gt;
&lt;li&gt;짧은 시간에 집중되는 트래픽(burst of traffic)도 처리 가능하다. 버킷에 남은 토큰이 있기만 하면 요청은 시스템에 전달될 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 알고리즘은 버킷 크기와 토큰 공급률이라는 두 개 인자를 가지고 있는데, 이 값을 적절하게 튜닝하는 것은 까다로운 일이다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 누출 버킷 알고리즘&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누출 버킷(leaky bucket) 알고리즘은 토큰 버킷 알고리즘과 비슷하지만 요청 처리율이 고정되어 있다는 점이 다르다. 누출 버킷 알고리즘은 보통 FIFO 큐로 구현한다.&lt;br /&gt;동작 원리는 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;요청이 도착하면 큐가 가득 차 있는지 본다. 빈자리가 있는 경우에는 큐에 요청을 추가한다.&lt;/li&gt;
&lt;li&gt;큐가 가득 차 있는 경우에는 새 요청은 버린다.&lt;/li&gt;
&lt;li&gt;지정된 시간마다 큐에서 요청을 꺼내어 처리한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림으로 살펴보면 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1064&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/elTdsi/btsOXheEZcw/i6Wp3nH1oL52wKwVZLGWY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/elTdsi/btsOXheEZcw/i6Wp3nH1oL52wKwVZLGWY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/elTdsi/btsOXheEZcw/i6Wp3nH1oL52wKwVZLGWY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FelTdsi%2FbtsOXheEZcw%2Fi6Wp3nH1oL52wKwVZLGWY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;494&quot; height=&quot;197&quot; data-origin-width=&quot;1064&quot; data-origin-height=&quot;424&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;누출 버킷 알고리즘은 다음 두 인자를 사용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버킷 크기 : 큐 사이즈와 같은 값이다. 큐에는 처리될 항목들이 보관된다.&lt;/li&gt;
&lt;li&gt;처리율(outflow rate) : 지정된 시간당 몇 개의 항목을 처리할지 지정하는 값이다. 보통 초 단위로 표현된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전자상거래 기업인 Shopify가 이 알고리즘을 사용하여 처리율 제한을 구현하고 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;큐의 크기가 제한되어 있어 메모리 사용량 측면에서 효율적이다.&lt;/li&gt;
&lt;li&gt;고정된 처리율을 갖고 있어 안정적 출력(stable outflow rate)이 필요한 경우에 적합하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단시간에 많은 트래픽이 몰리는 경우 큐에는 오래된 요청들이 쌓이게 되고, 그 요청들을 제때 처리 못하면 최신 요청들은 버려지게 된다.&lt;/li&gt;
&lt;li&gt;두 개 인자를 갖고 있는데, 이들을 올바르게 튜닝하기가 까다로울 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 고정 윈도 카운터 알고리즘&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고정 윈도 카운터(fixed window counter) 알고리즘은 다음과 같이 동작한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;타임라인을 고정된 간격의 윈도로 나누고, 각 윈도마다 카운터를 붙인다.&lt;/li&gt;
&lt;li&gt;요청이 접수될 때마다 이 카운터의 값은 1씩 증가한다.&lt;/li&gt;
&lt;li&gt;이 카운터의 값이 사전에 설정된 임계치(threshold)에 도달하면 새로운 요청은 새 윈도가 열릴 때까지 버려진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 원리를 구체적인 예제로 살펴보자!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;574&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbRk6d/btsOWh7jXOl/L5KlGXjGLQ0JUZrPbtFkr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbRk6d/btsOWh7jXOl/L5KlGXjGLQ0JUZrPbtFkr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbRk6d/btsOWh7jXOl/L5KlGXjGLQ0JUZrPbtFkr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbRk6d%2FbtsOWh7jXOl%2FL5KlGXjGLQ0JUZrPbtFkr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;423&quot; height=&quot;261&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;574&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림에서 타임라인의 시간 단위는 1초이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템은 초당 3개까지의 요청만 응 허용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매초마다 열리는 윈도에 3개 이상의 요청이 밀려오면 초과분은 버려진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 알고리즘의 가장 큰 문제는 윈도의 경계 부근에 순각적으로 많은 트래픽이 집증 될 경우 윈도에 할당된 양보다 더 많은 요청이 처리될 수 있다는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/34gLJ/btsOY19DNu8/crrON1QFkhXTiSPCi181Qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/34gLJ/btsOY19DNu8/crrON1QFkhXTiSPCi181Qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/34gLJ/btsOY19DNu8/crrON1QFkhXTiSPCi181Qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F34gLJ%2FbtsOY19DNu8%2FcrrON1QFkhXTiSPCi181Qk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;454&quot; height=&quot;226&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림에서 예로 든 시스템은 분당 최대 5개의 요청만을 허용하는 시스템이다.&lt;br /&gt;카운터는 매분마다 초기화된다. 이 예를 보면 2:00:00, 2:01:00 사이에 다섯 개의 요청이 들어왔고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2:01:00과 2:02:00 사이에 또 다섯 개의 요청이 들어왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;윈도 위치를 조금 옮겨 2:00:30부터 2:01:30까지의 1분 동안을 살펴보면, 이 1분 동안 시스템이 처리한 요청은 10개이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;허용 한도의 2배인 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고정 윈도 카운터 알고리즘의 장단점으로는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 효율이 좋다.&lt;/li&gt;
&lt;li&gt;이해하기 쉽다.&lt;/li&gt;
&lt;li&gt;윈도가 닫히는 시점에 카운터를 초기화하는 방식은 특정한 트래픽 패턴을 처리하기에 적합하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;윈도 경계 부근에서 일시적으로 많은 트래픽이 몰리는 경우, 기대했던 시스템의 처리 한도보다 많은 양의 요청을 처리하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 이동 윈도 로깅 알고리즘&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 살펴본 대로, 고정 윈도 카운터 알고리즘에는 중대한 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 윈도 경계 부근에 트래픽이 집중된 경우 시스템에 설정된 한도보다 많은 요청을 처리하게 된다는 것이다.&lt;br /&gt;이동 윈도 로깅 알고리즘은 이 문제를 해결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작원리는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 알고리즘은 요청의 타임스탬프를 추적한다. 타임스탬프 데이터는 보통 Redis와 정렬 집합(sorted set) 같은 캐시에 보관한다.&lt;/li&gt;
&lt;li&gt;새 요청이 오면 만료된 타임스탬프는 제거한다. 만료된 타임스탬프는 그 값이 현재 윈도의 시작 시점보다 오래된 타임스탬프를 말한다.&lt;/li&gt;
&lt;li&gt;새 요청의 타임스탬프를 로그(log)에 추가한다.&lt;/li&gt;
&lt;li&gt;로그의 크기가 허용치보다 같거나 작으면 요청을 시스템에 전달한다. 그렇지 않은 경우에는 처리를 거부한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;676&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGMOQ4/btsOXQA9wWS/rfQd7PT1ZV5o4Kin4m1QPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGMOQ4/btsOXQA9wWS/rfQd7PT1ZV5o4Kin4m1QPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGMOQ4/btsOXQA9wWS/rfQd7PT1ZV5o4Kin4m1QPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGMOQ4%2FbtsOXQA9wWS%2FrfQd7PT1ZV5o4Kin4m1QPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;351&quot; height=&quot;307&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;676&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 분당 최대 2회의 요청만을 처리하도록 설정되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;보통 로그에 보관되는 값은 리눅스 타임스탬프일 것이지만, 본 예제에서는 이해를 돕기 위해 사람이 읽기 적합한 표현법으로 바꾸었다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;요청이 &lt;code&gt;1:00:01&lt;/code&gt;에 도착하였을 때, 로그는 비어 있는 상태다. 따라서 요청은 허용된다.&lt;/li&gt;
&lt;li&gt;새로운 요청이 &lt;code&gt;1:00:30&lt;/code&gt;에 도착한다.&lt;br /&gt;해당 타임스탬프가 로그에 추가된다. 추가 직후 로그의 크기는 2이며, 허용 한도보다 크지 않은 값이다. 따라서 요청은 시스템에 전달된다.&lt;/li&gt;
&lt;li&gt;새로운 요청이 &lt;code&gt;1:00:50&lt;/code&gt;에 도착한다.&lt;br /&gt;해당 타임스탬프가 로그에 추가된다. 추가 직후 로그의 크기는 3으로, 허용 한도보다 큰 값이다. 따라서 타임스탬프는 로그에 남지만 요청은 거부된다.&lt;/li&gt;
&lt;li&gt;새로운 요청이 &lt;code&gt;1:01:40&lt;/code&gt;에 도착한다.&lt;br /&gt;&lt;code&gt;1:00:40&lt;/code&gt;, &lt;code&gt;1:01:40&lt;/code&gt; 범위 안에 있는 요청은 1분 윈도 안에 있는 요청이지만, &lt;code&gt;1:00:40&lt;/code&gt; 이전의 타임스탬프는 전부 만료된 값이다. 따라서 두 개의 만료된 타임스탬프 &lt;code&gt;1:00:01&lt;/code&gt;과 &lt;code&gt;1:00:30&lt;/code&gt;을 로그에서 삭제한다. 삭제 직후 로그의 크기는 2이다.&lt;br /&gt;따라서, &lt;code&gt;1:01:40&lt;/code&gt;의 신규 요청은 시스템에 전달된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이동 윈도 로깅 알고리즘의 장단점은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 알고리즘이 구현하는 처리율 제한 메커니즘은 아주 정교하다. 어느 순간의 윈도를 보더라도, 허용되는 요청의 개수는 시스템의 처리율 한도를 넘지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;거부된 요청의 타임스탬프도 보관하기 때문에 다량의 메모리를 사용하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;❓ 거부된 요청의 타임스탬프도 저장해야 하는 이유&lt;br /&gt;&lt;/b&gt;&lt;br /&gt;이유는 정확한 판단을 위해서이다.&lt;br /&gt;이동 윈도 로깅 방식은 요청마다 타임스탬프를 기록하고, 현재 시간 기준으로 슬라이딩 윈도 내의 요청 개수를 계산한다.&lt;br /&gt;&lt;br /&gt;여기서 중요한 점은 요청이 허용되었든 거부되었든, 해당 시점에 클라이언트는&amp;nbsp;실제로 요청을 시도한 것이다. &lt;br /&gt;따라서&amp;nbsp;전체 요청 시도 수를 기준으로 제한을 판단해야 하며, 거부된 요청도 포함되어야 한다.&lt;br /&gt;예를 들어, &quot;최근 1분 동안 100건 이하&quot;가 제한 조건이라면, 허용된 95건 + 거부된 5건 = 100건으로 보고&amp;nbsp;다음 요청은 거부되어야 한다. 요약하자면, 모든 요청의 시도(허용 여부와 무관)가 로깅되어야 윈도 범위 내 요청 수를 정확히 계산할 수 있고, 그 기준이 정확한 처리를 가능하게 한다.&lt;br /&gt;이 부분 관련해서&amp;nbsp;&lt;a href=&quot;https://www.reddit.com/r/AskComputerScience/comments/xktn2j/rate_limiting_why_log_rejected_requests/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;레딧&lt;/a&gt;이 있는데 다들 좀 헷갈려하는 듯..&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❗ 많은 메모리를 사용함에도 불구하고 이 알고리즘을 선택하는 상황&lt;br /&gt;이동 윈도 로깅 알고리즘의 장점은 &lt;b&gt;정확성&lt;/b&gt;이다. 다음과 같은 경우에 유리하다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;민감한 보안이나 인증 API처럼 과도한 요청을 엄격히 제한해야 할 때&lt;/li&gt;
&lt;li&gt;사용량 과금 API처럼 정밀한 요청 수 추적이 필요한 경우&lt;/li&gt;
&lt;li&gt;실시간 서비스 품질(QoS)을 보장해야 하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 &lt;b&gt;메모리 사용량이 증가하더라도&lt;/b&gt;, 정확한 요청 수 추적이 중요한 시나리오에서는 이동 윈도 로깅 알고리즘이 유리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 이동 윈도 카운터 알고리즘&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이동 윈도 카운터(sliding window counter) 알고리즘은 고정 윈도 카운터 알고리즘과 이동 윈도 로깅 알고리즘을 결합한 것이다.&lt;br /&gt;이 알고리즘을 구현하는 데는 두 가지 접근법이 사용될 수 있는데 그중 하나를 살펴보면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간에 따라 움직이는 윈도의 카운터를 어떻게 계산할까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 타임슬롯의 요청수 + 직전 타임슬롯의 요청 수 * 이동 윈도와 직전 타임슬롯이 겹치는 비율&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwQ4kr/btsOW5MCZeg/iJelqEt9TKOKa64ZTqlw0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwQ4kr/btsOW5MCZeg/iJelqEt9TKOKa64ZTqlw0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwQ4kr/btsOW5MCZeg/iJelqEt9TKOKa64ZTqlw0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwQ4kr%2FbtsOW5MCZeg%2FiJelqEt9TKOKa64ZTqlw0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;466&quot; height=&quot;291&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;처리율 제한 장치의 한도가 분당 7개 요청으로 설정되어 있다고 가정하고, 이전 1분 동안 5개의 요청이, 그리고 현재 1분 동안 3개의 요청이 들어왔다고 해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;현재 1분의 30% 시점에 도착한 새 요청의 경우, 현재 윈도에 몇 개의 요청이 온 것으로 보고 처리해야 할까? 다음과 같이 계산할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 1분간의 요청 수 + 직전 1분간의 요청 수 * 이동 윈도와 직전 1분이 겹치는 비율&lt;/li&gt;
&lt;li&gt;이 공식에 따르면 현재 윈도에 들어 있는 요청은 3+5*70% = 6.5개이다.&lt;br /&gt;반올림해서 쓸 수도 있고 내림하여 쓸 수도 있는데, 지금은 내림하여 6으로 쓰겠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제의 경우 처리율 제한 한도가 분당 7개 요청이라고 했으므로, 현재 1분의 30% 시점에 도달한 신규 요청은 시스템으로 전달된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 그 직후에는 한도에 도달하였으므로 더 이상 요청을 받을 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이동 윈도 카운터 알고리즘의 장단점은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이전 시간대의 평균 처리율에 따라 현재 윈도의 상태를 계산하므로 짧은 시간에 몰리는 트래픽에도 잘 대응한다.&lt;/li&gt;
&lt;li&gt;메모리 효율이 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;직전 시간대에 도착한 요청이 균등하게 분포되어 있다고 가정한 상태에서 추정치를 계산하기 때문에 다소 느슨하다.&lt;/li&gt;
&lt;li&gt;하지만 위 문제는 심각한 것은 아니다. 클라우드플레어가 실시했던 실험에 따르면 40억 개의 요청 가운데 시스템의 실제 상태와 맞지 않게 허용되거나 버려진 요청은 0.003%에 불과하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개략적인 아키텍처&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리율 제한 알고리즘의 기본 아이디어는 단순하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;얼마나 많은 요청이 접수되었는지를 추적할 수 있는 카운터를 추적 대상별로 두고(사용자별로 추적할 것인지, IP 주소별로 할 것인지, API 엔드포인트나 서비스 단위로 할 것인지), 이 카운터의 값이 어떤 한도를 넘어서면 한도를 넘어 도착한 요청은 거부하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 이 카운터는 어디에 보관해야 할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스는 디스크 접근 때문에 느리니까 사용하기엔 어려울 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리상에서 동작하는 캐시가 바람직한데, 빠른 데다가 시간에 기반한 만료 정책을 지원하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일례로 Redis는 처리율 제한 장치를 구현할 때 자주 사용되는 메모리 기반 저장장치로서, &lt;code&gt;INCR&lt;/code&gt;, &lt;code&gt;EXPIRE&lt;/code&gt; 이 두 가지 명령어를 지원한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;INCR: 메모리에 저장된 카운터의 값을 1만큼 증가시킨다.&lt;/li&gt;
&lt;li&gt;EXPIRE: 카운터에 타임아웃 값을 설정한다. 설정된 시간이 지나면 카운터는 자동으로 삭제된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개략적인 구조는 다음 그림과 같다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동작 원리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 처리율 제한 미들웨어에게 요청을 보낸다.&lt;/li&gt;
&lt;li&gt;처리율 제한 미들웨어는 레디스의 지정 버킷에서 카운터를 가져와서 한도에 도달했는지 아닌지를 판단한다.&lt;/li&gt;
&lt;li&gt;한도에 도달했다면 요청은 거부된다.&lt;/li&gt;
&lt;li&gt;한도에 도달하지 않았다면 요청은 API 서버로 전달된다. 한편 미들웨어는 카운터의 값을 증가시킨 후 다시 레디스에 저장된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3단계: 상세 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전의 개략적인 설계만 봐서는 다음과 같은 사항을 알 수 없다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처리율 제한 규칙은 어떻게 만들어지고 어디에 저장되는가?&lt;/li&gt;
&lt;li&gt;처리가 제한된 요청들은 어떻게 처리되는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 처리율 제한 규칙에 관한 내용부터 차례대로 살펴보자&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처리율 제한 규칙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리프트(Lyft)의 오픈소스인 &lt;a href=&quot;https://github.com/envoyproxy/ratelimit#overview&quot;&gt;ratelimit&lt;/a&gt;을 사용하면 처리율 제한 규칙을 이런 식으로 정의할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;domain: messaging
descriptors:
  # Only allow 5 marketing messages a day
  - key: message_type
    value: marketing
    descriptors:
      - key: to_number
        rate_limit:
          unit: day
          requests_per_unit: 5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위는 시스템이 처리할 수 있는 마케팅 메시지의 최대치를 하루 5개로 제한하는 설정이다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;domain: auth
descriptors:
    - key: auth_type
      Value: login
      rate_limit:
          unit: minute
        requests_per_unit:5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 규칙은 클라이언트가 분당 5회 이상 로그인 할 수 없도록 제한하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 규칙들은 보통 설정 파일(config, configuration file) 형태로 디스크에 저장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈소스를 사용하지 않더라도 우리가 풀어야 하는 문제에서는 이런 식으로 규칙을 유동적으로 정할 수 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처리율 한도 초과 트래픽의 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 요청이 한도 제한에 걸리면 API는 HTTP 429 응답(too many requests)을 클라이언트에게 보낸다. 경우에 따라서는 한도 제한에 걸린 메시지를 나중에 처리하기 위해 큐에 보관할 수도 있다. 예를 들어 어떤 주문이 시스템 과부하 때문에 한도 제한에 걸렸다고 할 때, 해당 주문들은 보관했다가 나중에 처리할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;처리율 제한 장치가 사용하는 HTTP 헤더&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 자기 요청이 처리율 제한에 걸리고 있는지를(throttle) 어떻게 감지할까? 자기 요청이 처리율 제한에 걸리기까지 얼마나 많은 요청을 보낼 수 있는지 어떻게 알 수 있을까?&lt;br /&gt;답은 HTTP 응답 헤더에 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;X-Ratelimit-Remaining&lt;/code&gt;: 윈도 내에 남은 처리 가능 요청의 수&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-Ratelimit-Limit&lt;/code&gt;: 매 윈도마다 클라이언트가 전송할 수 있는 요청의 수&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-Ratelimit-Retry-After&lt;/code&gt;: 한도 제한에 걸리지 않으려면 몇 초 뒤에 요청을 다시 보내야 하는지 알림.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 너무 많은 요청을 보내면 429(too many requests) 오류를 &lt;code&gt;X-Ratelimit_Retry_After&lt;/code&gt; 헤더와 함께 반환하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상세 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 그림은 처리율 제한 장치의 상세한 설계 도면이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1052&quot; data-origin-height=&quot;912&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSpSVg/btsOZdaUL97/C91oCzHUhN3TGOlrFeApWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSpSVg/btsOZdaUL97/C91oCzHUhN3TGOlrFeApWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSpSVg/btsOZdaUL97/C91oCzHUhN3TGOlrFeApWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSpSVg%2FbtsOZdaUL97%2FC91oCzHUhN3TGOlrFeApWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;521&quot; height=&quot;912&quot; data-origin-width=&quot;1052&quot; data-origin-height=&quot;912&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처리율 제한 규칙은 디스크에 보관한다. 작업 프로세스(workers)는 수시로 규칙을 디스크에서 읽어 캐시에 저장한다.&lt;/li&gt;
&lt;li&gt;클라이언트가 요청을 서버에 보내면 요청은 먼저 처리율 제한 미들웨어에 도달한다.&lt;/li&gt;
&lt;li&gt;처리율 제한 미들웨어는 제한 규칙을 캐시에서 가져온다. 아울러 카운터 및 마지막 요청의 타임스탬프(timestamp)를 레디스 캐시에서 가져온다. 가져온 값들에 근거하여 해당 미들웨어는 다음과 같은 결정을 내린다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 요청이 처리율 제한에 걸리지 않은 경우에는 API 서버로 보낸다.&lt;/li&gt;
&lt;li&gt;해당 요청이 처리율 제한에 걸렸다면 429 에러를 클라이언트에 보낸다. 한편 해당 요청은 그대로 버릴 수도 있고 메시지 큐에 보관할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;분산 환경에서의 처리율 제한 장치의 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 서버를 지원하는 처리율 제한 장치를 구현하는 것은 어렵지 않다. 하지만 여러 대의 서버와 병렬 스레드를 지원하도록 시스템을 확장하는 경우 다음 두 가지 문제를 해결해야 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;경쟁 조건 (race condition)&lt;/li&gt;
&lt;li&gt;동기화 (synchronization)&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;경쟁 조건&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 살펴본 대로, 처리율 제한 장치는 다음과 같이 동작한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레디스에서 카운터 값을 읽는다.&lt;/li&gt;
&lt;li&gt;counter+1 값이 임계치를 넘는지 본다.&lt;/li&gt;
&lt;li&gt;넘지 않는다면 레디스에 보관된 카운터 값을 1만큼 증가시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병행성이 심한 환경에서는 다음 그림과 같은 경쟁 조건 이슈가 발생할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJ5egl/btsOXFUdlLV/FdpNKkcfOZKwVM7tPHhm5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJ5egl/btsOXFUdlLV/FdpNKkcfOZKwVM7tPHhm5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJ5egl/btsOXFUdlLV/FdpNKkcfOZKwVM7tPHhm5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJ5egl%2FbtsOXFUdlLV%2FFdpNKkcfOZKwVM7tPHhm5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;442&quot; height=&quot;235&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;레디스에 저장된 변수 &lt;code&gt;counter&lt;/code&gt;의 값이 3이라고 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그리고 두 개 요청을 처리하는 스레드가 각각 병렬로 &lt;code&gt;counter&lt;/code&gt; 값을 읽었으며 그 둘 가운데 어느 쪽도 아직 변경된 값을 저장하지는 않은 상태라 해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 다른 요청의 처리 상태는 상관하지 않고 &lt;code&gt;counter&lt;/code&gt;에 1을 더한 값을 레디스에 기록할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;code&gt;counter&lt;/code&gt;의 값을 올바르게 바뀌었다고 판단했겠지만 그림과 같이 &lt;code&gt;counter&lt;/code&gt;의 값은 5가 아닌 4가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경쟁 조건 문제를 해결하는 가장 널리 알려진 해결책은 바로 락(lock)이다.&lt;br /&gt;하지만 락은 시스템의 성능을 상당히 떨어뜨린다는 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설계의 경우 에는 락 대신 쓸 수 있는 해결책이 두 가지 있는데, 하나는 &lt;a href=&quot;https://ko.wikipedia.org/wiki/%EB%A3%A8%EC%95%84_(%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D_%EC%96%B8%EC%96%B4)&quot;&gt;루아 스크립트(Lua script)&lt;/a&gt;이고 또 다른 하나는 &lt;a href=&quot;https://redis.io/docs/data-types/sorted-sets/&quot;&gt;정렬 집합(sorted set)&lt;/a&gt;이라 불리는 레디스 자료구조를 쓰는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;동기화 이슈&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기화는 분산 환경에서 고려해야 할 또 다른 중요한 요소다. 수백만 사용자를 지원하려면 한 대의 처리율 제한 장치 서버로는 충분하지 않을 수 있다. 그래서 처리율 제한 장치 서버를 여러 대 두면 동기화가 필요해진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1094&quot; data-origin-height=&quot;366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t06oL/btsOYE1n4EZ/mP86haE0sJoOcL8RNYWe91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t06oL/btsOYE1n4EZ/mP86haE0sJoOcL8RNYWe91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t06oL/btsOYE1n4EZ/mP86haE0sJoOcL8RNYWe91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft06oL%2FbtsOYE1n4EZ%2FmP86haE0sJoOcL8RNYWe91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;490&quot; height=&quot;164&quot; data-origin-width=&quot;1094&quot; data-origin-height=&quot;366&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;왼쪽그림의 경우 클라이언트 1은 제한 장치 1에 요청을 보내고 클라이언트 2는 제한 장치 2에 요청을 보내고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 계층은 무상태(stateless)이므로 클라이언트는 다음 요청을 오른쪽 그림처럼 각기 다른 제한 장치로 보내게 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 동기화를 하지 않는다면 제한 장치 1은 클라이언트 2에 대해서는 아무것도 모르므로 처리율 제한을 올바르게 수행할 수 없을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 대한 한 가지 해결책은 &lt;code&gt;고정 세션(sticky session)&lt;/code&gt;을 활용하여 같은 클라이언트로부터의 요청은 항상 같은 처리율 제한 장치로 보낼 수 있도록 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 방법은 규모면에서 확장 가능하지도 않고 유연하지도 않기에 쓰는 것을 고려해보아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 나은 해결책은 &lt;code&gt;레디스&lt;/code&gt;와 같은 중앙 집중형 데이터 저장소를 쓰는 것이다. 이 접근법에 기반한 설계는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhFqqb/btsOYSry04r/GJXALA2FrEXIVkfHvad8oK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhFqqb/btsOYSry04r/GJXALA2FrEXIVkfHvad8oK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhFqqb/btsOYSry04r/GJXALA2FrEXIVkfHvad8oK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhFqqb%2FbtsOYSry04r%2FGJXALA2FrEXIVkfHvad8oK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;448&quot; height=&quot;198&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;성능 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 살펴본 설계는 두 가지 지점에서 성능 최적화 개선이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째로, 여러 데이터 센터를 지원하는 문제는 처리율 제한 장치에 매우 중요한 문제라는 것을 상기하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 센터에서 멀리 떨어진 사용자를 지원하려다 보면 지연시간(latency)이 증가할 수밖에 없기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 클라우드 서비스 사업자는 세계 곳곳에 에지 서버(edge server)가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;예를 들어, 클라우드플레어는 지역적으로 분산된 194곳의 위치에 엣지 서버를 설치해두고 있다. (2020/05/20 기준)&lt;br /&gt;사용자의 트래픽을 가장 가까운 엣지 서버로 전달하여 지연시간을 줄인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째로 고려해야 할 것은 제한 장치 간에 데이터를 동기화할 때 최종 일관성 모델(eventual consistency model)을 사용하는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모니터링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리율 제한 장치를 설치한 이후에는 효과적으로 동작하는지 보기 위해 데이터를 모을 필요가 있다. 기본적으로 모니터링을 통해 확인하려는 것은 다음 두 가지다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;채택된 처리율 제한 알고리즘이 효과적이다.&lt;/li&gt;
&lt;li&gt;정의한 처리율 제한 규칙이 효과적이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 처리율 제한 규칙이 너무 빡빡하게 설정되었다면 많은 유효 요청이 처리되지 못하고 버려질 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럴 경우에는 규칙을 다소 완화할 필요가 있다.&lt;br /&gt;깜짝 세일 같은 이벤트 때문에 트래픽이 급증할 때 처리율 제한 장치가 비효율적으로 동작한다면, 그런 트래픽 패턴을 잘 처리할 수 있도록 알고리즘을 바꾸는 것을 생각해 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럴 경우에는 토큰 버킷이 적합할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4단계: 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리율 제한을 구현하는 알고리즘(토큰 버킷, 누출 버킷, 고정 윈도 카운터, 이동 윈도 로그, 이동 윈도 카운터)과 알고리즘을 구현하는 아키텍처, 분산환경에서의 처리율 제한 장치, 성능 최적화와 모니터링 등의 주제를 살펴보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;추가적으로, 처리율 제한 장치에 있어 다음과 같은 부분도 찾아보면 좋을 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;경성(hard) 또는 연성(soft) 처리율 제한
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;경성 처리율 제한: 요청의 개수는 임계치를 절대 넘어설 수 없다.&lt;/li&gt;
&lt;li&gt;연성 처리율 제한: 요청 개수는 잠시 동안은 임계치를 넘어설 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다양한 계층에서의 처리율 제한
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;4장에서는 OSI 7계층 중 application layer 계층에서의 처리율 제한에 대해서만 다뤘다. 하지만 다른 계층에서도 처리율 제한이 가능하다.&lt;br /&gt;예를 들어, IPtables를 사용하면 IP 주소에 처리율 제한을 적용하는 것이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;처리율 제한을 회피하는 방법. 클라이언트를 어떻게 설계하는 것이 최신인가?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 측 캐시를 사용하여 API 호출 횟수를 줄인다.&lt;/li&gt;
&lt;li&gt;처리율 제한의 임계치를 이해하고, 짧은 시간 동안 너무 많은 메시지를 보내지 않도록 한다.&lt;/li&gt;
&lt;li&gt;예외나 에러를 처리하는 코드를 도입하여 클라이언트가 예외적 상황으로부터 복구될 수 있도록 한다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jungseob86.tistory.com/12&quot;&gt;재시도(retry)&lt;/a&gt; 로직을 구현할 때는 충분한 백오프(back-off) 시간을 둔다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 및 참고&lt;br /&gt;&lt;a href=&quot;https://velog.io/@minu/4%EC%9E%A5.-%EC%B2%98%EB%A6%AC%EC%9C%A8-%EC%A0%9C%ED%95%9C-%EC%9E%A5%EC%B9%98%EC%9D%98-%EC%84%A4%EA%B3%84&quot;&gt;https://velog.io/@minu/4%EC%9E%A5.-%EC%B2%98%EB%A6%AC%EC%9C%A8-%EC%A0%9C%ED%95%9C-%EC%9E%A5%EC%B9%98%EC%9D%98-%EC%84%A4%EA%B3%84&lt;/a&gt;&lt;/p&gt;</description>
      <category>   Dev/System Design</category>
      <author>현주먹</author>
      <guid isPermaLink="true">https://javacatcher.tistory.com/224</guid>
      <comments>https://javacatcher.tistory.com/224#entry224comment</comments>
      <pubDate>Mon, 30 Jun 2025 17:01:59 +0900</pubDate>
    </item>
  </channel>
</rss>