<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ye_suri_106.log</title>
        <link>https://velog.io/</link>
        <description>코딩 해라 스리스리 예스리 얍!</description>
        <lastBuildDate>Wed, 21 Aug 2024 11:09:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ye_suri_106.log</title>
            <url>https://velog.velcdn.com/images/ye_suri_106/profile/c920e0a0-8ee2-4cdf-a777-9f01e9b31d1e/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ye_suri_106.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ye_suri_106" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[11장 결제 시스템]]></title>
            <link>https://velog.io/@ye_suri_106/11%EC%9E%A5-%EA%B2%B0%EC%A0%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
            <guid>https://velog.io/@ye_suri_106/11%EC%9E%A5-%EA%B2%B0%EC%A0%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C</guid>
            <pubDate>Wed, 21 Aug 2024 11:09:00 GMT</pubDate>
            <description><![CDATA[<p>이번 장에서는 결제 시스템을 설계해보도록 한다. 전자상거래에서 가장 중요한 것은 결제 시스템이고, 이러한 결제 시스템은 안정적이고 확장 가능하며 유연해야 한다.</p>
<p>위키 백과에 따르면, &quot;결제 시스템은 금전적 가치의 이전을 통해 금융 거래를 정산하는 데 사용되는 모든 시스템&quot;이다.</p>
<h2 id="1단계-문제-이해-및-설계-범위-확정">1단계 문제 이해 및 설계 범위 확정</h2>
<h3 id="기능-요구사항">기능 요구사항</h3>
<ul>
<li><p>대금 수신 흐름 : 결제 시스템이 판매자를 대신하여 고객으로부터 대금을 수령한다.</p>
</li>
<li><p>대금 정산 흐름 : 결제 시스템이 전 세계의 판매자에게 제품 판매 대금을 송금한다.</p>
<h3 id="비기능-요구사항">비기능 요구사항</h3>
</li>
<li><p>신뢰성 및 내결함성 : 결제 실패는 신중하게 처리해야 한다.</p>
</li>
<li><p>내부 서비스(결제 시스템, 회계 시스템)와 외부 시스템(결제 서비스 제공업체) 간의 조정 프로세스</p>
<ul>
<li><p>여기서 결제 서비스 제공업체는 신용 카드 처리를 직접 하지 않고 사용하게 되는 스트라이프, 브레인트리, 스퀘어 같은 전문 결제 서비스 업체를 뜻한다.</p>
</li>
<li><p>시스템 간의 결제 정보가 일치하는데 비동기적으로 확인한다.</p>
</li>
</ul>
</li>
</ul>
<h3 id="개략적인-규모-추정">개략적인 규모 추정</h3>
<p>이 시스템은 하루에 100만 건의 트랜잭션을 처리해야 하는데, 이는 초당 10건의 트랜잭션(TPS)이다.
10TPS는 일반적인 데이터베이스로 별 문제 없이 처리 가능한 양이므로, 처리 대역폭 대신 결제 트랜잭션의 정확한 처리에 초점을 맞춰 면접을 진행해야 한다.</p>
<h2 id="2단계-개략적-설계안-제시-및-동의-구하기">2단계 개략적 설계안 제시 및 동의 구하기</h2>
<p>결제 흐름은 자금의 흐름을 반영하기 위해 크게 두 단계로 세분화 된다.</p>
<ul>
<li>대금 수신 흐름</li>
<li>대금 정산 흐름</li>
</ul>
<p>전자상거래 사이트 아마존을 예로 들어보자. 구매자가 주문을 하면 아마존의 은행 계좌로 돈이 들어오는데, 이것이 바로 <strong>대금 수신 흐름</strong>이다.</p>
<p>이 돈은 아마존의 은행 계좌에 있지만 소유권이 전부 아마존에 있는 것은 아니다. 판매자가 상당 부분을 소유하며, 아마존은 수수료를 받고 자금 관리자 역할만 수행한다.
나중에 제품이 배송되고 나면, 그때까지 계좌에 묶여 있던 판매 대금에서 수수료를 제외한 잔액이 판매자의 은행 계좌로 지급된다. 이것이 <strong>대금 정산 흐름</strong>이다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/b50f3d31-1d98-4dfc-981d-ee6ccd0e334d/image.png" alt=""></p>
<h3 id="대금-수신-흐름">대금 수신 흐름</h3>
<p>대금 수신 흐름을 개략적인 다이어그램으로 표현하면 아래 그림과 같다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/a03f1d8a-2976-4154-8d7c-888bc4e9ec4d/image.png" alt=""></p>
<blockquote>
<p>결제 서비스</p>
</blockquote>
<p>결제 서비스는 사용자로부터 결제 이벤트를 수락하고 결제 프로세스를 조율한다. 일반적으로 가장 먼저 하는 일은 AML/CFT와 같은 규정을 준수하는지, 자금 세탁이나 테러 자금 조달과 같은 범죄 행위의 증거가 있는지 평가하는 위험 점검이다.</p>
<p>결제 서비스는 이 위험 확인을 통과한 결제만 처리한다. 일반적으로 위험 확인 서비스는 매우 복잡하고 고도로 전문화가 되어있기 때문에 제3자 제공업체를 이용한다.</p>
<blockquote>
<p>결제 실행자</p>
</blockquote>
<p>결제 실행자는 결제 서비스 공급자(PSP)를 통해 결제 주문 하나를 실행한다. 하나의 결제 이벤트에는 여러 결제 주문이 포함될 수 있다.</p>
<blockquote>
<p>결제 서비스 공급자</p>
</blockquote>
<p>PSP는 A 계정에서 B 계정으로 돈을 옮기는 역할을 담당한다. 본 예제의 경우에는 구매자의 신용 카드 계좌에서 돈을 인출하는 역할을 맡는다.</p>
<blockquote>
<p>카드 유형</p>
</blockquote>
<p>카드사는 신용 카드 업무를 처리하는 조직이다. 잘 알려진 카드 유형으로는 비자, 마스터카드, 디스커버리 등이 있다.</p>
<blockquote>
<p>원장</p>
</blockquote>
<p>원장은 <strong>결제 트랜잭션에 대한 금융 기록</strong>이다. 예를 들어 사용자가 판매자에게 1달러를 결제하면 사용자로부터 1달러를 인출하고 판매자에게 1달러를 지급하는 기록을 남긴다. 원장 시스템은 전자상거래 웹사이트의 총 수익을 계산하거나 향후 수익을 예측하는 등, <em>결제 후 분석에서 매우 중요한 역할을 한다.</em></p>
<blockquote>
<p>지갑</p>
</blockquote>
<p>지갑에는 <strong>판매자의 계정 잔액을 기록</strong>한다. 특정 사용자가 결제한 총 금액을 기록할 수도 있다. 위의 그림에서도 볼 수 있듯이, 일반적인 결제 흐름읕 다음과 같다.</p>
<ol>
<li>사용자가 &#39;주문하기&#39; 버튼을 클릭하면 결제 이벤트가 생성되어 결제 서비스로 전송된다.</li>
<li>결제 서비스는 결제 이벤트를 데이터베이스에 저장한다.</li>
<li>때로는 단일 결제 이벤트에 여러 결제 주문이 포함될 수 있다. 한 번 결제로 여러 판매자의 제품을 처리하는 경우가 그 예다. 전자상거래 웹사이트에서 한 결제를 여러 결제 주문으로 분할하는 경우, 결제 서비스는 결제 주문마다 결제 실행자를 호출한다.</li>
<li>결제 실행자는 결제 주문을 데이터베이스에 저장한다.</li>
<li>결제 실행자가 외부 PSP를 호출하여 신용 카드 결제를 처리한다.</li>
<li>결제 실행자가 결제를 성공적으로 처리하고 나면 결제 서비스는 지갑을 갱신하여 특정 판매자의 잔고를 기록한다.</li>
<li>지갑 서버는 갱신된 잔고 정보를 데이터베이스에 저장한다.</li>
<li>지갑 서비스가 판매자 잔고를 성공적으로 갱신하면 결제 서비스는 원장을 호출한다.</li>
<li>원장 서비스는 새 원장 정복를 데이터베이스에 추가한다.</li>
</ol>
<h3 id="결제-서비스-api">결제 서비스 API</h3>
<blockquote>
<p>POST /v1/payments</p>
</blockquote>
<p>이 엔드포인트는 결제 이벤트를 실행한다. 앞에서 언급했듯 하나의 결제 이벤트에는 여러 결제 주문이 포함될 수 있다. 요청 매개변수는 아래와 같다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/56676f2a-4828-4ea0-a622-64fb15b1b1aa/image.png" alt=""></p>
<p>payment_orders는 아래 형태를 가진다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/0b15950d-86fe-4338-b8ed-0df6abb43cfe/image.png" alt=""></p>
<p>payment_order_id가 전역적으로 고유한 ID라는 점에 유의하자. 결제 실행자가 타사 PSP에 결제 요청을 전송할 때, PSP는 payment_order_id를 중복제거 ID로 사용한다. 이는 멱등키라고도 한다.</p>
<p>amount 필드의 데이터 유형이 double이 아닌 string이라는 것에 유의하자. 이는 아래와 같은 이유로 string 타입으로 정의한다.</p>
<ol>
<li>프로토콜, 소프트웨어, 하드웨어에 따라 직렬화/역직렬화에 사용하는 숫자 정밀도가 다를 수 있다. 이러한 차이가 의도치 않은 반올림 오류를 유발할 수 있다.</li>
<li>이 숫자는 매우 클 수도 있고 매우 작을 수도 있다.</li>
</ol>
<p>따라서 전송 및 저장 시 숫자는 문자열로 보관하는 것이 좋다. 표시하거나 계산에 쓸 때만 숫자로 변환한다.</p>
<blockquote>
<p>GET /v1/payments/:{id}</p>
</blockquote>
<p>이 엔드포인트는 payment_order_id가 가리키는 단일 결제 주문의 실행 상태를 반환한다.
이 결제 API는 잘 알려진 일부 PSP의 API와 유사하다.</p>
<h3 id="결제-서비스-데이터-모델">결제 서비스 데이터 모델</h3>
<p>결제 서비스에는 결제 이벤트와 결제 주문의 두 개의 테이블이 필요하다.
결제 시스템용 저장소 솔루션을 고를 때 일반적으로 성능은 가장 중요한 고려사항이 아니고, 다음 사항에 중점을 둔다.</p>
<ol>
<li>안정성이 검증 되었는가 ? 즉, 다른 대형 금융 회사에서 수년동안 긍정적인 피드백을 받으며 사용된 적이 있는가 ?</li>
<li>모니터링 및 데이터 탐사에 필요한 도구가 풍부하게 지원되는가 ?</li>
<li>데이터베이스 관리자 채용 시장이 성숙했는가 ? 다시 말해 숙련된 DBA를 쉽게 채용할 수 있는가 ?</li>
</ol>
<p>일반적으로는 NoSQL/NewSQL 보다는 ACID 트랜잭션을 지원하는 전통적인 관계형 데이터베이스를 선호한다.
결제 이벤트 테이블에는 자세한 결제 이벤트 정보가 저장된다. 테이블 스키마는 아래와 같다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/c08c3c9d-e8fe-45e0-a978-8193a2bb201b/image.png" alt=""></p>
<p>결제 주문 테이블에는 각 결제 주문의 실행 상태가 저장된다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/92a4977e-c201-4b1e-a78e-553bacf16377/image.png" alt=""></p>
<ul>
<li>checkout_id는 외래 키다. 한 번의 결제 행위는 하나의 결제 이벤트를 만들고, 하나의 결제 이벤트에는 여러 개의 결제 주문이 포함될 수 있다.</li>
<li>구매자의 신용 카드에서 금액을 공제하기 위해 타사 PSP를 호출하면 판매자 대신 전자상거래 웹사이트의 은행 계좌에 이체가 이루어지는데, 이 프로세스를 대금 수신이라 부른다. 제품이 배송되는 등 대금 정산 조건이 충족되면 해당 대금을 판매자에게 정산하는 절차를 시작한다. 그 결과로 전자상거래 웹사이트의 은행 계좌에서 판매자의 은행 계좌로 금액이 이체된다. 따라서 사용자의 결제를 처리하는 중에는 판매자의 은행 계좌가 아닌, 구매자의 카드 정보만 필요하다.</li>
</ul>
<p>결제 주문 테이블에서 payment_order_status는 결제 주문의 실행 상태를 유지하는 열거 자료형 (enum) 이다. 실행 상태로는 NOT_STARTED, EXECUTING, SUCCESS, FAILED 등이 있다. </p>
<ol>
<li>payment_order_status의 초깃값은 NOT_STARTED이다.</li>
<li>결제 서비스는 결제 실행자에 주문을 전송하면 payment_order_status의 값을 EXECUTING로 바꾼다.</li>
<li>결제 서비스는 결제 처리자의 응답에 따라 payment_order_status의 값을 SUCCESS 또는 FAIL로 변경한다.</li>
</ol>
<p>payment_order_status의 값이 SUCCESS로 되면 결제 서비스는 지갑 서비스를 호출하여 판매자 잔액을 업데이트하고 wallet_updated 필드 값은 TRUE로 업데이트 한다. 여기서는 지갑 업데이트가 항상 성공한다고 가정하고 설계를 단순화하였다.</p>
<p>이 절차가 끝나면 결제 서비스는 다음 단계로 원장 서비스를 호출하여 원장 데이터베이스의 ledger_updated 필드를 TRUE로 갱신한다.
동일한 checked_id 아래의 모든 결제 주문이 성공적으로 처리되면 결제 서비스는 결제 이벤트 테이블의 is_payment_done을 TRUE로 업데이트 한다. 일반적으로, 종결되지 않은 결제 주문을 모니터링 하기 위해 주기적으로 실행되는 작업을 마련해둔다. 이 작업은 임계값 형태로 설정된 기간이 지나도록 완료되지 않은 결제 주문이 있을 경우 살펴보도록 엔지니어에게 경보를 보낸다.</p>
<h3 id="복식부기-원장-시스템">복식부기 원장 시스템</h3>
<p>원장 시스템에는 복식부기라는 아주 중요한 설계 원칙이 있다. 복식부기는 모든 결제 시스템에 필수 요소이며 정확한 기록을 남기는데 핵심적인 역할을 한다. 모든 결제 거래를 두 개의 별도 원장 계좌에 같은 금액으로 기록한다. 한 계좌에서는 차감이 이루어지고 다른 계좌에서는 입금이 이루어진다.</p>
<p>복식부기 시스템에서 모든 거래 항목의 합계는 0이어야 한다. 이 시스템을 활용하면 자금의 흐름을 시작부터 끝까지 추적할 수 있으며 결제 주기 전반에 걸쳐 일관성을 보장할 수 있다.</p>
<h3 id="외부-결제-페이지">외부 결제 페이지</h3>
<p>대부분의 기업은 신용 카드 정보를 취급하지 않기 위해 PSP에서 제공하는 외부 신용 카드 페이지를 사용한다. 웹사이트의 경우 이 외부 신용 카드 페이지는 위젯 또는 iframe이며, 모바일 어플리케이션의 경우에는 결제 SDK에 포함된 사전에 구현된 페이지다. 결국 우리의 결제 서비스가 아니라 PSP가 제공하는 외부 결제 페이지가 직접 고객 카드 정보를 수집한다는 것이다.</p>
<h3 id="대금-정산-흐름">대금 정산 흐름</h3>
<p>대금 정산 흐름의 구성 요소는 대금 수신 흐름과 아주 유사하다. 한 가지 차이는 PSP를 사용하여 구매자의 신용 카드에서 전자상거래 웹사이트 은행 계좌로 돈을 이체하는 대신, 정산 흐름에서는 타사 정산 서비스를 사용하여 전자상거래 웹사이트 은행 계좌에서 판매자 은행 계좌로 돈을 이체한다는 점이다.
일반적으로 결제 시스템은 대금 정산을 위해 티팔티와 같은 외상매입금 지급 서비스 제공 업체를 이용한다.</p>
<h2 id="3단계-상세-설계">3단계 상세 설계</h2>
<h3 id="psp-연동">PSP 연동</h3>
<p>대부분의 회사는 다음 두 가지 방법 중 하나로 결제 시스템을 PSP와 연동한다.</p>
<ol>
<li>회사가 민감한 결제 정보를 안전하게 저장할 수 있다면 API를 통해 PSP와 연동하는 방법을 택할 수 있다. 회사는 결제 웹페이지를 개발하고 민감한 결제 정보를 수집하며, PSP는 은행 연결, 다양한 카드 유형을 지원하는 역할을 한다.</li>
<li>복잡한 규정 및 보안 문제로 인해 민감한 결제 정보를 저장하지 않기로 결정한 경우, PSP는 카드 결제 세부 정보를 수집하여 PSP에 안전하게 저장할 수 있도록 외부 결제 페이지를 제공한다. 대부분의 기업이 택하는 접근법이다.</li>
</ol>
<p>아래는 외부 결제 페이지의 작동 방식을 설명한 것이다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/c9833b54-d1e1-4f96-9172-b3ae4bf47715/image.png" alt=""></p>
<ol>
<li>사용자가 클라이언트 브라우저에서 &#39;결제&#39; 버튼을 클릭한다. 클라이언트는 결제 주문 정보를 담아 결제 서비스를 호출한다.</li>
<li>결제 주문 정보를 수신한 결제 서비스는 결제 등록 요청을 PSP로 전송한다. 이 등록 요청에는 결제 금액, 통화(currency), 결제 요청 만료일, 리디렉션 URL 등의 결제 정보가 포함된다. 결제 주문이 정확히 한 번만 등록될 수 있도록 UUID 필드를 둔다. 이 UUID는 비중복 난라고도 부른다. 이 일반적으로 이 UUID는 결제 주문의 ID로 사용된다.</li>
<li>PSP는 결제 서비스에 토큰을 반환한다. 토큰은 등록된 결제 요청을 유일하게 식별하는, PSP가 발급한 UUID. 나중에 이 토큰을 사용하여 결제 등록 및 결제 실행 상태를 확인할 수 있다.</li>
<li>결제 서비스는 PSP가 제공하는 외부 결제 페이지를 호출하기 전에 토큰을 데이터베이스에 저장한다.</li>
<li>토큰을 저장하고 나면 클라이언트는 PSP가 제공하는 외부 결제 페이지를 표시한다. 모바일 애플리케이션은 일반적으로 이를 위해 PSP SDK를 연동한다. 여기서는 스트라이프 사의 웹 연동 사례를 들겠다. 스트라이프가 제공하는 자바스크립트 라이브러리에는 결제 UI를 표시하고, 민감한 결제 정보를 수집하고, 결제를 완료하는 등의 작업을 위해 PSP를 직접 호출하는 로직이 포함되어 있다. 민감한 결제 정보는 스트라이프가 수집하며, 이런 정보는 우리 시스템으로는 절대로 넘어오지 않는다. 외부 결제 페이지는 일반적으로 다음 두 가지 정보를 필요로 한다.
 a. 4단계에서 받은 토큰: PSP의 자바스크립트 코드는 이 토큰을 사용하여 PSP의 백엔드에서 결제 요청에 대한 상세 정보를 검색한다. 이 과정을 통해 알아내야 하는 중요 정보 하나는 사용자에게서 받을 금액이다.
 b. 리디렉션 URL : 결제가 완료되면 호출될 웹 페이지 URL이다. PSP의 자바스크립트는 결제가 완료되면 브라우저를 리디렉션 URL로 돌려보낸다. 일반적으로 리디렉션 URL은 결제 상태를 표시하는 전자상거래 웹사이트상의 한 페이지다.</li>
<li>사용자는 신용 카드 번호, 소유자 이름, 카드 유효기간 등의 결제 세부 정보를 PSP의 웹 페이지에 입력한 다음 결제 버튼을 클릭한다. PSP가 결제 처리를 시작한다.</li>
<li>PSP가 결제 상태를 반환한다.</li>
<li>이제 사용자는 리디렉션 URL 가리키는 웹 페이지로 보내진다. 이때 보통 7 단계에서 수신된 결제 상태가 URL에 추가된다.</li>
<li>비동기적으로 PSP는 웹혹을 통해 결제 상태와 함께 결제 서비스를 호출한다. 웹혹은 결제 시스템 측에서 PSP를 처음 설정할 때 등록한 URL이다. 결제 시스템이 웹혹을 통해 결제 이벤트를 다시 수신하면 결제 상태를 추출하여 결제 주문 데이터베이스 테이블의 payment_order_status 필드를 최신 상태로 업데이트한다.</li>
</ol>
<p>지금까지 외부 결제 페이지가 잘 동작할 때 시스템들이 어떻게 상호 연동하는지 설명했다. 그러나 실제로는 위의 아홉 단계 각각이 네트워크 문제로 실패할 수 있다. 실제로 장애가 발생하면 체계적으로 처리할 수 있는 방법이 있을까?
조정(reconciliation)이 바로 그 방법이다.</p>
<h3 id="조정">조정</h3>
<p>시스템 구성 요소가 비동기적으로 통신하는 경우 메시지가 전달되거나 응답이 반환된다는 보장이 없다. 이는 시스템 성능을 높이기 위해 비동기 통신을 자주 사용하는 결제 관련 사업에 일반적인 문제다. PSP나 은행 같은 외부 시스템도 비동기 통신을 선호한다. 그렇다면 어떻게 정확성을 보장할 수 있을까?</p>
<p>답은 조정이다. 관련 서비스 간의 상태를 주기적으로 비교하여 일치하는지 확인하는 것이다.
매일 밤 PSP나 은행은 고객에게 정산 파일을 보낸다. 정산 파일에는 은행 계좌의 잔액과 하루 동안 해당 계좌에서 발생한 모든 거래 내역이 기재되어 있다. 조정 시스템은 정산 과일의 세부 정보를 읽어 원장 시스템과 비교한다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/e9c6c299-5a09-4bae-be1e-ce73ba23744b/image.png" alt="">
조정은 결제 시스템의 내부 일관성을 확인할 때도 사용된다. 예를 들어, 원장과 지갑의 상태가 같은지 확인할 수 있다.
조정 중에 발견된 차이는 일반적으로 재무팀에 의뢰하여 수동으로 고친다.
발생 가능한 불일치 문제 및 해결 방안은 다음 세 가지 범주로 나눌 수 있다.</p>
<ol>
<li>어떤 유형의 문제인지 알고 있으며 문제 해결 절차를 자동화할 수 있는 경우: 원인과 해결 방법을 알고 있으며, 자동화 프로그램을 작성하는 것이 비용 효율적인 경우다. 엔지니어는 발생한 불일치 문제의 분류와 조정 작업을 모두 자동화할 수 있다.</li>
<li>어떤 유형의 문제인지는 알지만 문제 해결 절차를 자동화할 수는 없는 경우: 불일치의 원인과 해결 방법을 알고는 있지만 자동 조정 프로그램의 작성 비용이 너무 높다. 발생한 불일치 문제는 작업 대기열에 넣고 재무팀에서 수동으로 수정하도록 한다.</li>
<li>분류할 수 없는 유형의 문제인 경우: 불일치가 어떻게 발생하였는지 알지 못하는 경우다. 이런 불일치 문제는 특별 작업 대기열에 넣고 재무팀에서 조사하도록 한다.</li>
</ol>
<h3 id="결제-지연-처리">결제 지연 처리</h3>
<p>앞서 설명한 것처럼 결제 요청은 많은 컴포넌트를 거치며, 내부 및 외부의 다양한 처리 주체와 연동한다. 대부분의 경우 결제 요청은 몇 초만에 처리되지만, 완료되거나 거부되기까지 몇 시간 또는 며칠이 걸리는 경우도 있다. 다음은 결제 요청이 평소보다 오래 걸리게 되는 몇 가지 사례다.</p>
<ul>
<li>PSP가 해당 결제 요청의 위험성이 높다고 보고 담당자 검토를 요구하는
경우</li>
<li>신용 카드사가 구매 학인 용도로 카드 소유자의 추가 정보를 요청하는 3D 보안 인증 같은 추가 보호 장치를 요구하는 경우</li>
</ul>
<p>결제 서비스는 처리하는 데 시간이 오래 걸리는 이런 요청도 처리할 수 있어야한다. 구매 페이지가 외부 PSP에 호스팅 되는 경우(요즘은 아주 일반적인 관행) PSP는 다음과 같이 처리한다.</p>
<ul>
<li>PSP는 결제가 대기 상태임을 알리는 상태 정보를 클라이언트에 반환하고, 클라이언트는 이를 사용자에게 표시한다. 클라이언트는 또한 고객이 현재 결제 상태를 확인할 수 있는 페이지도 제공한다.</li>
<li>PSP는 우리 회사를 대신하여 대기 중인 결제의 진행 상황을 추적하고, 상태가 바뀌면 PSP에 등록된 웹훅을 통해 결제 서비스에 알린다.</li>
</ul>
<p>결제 요청이 최종적으로 완료되면 PSP는 방금 언급한 사전에 등록된 웹혹을 호출한다. 결제 서비스는 내부 시스템에 기록된 정보를 업데이트하고 고객에게 배송을 완료한다.</p>
<h3 id="내부-서비스-간-커뮤니케이션">내부 서비스 간 커뮤니케이션</h3>
<p>내부 서비스 통신에는 동기식과 비동기식의 두 가지 패턴이 있다.</p>
<blockquote>
<p>동기식 통신</p>
</blockquote>
<p>HTTP와 같은 동기식 통신은 소규모 시스템에서는 잘 작동하지만 규모가 커지면 단점이 분명해진다. 동기식 통신에서 한 요청에 응답을 만드는 처리 주기는 관련된 서비스가 많을수록 길어진다. 단점은 다음과 같다.
    - 성능 저하: 요청 처리에 관계된 서비스 가운데 하나에 발생한 성능 문제가 전체 시스템의 성능에 영향을 끼친다.
    - 장애 격리 곤란: PSP 등의 서비스에 장애가 발생하면 클라이언트는 더 이상 응답을 받지 못한다.
    - 높은 결합도: 요청 발신자는 수신자를 알아야만 한다.
    - 낮은 확장성: 큐를 버퍼로 사용하지 않고서는 갑자스러운 트래픽 중가에 대
웅할 수 있도록 시스템을 확장하기 어렵다.</p>
<blockquote>
<p>비동기 통신</p>
</blockquote>
<p>비동기 통신은 크게 두 가지 범주로 나눌 수 있다.</p>
<ul>
<li><p>단일 수신자: 각 요청(메시지)은 하나의 수신자 또는 서비스가 처리한다. 일반적으로 공유 메시지 큐를 사용해 구현한다. 큐에는 복수의 구독자가 있을 수 있으나 처리된 메시지는 큐에서 바로 제거된다. 아래 그림을 보면 서비스 A와 서비스 B는 모두 같은 메세지 큐를 구독한다.
서비스 A와 서비스 B가 각각 m1과 m2를 처리하면 그림과 같이 두 메시지는 모두 큐에서 사라진다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/dfc38eba-ded6-4178-a7fb-4c1bf9a6e102/image.png" alt=""></p>
</li>
<li><p>다중 수신자: 각 요청(메시지)은 여러 수신자 또는 서버가 처리한다. 카프카는 이런 시나리오를 잘 처리할 수 있다. 소비자가 수신한 메시지는 카프카에서 바로 사라지지 않는다. 따라서 동일한 메시지를 여러 서비스가 받아 처리할 수 있다. 따라서 결제 시스템 구현에 적합한데, 하나의 요청이 푸시 알림 전송, 재무 보고 업데이트, 분석 결과 업데이트 등의 다양한 용도에 쓰 일 수 있기 때문이다. 아래 그림을 보면 카프카에 발행된 하나의 결제 이벤트가 결제 시스템, 분석 서비스, 결제 청구 서비스 등에 입력으로 활용된다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/f4c8c785-8618-4ee0-b824-a5d329229269/image.png" alt=""></p>
<p>일반적으로 보자면 동기식 통신은 설계하기는 쉽지만 서비스의 자율성을 높이기에는 적합하지 않다. 의존성 그래프가 커지면 전반적 성능은 낮아진다. 비동기 통신은 설계의 단순성과 데이터 일관성을 시스템 확장성 및 장애 감내 능력과 맞바꾼 결과다. 비즈니스 로직이 복잡하고 타사 서비스 의존성이 높은 대규모 결제 시스템에는 비동기 통신이 더 나은 선택이다.</p>
<h3 id="결제-실패-처리">결제 실패 처리</h3>
<p>모든 결제 시스템은 실패한 결제를 적절히 처리할 수 있어야 한다. 안정성 및 결합 내성은 결제 시스템의 핵심적 요구사항이다. 이 문제를 해결하는 몇 가지 기법을 알아보자.</p>
<blockquote>
<p>결제 상태 추적</p>
</blockquote>
<p>결제 주기의 모든 단계에서 결제 상태를 정확하게 유지하는 것은 매우 중요하다. 실패가 일어날 때마다 결제 거래의 현재 상태를 파악하고 재시도 또는 환불이 필요한지 여부를 결정한다. 결제 생태는 데이터 추가만 가능한 데이터베이스 테이블에 보관한다.</p>
<blockquote>
<p>재시도 큐 및 실패 메세지 큐</p>
</blockquote>
<p>실패를 잘 처리하기 위해서는 아래 구조와 같이 재시도 큐와 실패 메시지 큐를 두는 것이 바람직하다.</p>
<ul>
<li>재시도 큐: 일시적 오류 같은 재시도 가능 오류는 재시도 큐에 보낸다.</li>
<li>실패 메시지 큐: 반복적으로 처리에 실패한 메시지는 결국에는 실패 메시지 큐로 보낸다. 이 큐는 문제가 있는 메시지를 디버깅하고 격리하여 성공적으로 처리되지 않은 이유를 파악하기 위한 검사에 유용하다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/079554ac-ea8e-4064-9e8e-741335685976/image.png" alt=""></li>
</ul>
<ol>
<li>재시도가 가능한지 확인한다.
 a. 재시도 가능 실패는 재시도 큐로 보낸다.
 b. 잘못된 입력과 같이 재시도가 불가능한 실패는 오류 내역을 데이터베이스에 저장한다.</li>
<li>결제 시스템은 재시도 큐에 쌓인 이벤트를 읽어 실패한 결제를 재시도한다.</li>
<li>결제 거래가 다시 실패하는 경우에는 다음과 같이 처리한다.
 a. 재시도 횟수가 임계값 이내라면 해당 이벤트를 다시 재시도 큐로 보낸다.
 b. 재시도 횟수가 임계값을 넘으면 해당 이벤트를 실패 메시지 큐에 넣는다. 이런 이벤트에 대해서는 별도 조사가 필요할 수도 있다.</li>
</ol>
<p>실무에서 이런 큐가 어떻게 쓰이는지 궁금하다면 우버에서 카프카를 활용해 결제 시스템 안정성과 결함 내성 요건을 어떻게 충족하고 있는지 살펴보면 좋다.</p>
<h3 id="정확히-한-번-전달">정확히 한 번 전달</h3>
<p>결제 시스템에 발생 가능한 가장 심각한 문제 중 하나는 고객에게 이중으로 청구하는 것이다. 결제 주문이 정확히 한 번만 실행되도록 결제 시스템을 설계하는 것이 중요하다.</p>
<p>언뜻 보기에 메시지를 정확히 한 번 전달하는 것은 매우 어려운 문제처럼 느껴지지만, 문제를 두 부분으로 나누면 훨씬 쉽게 해결할 수 있다. 수학적으로 보자면, 다음의 요건이 충족되면 주어진 연산은 정확히 한 번 실행된다.</p>
<ol>
<li>최소 한 번은 실행된다.</li>
<li>최대 한 번 실행된다.</li>
</ol>
<p>지금부터 재시도를 통해 최소 한 번 실행을 보증하는 방법과, 멱등성 검사를 통해 최대 한 번 실행을 보증하는 방법을 알아보도록 하겠다.</p>
<blockquote>
<p>재시도</p>
</blockquote>
<p>간혹 네트워크 오류나 시간 초과로 인해 결제 거래를 다시 시도해야 하는 경우가 있다. 재시도 메커니즘을 활용하면 어떤 결제가 최소 한 번은 실행되도록 보장 가능하다. </p>
<p>예를 들어, 아래 그림에서와 같이 클라이언트가 10단러 결제를 시도하지만 네트워크 연결 상태가 좋지 않아 결제 요청이 계속 실패하는 경우를 생각해 보자. 이 사례에서는 네트워크가 결국 복구되어 네 번째 시도 만에 요청이 성공한다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/a97e618b-935f-4df6-88d8-be6b647f3a56/image.png" alt=""></p>
<p>재시도 메커니즘을 도입할 때는 얼마나 간격을 두고 재시도할지 정하는 것이 중요하다. 일반적으로 사용되는 전략은 다음과 같다.</p>
<ul>
<li>즉시 재시도: 클라이언트는 즉시 요청을 다시 보낸다.</li>
<li>고정 간격: 재시도 전에 일정 시간 기다리는 방안이다.</li>
<li>증분 간격: 재시도 전에 기다리는 시간을 특정한 양만큼 점진적으로 늘려 나가는 방안이다.</li>
<li>지수적 백오프: 재시도 전에 기다리는 시간을 직전 재시도 대비 두 배씩 늘려 나가는 방안. 예를 들어, 요청이 처음 실패하면 1초 후에 재시도하고, 두 번째로 실패하면 2초, 세 번째로 실패하면 4초를 기다린 후 재시도한다.</li>
<li>취소: 요청을 철회하는 방안. 실패가 영구적이거나 재시도를 하더라도 성공 가능성이 낮은 경우에 흔히 사용되는 방안이다.</li>
</ul>
<p>적절한 재시도 전략을 결정하는 것은 어렵다. &#39;모든 상황에 맞는&#39; 해결책은 없다. 다만 일반적으로 적용 가능한 지침은, 네트워크 문제가 단시간 내에 해결 될 것 같지 않다면 지수적 백오프를 사용하라는 것이다. 지나치게 공격적인 재시도 전략은 컴퓨딩 자원을 낭비하고 서비스 과부하를 유발한다. 에러 코드를 반환할 때는 Retry-After 헤더를 같이 붙여 보내는 것이 바람직하다.</p>
<p>재시도 시 발생할 수 있는 잠재적 문제는 이중 결제다. 다음 두 가지 시나리오를 살펴보자.</p>
<ul>
<li><p>시나리오 1: 결제 시스템이 외부 결제 페이지를 통해 PSP와 연동하는 환경에서 클라이언트가 결제 버튼을 두 번 중복 클릭한다.</p>
</li>
<li><p>시나리오 2: PSP가 결제를 성공적으로 하였으나 네트위크 오류로 인해 응답이 결제 시스템에 도달하지 못했다. 사용자가 &#39;결제&#39; 버튼을 다시 클릭하거나 클라이언트가 결제를 다시 시도한다.</p>
</li>
</ul>
<p>이중 결제를 방지하려면 결제는 &#39;최대 한 번&#39; 이루어져야 한다. &#39;최대 한 번 실 행&#39;은 다른 말로 멱등성이라고도 부른다.</p>
<h3 id="멱등성">멱등성</h3>
<p>멱등성은 최대 한 번 실행을 보장하기 위한 핵심 개념이다. API 관점에서 보자면 멱등성은 클라이언트가 같은 API 호출을 여러 번 반복해도 항상 동일한 결과가 나온다는 뜻이다.</p>
<p>클라이언트와 서버 간의 통신을 위해서는 일반적으로 클라이언트가 생성하고 일정 시간이 지나면 만료되는 고유한 값을 멱등키로 사용한다. 스트라이프, 페이팔 같은 많은 기술 회사가 UUID를 멱등키로 권장하며 실제로 널리 쓰인다. 결제 요청의 멱등성을 보장하기 위해서는 HTTP 헤더에 &lt;멱등 키: 값&gt;의 형태로 멱등 키를 추가하면 된다.
그럼 이 멱등성을 가지고 이중 결제 문제를 어떻게 해결할까 ?</p>
<p><strong>시나리오 1: 고객이 &#39;결제&#39; 버튼을 빠르게 두 번 클릭하는 경우</strong>
아래 그림에서 사용자가 &#39;결제&#39;를 클릭하면 멱등 키가 HTTP 요청의 일부로 결제 시스템에 전송된다. 전자상거래 웹사이트에서 멱등 키는 일반적으로 결제가 이루어지기 직전의 장바구니 ID이다.</p>
<p>결제 시스템은 두 번째 요청을 재시도로 처리하는데, 요청에 포함된 멱등 키를 이전에 받은 적이 있기 때문이다. 그런 경우 결제 시스템은 이전 결제 요청의 가장 최근 상태를 반환한다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/09d933ec-6b5a-4ded-9483-1b015141a8f9/image.png" alt=""></p>
<p>동일한 멱등 키로 동시에 많은 요청을 받으면 결제 서비스는 그 가운데 하나만 처리하고 나머지에 대해서는 429 Too Many Requests 상태 코드를 반환한다.</p>
<p>멱등성을 지원하는 한 가지 방법은 데이터베이스의 고유 키 제약 조건을 활용하는 것이다. 예를 들어, 데이터베이스 테이블의 기본 키를 멱등 키로 사용한다. 그 경우 시스템은 다음과 같이 동작한다.</p>
<ol>
<li>결제 시스템은 결제 요청을 받으면 데이터베이스 테이블에 새 레코드를 넣으려 시도한다.</li>
<li>새 레코드 추가에 성공했다는 것은 이전에 처리한 적이 없는 결제 요청이라는 뜻이다.</li>
<li>새 레코드 추가에 실패했다는 것은 이전에 받은 적이 있는 결제 요청이라는 뜻이다. 그런 중복 요청은 처리하지 않는다.</li>
</ol>
<p><strong>시나리오 2: PSP가 결제를 성공적으로 처리했지만 네트워크 오류로 응답이 결제 시스템에 전달되지 못하여, 사용자가 &#39;결제&#39; 버튼을 다시 클릭하는 경우</strong>
<img src="https://velog.velcdn.com/images/ye_suri_106/post/51caf974-b1fa-4d7a-8c9d-c050e01ac5ad/image.png" alt=""></p>
<p>위의 그림을 다시보자. 2단계 및 3단계에서 결제 서비스는 PSP에 비중복 난수를 전송하고 PSP는 해당 난수에 대응되는 토큰을 반환한다. 이 난수는 결제 주문을 유일하게 식별하는 구실을 하며, 해당 토큰은 그 난수에 일대일로 대응된다. 따라서 토큰 또한 결제 주문을 유일하게 식별 가능하다.</p>
<p>사용자가 &#39;결제&#39; 버튼을 다시 누른다 해도 결제 주문이 같으니 PSP로 전송되는 토큰도 같다. PSP는 이 토큰을 멱등 키로 사용하므로, 이중 결제로 판단하고 종전 실행 결과를 반환한다.</p>
<h3 id="일관성">일관성</h3>
<p>결제 실행 과정에서 상태 정보를 유지 관리하는 여러 서비스가 호출된다.</p>
<ol>
<li>결제 서비스는 비중복 난수, 토큰, 결제 주문, 실행 상태 등의 결제 관련 데이터를 유지 관리한다.</li>
<li>원장은 모든 회계 데이터를 보관한다.</li>
<li>지갑은 판매자의 계정 잔액을 유지한다.</li>
<li>PSP는 결제 실행 상태를 유지한다.</li>
<li>데이터는 안정성을 높이기 위해 여러 데이터베이스 사본에 복제될 수 있다.</li>
</ol>
<p>분산 환경에서는 서비스 간 통신 실패로 데이터 불일치가 발생할 수 있다. 지금부터 결제 시스템에서 발생 가능한 데이터 일관성 문제를 해결하는 기법들을 살펴보자.</p>
<p>내부 서비스 간에 데이터 일관성을 유지하려면 요청이 &#39;정확히 한 번 처리&#39;되도록 보장하는 것이 아주 중요하다.
내부 서비스와 외부 서비스(PSP) 간의 데이터 일관성 유지를 위해서는 일반적으로 멱등성과 조정 프로세스를 활용한다. 외부 서비스가 멱등성을 지원하는 경우, 결제를 재시도할 때는 같은 멱등 키를 사용해야 한다. 그러나 외부 서비스가 멱등 API를 지원하더라도 외부 시스템이 항상 옳다고 가정할 수는 없으므로, 조정 절차를 생략할 수는 없다.</p>
<p>데이터를 다중화하는 경우에는 복제 지연으로 인해 기본 데이터베이스와 사본 데이터가 불일치하는 일이 생길 수 있다. 일반적으로 이 문제에는 두 가지 해결 방법이 있다.</p>
<ol>
<li>주 데이터베이스에만 읽기와 쓰기 연산을 처리한다. 이 접근법은 설정하기는 쉽지만 규모 확장성이 떨어진다는 단점이 있다. 사본은 데이터 안정성 보장에만 활용되고 트래픽은 처리하지 않는다. 따라서 자원이 낭비 된다.</li>
<li>모든 사본이 항상 동기화되도록 한다. 팩서스, 래프트 같은 합의 알고리즘을 사용하거나, 합의 기반 분산 데이터베이스를 사용한다.<h3 id="결제-보안">결제 보안</h3>
사이버 공격과 카드 도난에 대응하기 위한 몇 가지 기술을 간략하게 살펴보자.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/10800778-2981-4b7e-b7a2-5c95865180d9/image.png" alt=""><h2 id="4단계-마무리">4단계 마무리</h2>
이번 장에서는 대금 수신 흐름과 정산 흐름을 살펴보았다. 결제 시스템은 많이 복잡하기 때문에 많은 주제를 다뤘지만 아직 언급하고 넘어갈 주제가 많다. 대표적으로는 아래와 같은 것들이 있으니, 참고하면 좋을 것 같다.</li>
</ol>
<ul>
<li>모니터링: 주요 지표를 모니터링 하는 것은 모든 현대적 애플리케이션에 아주 중요하다. 광범위한 모니터링을 통해 &quot;특정 결제 수단의 평균 승인율은 얼마인가?, 서버의 CPU 사용량은 얼마인가? 같은 질문에 답을 얻을 수 있 다. 지표들을 한데 모아 대시보드를 만들 수도 있다.</li>
<li>정보: 비정상적인 상황이 발생하면 온콜(on-call) 중인 개발자에게 알려 신속하게 내용할 수 있도록 하는 것이 중요하다.</li>
<li>디버깅 도구: &#39;왜 결제가 실패하나요?&#39;는 아주 흔한 질문이다. 엔지니어와 고객 지원팀이 더 쉽게 디버깅할 수 있도록 결제 거래의 상태, 처리 서버 기록, PSP 기록 등을 검토할 수 있는 도구를 개발하는 것이 중요하다.</li>
<li>환율: 국제적인 결제 시스템을 설계할 때 환율은 중요한 고려사항이다.</li>
<li>지역: 지역마다 가용한 결제 수단이 완전히 달라질 수 있다.</li>
<li>현금 결제: 현금 결제는 인도, 브라질 등의 국가에서 매우 일반적이다. 우버와 에어비앤비가 현금 결제 방법에 대해 자세한 엔지니어링 블로그 기사를 펴낸 바 있으니, 참고하기 바란다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[7장 호텔 예약 시스템]]></title>
            <link>https://velog.io/@ye_suri_106/7%EC%9E%A5-%ED%98%B8%ED%85%94-%EC%98%88%EC%95%BD-%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
            <guid>https://velog.io/@ye_suri_106/7%EC%9E%A5-%ED%98%B8%ED%85%94-%EC%98%88%EC%95%BD-%EC%8B%9C%EC%8A%A4%ED%85%9C</guid>
            <pubDate>Wed, 07 Aug 2024 13:08:12 GMT</pubDate>
            <description><![CDATA[<h2 id="1단계-문제-이해-및-설계-범위-확정">1단계 문제 이해 및 설계 범위 확정</h2>
<h3 id="기능-요구사항">기능 요구사항</h3>
<ul>
<li>호텔 정보 페이지 표시</li>
<li>객실 정보 페이지 표시</li>
<li>객실 예약 지원</li>
<li>호텔이나 객실 정보를 추가/삭제/갱신하는 관리자 페이지 지원</li>
<li>초과 예약 지원<ul>
<li>10% 초과 예약 가능, 실제 객실 수보다 더 많은 객실을 판매할 수 있어야 함</li>
</ul>
</li>
<li>객실 가격 유동적<h3 id="비기능-요구사항">비기능 요구사항</h3>
</li>
<li>높은 수준의 동시성 지원 : 성수기, 대규모 이벤트 기간에는 일부 인기 호텔의 특정 객실을 예약하려는 고객이 많이 몰릴 수 있다.</li>
<li>적절한 지연 시간 : 사용자가 예약을 할 때는 응답 시간이 빠르면 이상적이겠으나 예약 요청 처리에 몇 초 정도 걸리는 것은 괜찮다.<h3 id="개략적-규모-추정">개략적 규모 추정</h3>
</li>
<li>총 5,000개 호텔, 100만 개의 객실이 있다고 가정한다.</li>
<li>평균적으로 객실의 70%가 사용 중이고, 평균 투숙 기간은 3일이라고 가정한다.</li>
<li>일일 예상 예약 건수 : 3 ( 초당 예약 트랜잭션 수 - TPS)</li>
</ul>
<p>다음으로 시스템 내 모든 페이지의 QPS(Queries-per-seoned)를 계산해 보자.
일반적으로 고객이 이 웹사이트를 사용하는 흐름에는 세 가지 단계가 있다.</p>
<ol>
<li>호텔/객실 상세 페이지: 사용자가 호텔/객실 정보를 확인한다 (조회 발생)</li>
<li>예약 상세 정보 페이지: 사용자가 날짜, 투숙 인원, 결제 방법 등의 상세 정보를 예약 전에 확인한다 (조회 발생)</li>
<li>객실 예약 페이지: 사용자가 &#39;예약&#39; 버튼을 눌러 객실을 예약한다 (트랜잭션
발생)</li>
</ol>
<p>대략 10%의 사용자가 다음 단계로 진행하고 90%의 사용자는 최종 단계에 도달하기 전에 흐름을 이탈한다고 하자. 아울러 다음 단계에 표시될 내용을 미리 계산해 두는 방안은 고려하지 않는다고 가정할 것이다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/cd3c0a3b-b671-4247-9f67-54fe4bcf735e/image.png" alt="">
최종 예약 TPS(Transaction Per Second)는 3이라는 것을 알고 있으므로, 그 수치에서 역산한 결과다. 예약 페이지의 QPS는 30, 그리고 객실 정보 확인 페이지의 QPS는 300이다.</p>
<h2 id="2단계-개략적-설계안-제시-및-동의-구하기">2단계: 개략적 설계안 제시 및 동의 구하기</h2>
<p>이번 절에서는 다음 사항을 살펴본다.</p>
<ul>
<li>API 설계</li>
<li>데이터 모델</li>
<li>개략적 설계안<h3 id="api-설계">API 설계</h3>
호텔 예약 시스템의 API 설계안을 살펴보자. 가장 중요한 APT를 RESTFul 관례에 따라 나열해 보았다.</li>
</ul>
<blockquote>
<p>호텔 관련 API</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/ec7ff2c3-e45c-41d9-980b-68729028c344/image.png" alt=""></p>
<blockquote>
<p>객실 관련 API</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/cd52029e-983a-4ff6-b135-fb88ad3916d9/image.png" alt=""></p>
<blockquote>
<p>예약 관련 API</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/957caed9-1c67-4f2f-9b2e-fdd7709f50bc/image.png" alt=""></p>
<p>신규 예약 접수는 아주 중요한 기능이다. 새 예약을 만들 때 API(POST /V1/ reservations)에 전달하는 인자의 형태는 다음과 같다.</p>
<pre><code>{
&quot;startDate&quot;: &quot;2021-04-28&quot;,
&quot;endDate&quot;: &quot;2021-04-30&quot;
&quot;hotelID&quot;: &quot;245&quot;,
&quot;roomID&quot;: &quot;U12354673389&quot;,
&quot;reservationID&quot;: &quot;13422445&quot;
}</code></pre><p>researvationID는 이중 예약을 방지하고 동일한 예약은 단 한번만 이루어지도록 보증하는 멱등 키(idempotent key)다. 여기서 이중 예약은 같은 날 같은 객실에 예약이 중복으로 이루어지는 것을 말한다.</p>
<h3 id="데이터-모델">데이터 모델</h3>
<p>어떤 데이터베이스를 사용할지 결정하기 전에 데이터 접근 패턴부터 자세히 살펴보자. 호텔 예약 시스템은 다음 질의를 지원해야 한다.</p>
<ul>
<li>질의 1: 호텔 상세 정보 확인</li>
<li>질의 2: 지정된 날짜 범위에 사용 가능한 객실 유형 확인</li>
<li>질의 3: 예약 정보 기록</li>
<li>질의 4: 예약 내역 또는 과거 예약 이력 정보 조회</li>
</ul>
<p>대략적인 추정 과정을 통해 시스템 규모가 크지 않은 것은 알았으나 대규모 이벤트가 있는 경우에는 트래픽이 급증할 수도 있으니 대비해야 한다. 이런 요구 사항을 종합적으로 고려하였을 때 본 설계안에서는 관계형 데이터베이스를 선택할 것이다.</p>
<ul>
<li>관계형 데이터베이스는 읽기 빈도가 쓰기 연산에 비해 높은 작업 흐름을 잘 지원한다. 호텔 웹사이트/앱을 방문하는 사용자의 수는 실제로 객실을 예약 하는 사용자에 비해 압도적으로 많다. NosQL 데이터베이스는 대체로 쓰기 연산에 최적화되어 있다. 관계형 데이터베이스는 읽기가 압도적인 작업 흐름은 충분히 잘 지원한다.</li>
<li>관계형 데이터베이스는 ACID 속성(원자성, 일관성, 격리성, 영속성)을 보장한다. ACID 속성은 예약 시스템을 만드는 경우 중요하다. 이 속성이 만족되지 않으면 잔액이 마이너스가 되는 문제, 이중 청구 문제, 이중 예약 문제 등을 방지하기 어렵다. ACID 속성이 충족되는 데이터베이스를 사용하면 애플리케이션 코드는 훨씬 단순해지고 이해하기 쉬워진다. 관계형 데이터베이스는 일반적으로 ACID 속성을 보장한다.</li>
<li>관계형 데이터베이스를 사용하면 데이터를 쉽게 모델링할 수 있다. 비즈니스 데이터의 구조를 명확하게 표현할 수 있을 뿐 아니라 엔티티(호텔, 객실, 객실 유형 등) 간의 관계를 안정적으로 지원할 수 있다.</li>
</ul>
<p>아래는 스키마 설계이다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/a7783abe-d153-4c72-ac4d-34d6ba64d5e2/image.png" alt="">
reservation 테이블의 status 필드는 pending, paid, refunded, canceled, rejected 즉 결제 대기, 결제 완료, 환불 완료, 취소, 승인 실패의 다섯 상태 가운데 하나를 값으로 가질 수 있다. 
<img src="https://velog.velcdn.com/images/ye_suri_106/post/29c0a0cd-2e0e-4cd8-b81a-5bafd608f76e/image.png" alt="">
하지만 이 스키마 디자인에는 큰 문제가 있다. room_id가 있기 때문에 에어비앤비 같은 회사에는 적합하지만 호텔은 그렇지 않다.</p>
<p>사용자는 특정 객실을 예약하는 것이 아니라 특정 호텔의 특정 객실 유형을 예약하기 때문이다.
여기서 객실 유형은 스탠다드 룸, 킹 사이즈 룸, 퀸 사이즈 룸 등이 될 수 있다.</p>
<p>객실 번호는 예약할 때가 아닌, 투숙객이 체크인 하는 시점에 부여된다. 이 요구사항을 반영하려면 데이터 모델을 손볼 필요가 있다. </p>
<h3 id="개략적-설계안">개략적 설계안</h3>
<p>이 호텔 예약 시스템에는 마이크로서비스(microservice) 아키텍처를 사용한다.
지난 몇 년 동안 이 아키텍처는 많은 인기를 끌었다. 마이크로서비스 아키텍처를 채택한 회사로는 아마존, 넷플릭스, 우버, 에어비앤비, X(구 트위터) 등이 있다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/faf31974-5e8a-4029-a980-0ab83f008703/image.png" alt="">
위 설계안의 각 컴포넌트는 아래와 같다.</p>
<ul>
<li>사용자: 휴대폰이나 컴퓨터로 객실을 예약하는 당사자다.</li>
<li>관리자(호텔 직원): 고객 환불, 예약 취소, 객실 정보 갱신 둥의 관리 작업을 수행할 권한이 있는 호텔 직원이다.</li>
<li>CDN(콘텐츠 전송 네트워크): 자바스크립트 코드 번들, 이미지, 동영상, HTML 등 모든 정적 콘텐츠를 캐시하여 웹사이트 로드 성능을 개선하는 데 이용된다.</li>
<li>공개 API 게이트웨이: 처리율 제한(rate limiting), 인증 등의 기능을 지원하는 완전 관리형 서비스(fully managed service)다. 엔드포인트 기반으로 특정 서비스에 요청을 전달할 수 있도록 구성된다. 예를 들어 호텔 홈페이지 요청은 호텔 서비스로, 호텔 객실 예약 요청은 예약 서비스로 전달하는 역할을 담당한다.</li>
<li>내부 API: 승인된 호텔 직원만 사용 가능한 API로, 내부 소프트웨어나 웹사이트를 통해서 사용 가능하다. VPN(Virtual Private Network, 즉 가상 사설 망) 등의 기술을 사용해 외부 공격으로부터 보호한다.</li>
<li>호텔 서비스: 호텔과 객실에 대한 상세 정보를 제공한다. 호텔과 객실 데이터는 일반적으로 정적이라서 쉽게 캐시해 둘 수 있다.</li>
<li>요금 서비스: 미래의 어떤 날에 어떤 요금을 받아야 하는지 데이터를 제공하는 서비스다. 재미있는 것은 객실의 요금은 해당 날짜에 호텔에 얼마나 많은 손님이 몰리느냐에 따라 달라진다는 것이다.</li>
<li>예약 서비스: 예약 요청을 받고 객실을 예약하는 과정을 처리한다. 객실이 예약되거나 취소될 때 잔여 객실 정보를 갱신하는 역할도 담당한다.</li>
<li>결제 서비스: 고객의 결제를 맡아 처리하고, 절차가 성공적으로 마무리되면 예약 상태를 결제 완료로 갱신하며 실패한 경우에는 승인 실패로 업데이트 한다.</li>
<li>호텔 관리 서비스: 승인된 호텔 직원만 사용 가능한 서비스다. 임박한 예약 기록 확인, 고객 객실 예약, 예약 취소 등의 기능을 제공한다.</li>
</ul>
<p>아래는 각 서비스 간의 연결을 간략하게 나타낸 도식이다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/536d9491-e096-42ee-94a2-229ab71e5f97/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[9장 S3와 유사한 객체 저장소]]></title>
            <link>https://velog.io/@ye_suri_106/9%EC%9E%A5-S3%EC%99%80-%EC%9C%A0%EC%82%AC%ED%95%9C-%EA%B0%9D%EC%B2%B4-%EC%A0%80%EC%9E%A5%EC%86%8C</link>
            <guid>https://velog.io/@ye_suri_106/9%EC%9E%A5-S3%EC%99%80-%EC%9C%A0%EC%82%AC%ED%95%9C-%EA%B0%9D%EC%B2%B4-%EC%A0%80%EC%9E%A5%EC%86%8C</guid>
            <pubDate>Wed, 07 Aug 2024 13:07:56 GMT</pubDate>
            <description><![CDATA[<p>이번 장에서는 아마존 S3와 유사한 객체 저장소 서비스를 설계해본다. S3는 AWS가 제공하는 서비스로 RESTful API 기반 인터페이스로 이용 가능한 객체 저장소다.</p>
<p>객체 저장소에 대해 더 자세히 살펴보기 전에, 일반적으로 저장소란 어떤 시스템인지 알아보고, 몇 가지 용어를 정의하도록 하겠다.</p>
<h3 id="저장소-시스템-101">저장소 시스템 101</h3>
<p>저장소 시스템에는 다음 세 가지 부류가 있다.</p>
<ul>
<li>블록 저장소</li>
<li>파일 저장소</li>
<li>객체 저장소<blockquote>
<p>블록 저장소</p>
</blockquote>
</li>
</ul>
<p>HDD(Hard Disk Drive)나 SSD(Solid State Drive)처럼 서버에 물리적으로 연결되는 형태의 드라이브는 블록 저장소의 가장 흔한 형태다.
블록 저장소는 원시 블록(raw block)을 서버에 볼륨(volume) 형태로 제공한다. 가장 유연하고 융통성이 높은 저장소다. 서버는 원시 블록을 포맷한 다음 파일 시스템으로 이용하거나 애플리케이션에 블록 제어권을 넘겨버릴 수도 있다. 데이터베이스나 가상 머신 엔진 같은 애플리케이션은 원시 블록을 직접 제어하여 최대한의 성능을 끌어낸다.</p>
<p>블록 저장소는 서버에 물리적으로 직접 연결되는 저장소에 국한되지 않는다. 고속 네트워크를 통해 연결될 수도 있고, 업계 표준 연결 프로토콜인 FC (Fibre Channelyl이나 iSCsT를 통해 연결될 수도 있다.</p>
<blockquote>
<p>파일 저장소</p>
</blockquote>
<p>파일 저장소는 블록 저장소 위에 구현된다. 파일과 디렉터리를 손쉽게 다루는 데 필요한, 더 높은 수준의 추상화(abstraction)를 제공한다. 데이터는 계층적으로 구성되는 디렉터리 안에 보관된다. 파일 저장소는 가장 널리 사용되는 범용 저장소 솔루션이다. SMB/CIFS이나 NFS와 같은 파일 수준 네트워크 프로토콜을 사용하면 하나의 저장소를 여러 서버에 동시에 붙일 수도 있다.
파일 저장소를 사용하는 서버는 블록을 직접 제어하고, 볼륨을 포맷하는 등의 까다로운 작업을 신경 쓸 필요가 없다. 파일 저장소는 단순하기 때문에 폴더나 파일을 같은 조직 구성원에 공유하는 솔루션으로 사용하기 좋다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[8장 분산 이메일 서비스]]></title>
            <link>https://velog.io/@ye_suri_106/8%EC%9E%A5-%EB%B6%84%EC%82%B0-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%84%9C%EB%B9%84%EC%8A%A4</link>
            <guid>https://velog.io/@ye_suri_106/8%EC%9E%A5-%EB%B6%84%EC%82%B0-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%84%9C%EB%B9%84%EC%8A%A4</guid>
            <pubDate>Wed, 07 Aug 2024 12:20:19 GMT</pubDate>
            <description><![CDATA[<p>이번 장에서는 지메일 또는 야후 메일과 같은 대규모 이메일 서비스를 설계해본다. </p>
<h2 id="1단계-문제-이해-및-설계-범위-확정">1단계 문제 이해 및 설계 범위 확정</h2>
<h3 id="기능-요구-사항">기능 요구 사항</h3>
<ul>
<li>이메일 발송/수신</li>
<li>모든 이메일 가져오기</li>
<li>읽음 여부에 따른 이메일 필터링</li>
<li>제목, 발신인, 메일 내용에 따른 검색 기능</li>
<li>스팸 및 바이러스 방지 기능</li>
<li>HTTP로 메일 서버와 통신</li>
<li>첨부 파일도 지원</li>
</ul>
<h3 id="비기능-요구사항">비기능 요구사항</h3>
<ul>
<li>안정성 : 이메일 데이터는 소실되어서는 안된다.</li>
<li>가용성 : 이메일과 사용자 데이터를 여러 노드에 자동으로 복제하여 가용성을 보장해야 한다. 아울러 부분적으로 장애가 발생해도 시스템은 계속 동작해야 한다.</li>
<li>확장성 : 사용자 수가 늘어나도 감당할 수 있어야 한다. 사용자나 이메일이 많아져도 시스템 성능은 저하되지 않아야 한다.</li>
<li>유연성과 확장성 : 새 컴포넌트를 더하여 쉽게 기능을 추가하고 성능을 개선할 수 있는 유연하고 확장성 높은 시스템이어야 한다. POP나 IMAP 같은 기존 이메일 프로토콜은 기능이 매우 제한적이다. 따라서 유연성과 확장성을 갖추려면 맞춤형 프로토콜이 필요할 수도 있다.<h3 id="개략적인-규모-추정">개략적인 규모 추정</h3>
</li>
<li>10억 명의 사용자</li>
<li>한 사람이 하루에 보내는 평균 이메일 수는 10건이라고 가정한다. 따라서 이메일 전송 QPS=100,000dlek.</li>
<li>한 사람이 하루에 수신하는 이메일 수는 평균 40건이라고 가정하고, 이메일 하나의 메타 데이터는 평균 50KB로 가정한다. 메타데이터는 주어진 이메일에 대한 모든 정보이며, 첨부 파일은 포함하지 않는다.</li>
<li>메타데이터는 데이터베이스에 저장한다고 가정. 1년간 메타 데이터를 유지하기 위한 스토리지 요구사항은 10억명 사용자 X 하루 40건의 이메일 X 365일 X 20% X 500KB = 1,4560PB에 달한다.</li>
</ul>
<p>결론적으로, 상당한 양의 데이터를 처리해야 하기 때문에 <strong><em>분산 데이터 베이스 솔루션</em></strong> 이 필요하다 !</p>
<h2 id="2단계-개략적-설계안-제시-및-동의-구하기">2단계 개략적 설계안 제시 및 동의 구하기</h2>
<h3 id="이메일-101">이메일 101</h3>
<p>이메일을 주고 받는 프로토콜은 여러 가지가 있는데, 대부분의 메일 서버는 POP, IMAP, SMTP 같은 프로토콜을 사용해왔다.</p>
<ul>
<li>SMTP<ul>
<li>이메일을 한 서버에서 다른 서버로 보내는 표준 프로토콜이다.</li>
</ul>
</li>
<li>POP<ul>
<li>이메일 클라이언트가 원격 메일 서버에서 이메일을 수신하고 다운로드하기 위해 사용하는 표준 프로토콜이다.</li>
<li>단말로 다운로드된 이메일은 서버에서 삭제되기 때문에 결과적으로 한 대 단말에서만 이메일을 읽을 수 있다.</li>
<li>해당 프로토콜을 사용하는 클라이언트는 이메일을 일부만 읽을 수 없기 때문에, 용량이 큰 첨부 파일이 붙은 이메일을 읽을려면 시간이 오래 걸린다.</li>
</ul>
</li>
<li>IMAP<ul>
<li>이메일 클라이언트가 원격 메일 서버에서 이메일을 수신하는 데 사용되는 또 다른 표준 프로토콜이다.</li>
<li>POP와는 달리 클릭하지 않으면 메세지는 다운로드 되지 않으며, 메일 서버에서 지워지지도 않는다. 따라서 여러 단말에서 이메일을 읽을 수 있다.</li>
<li>개인 이메일 계정에서 가장 널리 이용되는 프로토콜이다.</li>
</ul>
</li>
<li>HTTPS<ul>
<li>이 프로토콜은 기술적으로 보자면 메일 전송 프로토콜이 아니다. 하지만 웹 기반 이메일 프로젝트의 메일함 접속에 이용될 수 있다.<h3 id="도메인-이름-서비스-dns">도메인 이름 서비스 (DNS)</h3>
DNS 서버는 수신자 도메인의 메일 교환기 레코드 (MX) 검색에 이용된다. 가령 커맨드-라인에서 gmail.com의 DNS 레코드를 검색해 보면 다음과 같은 MX 레코드가 표시된다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/20892d2f-a067-4a13-a308-14cf8ea3929f/image.png" alt=""></li>
</ul>
</li>
</ul>
<p>우선순위 값은 선호도를 나타내는 것으로, 그 값이 낮을수록 우선순위가 높아서 선호하는 것으로 이해하면 된다. 따라서 위의 사진을 기준으로는, gmail-smtp-in.l.google.com이 우선순위가 가장 높기 때문에 송신자 측 메일 서버는 이 메일 서버에 접속해서 메세지를 보내려고 시도한다. 연결 실패 시, 그 다음으로 우선순위가 높은 메일 서버와 연결을 시도한다.</p>
<h3 id="첨부파일">첨부파일</h3>
<p>이메일 첨부 파일은 이메일 메세지와 함께 전송되며, 일반적으로 Base64 인코딩을 사용한다.</p>
<h3 id="전통-메일-서버">전통 메일 서버</h3>
<p>분산 메일 서버에 대해 알아보기 전에 기존 메일 서버(보통 서버 한대로 운용)의 역사와 동작 방식을 간단히 살펴보자.</p>
<blockquote>
<p>전통적 메일 서버 아키텍쳐</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/35bd656f-f6f5-4b94-9dac-750812427349/image.png" alt=""></p>
<p>해당 프로세스는 아래 단계로 구성된다.</p>
<ol>
<li>앨리스는 아웃룩 클라이언트에 로그인하여 이메일을 작성하고 &#39;보내기&#39; 버튼을 누른다. 이메일은 아웃룩 메일 서버로 전성되고, 아웃룩 클라이언트와 메일 서버 사이의 통신 프로토콜은 SMTP이다.</li>
<li>아웃룻 메일 서버는 DNS 질의를 통해 수신사 SMTP 서버 주소를 찾고 해당 메일 서버로 이메일을 보낸다. 메일 서버 간 통신 프로토콜도 SMTP이다.</li>
<li>지메일 서버는 이메일을 저장하고 수신자인 밥이 읽어갈 수 있도록 한다.</li>
<li>밥이 지메일에 로그인하면 지메일 클라이언트는 IMAP/POP 서버를 통해 새 이메일을 가져온다.</li>
</ol>
<blockquote>
<p>저장소</p>
</blockquote>
<p>전통적 메일 서버는 이메일을 파일 시스템의 디렉터리에 저장했다. 이때 각각의 이메일은 고유한 이름을 가진 별도 파일로 보관한다. 각 사용자의 설정 데이터와 메일함은 사용자 디렉터리에 보관한다.</p>
<p>하지만 이런 구조는 사용자가 많아짐에 따라 수십억 개의 이메일을 검색하고 백업하는 목적으로 활용하기에는 곤란했다. 이메일 양이 많아지고 파일 구조가 복잡해지면 디스크 I/O가 병목이 되곤 했다.
또한 이메일을 서버의 파일 시스템에 저장했으므로 가용성과 안정성 요구 사항도 만족할 수 없었다.
디스크 손상이나 서버 장애가 언제든 발생할 수 있었기 때문에, 더 안정적인 분산 데이터 저장소 계층이 필요했다.</p>
<h3 id="분산-메일-서버">분산 메일 서버</h3>
<p>이렇게 등장한 분산 메일 서버는 현대적 사용 패턴을 지원하고 확장성과 안정성 문제를 해결한다. 이번 절에서는 이메일 API, 분산 이메일 서버 아키텍처, 이메일 발송 및 수신 흐름을 살펴 본다.</p>
<h3 id="이메일-api">이메일 API</h3>
<p>이메일 API의 의미는 메일 클라이언트마다, 그리고 이메일 생명주기 단계마다 달라질 수 있다.</p>
<ul>
<li>모바일 단말 클라이언트를 위한 SMTP/POP/IMAP API</li>
<li>송신 측 메일 서버와 수신 측 메일 서버 간의 SMTP 통식</li>
<li>대화형 웹 기반 이메일 애플리케이션을 위한 HTTP 기반 RESTful API</li>
</ul>
<blockquote>
<p>POST /v1/messages 엔드포인트</p>
</blockquote>
<p>To, Tc, Bcc 헤더에 명시된 수신자에게 메세지를 전송한다.</p>
<blockquote>
<p>GET /v1/folder 엔드포인트</p>
</blockquote>
<p>주어진 이메일 계정에 존재하는 모든 폴더를 반환한다.</p>
<p>응답 형식은 아래와 같다.</p>
<pre><code>[{
  id: string        고유한 폴더 식별자
  name: string      폴더 이름
                    기본 폴더는 다음 폴더 가운데 하나다.
                    All, Archive, Drafts, Flagged, Junk, Sent, Trash
  user_id: string   계정 소유자 ID
}]</code></pre><blockquote>
<p>GET /v1/folders/{:folder_id}/messages 엔드포인트</p>
</blockquote>
<p>주어진 폴더 아래에 있는 메세지들을 전부 반환한다. </p>
<blockquote>
<p>GET /v1/messages/{:message_id}</p>
</blockquote>
<p>주어진 특정 메세지에 대한 모든 정보를 반환한다. 메세지는 이메일 애플리케이션의 핵심 구성 요소로, 발신자, 수신자, 메세지 제목, 본문, 첨부 파일 등의 정보로 구성된다.</p>
<pre><code>{
  user_id: string                         계정주의 ID
  from: [name: string, email: string]     발신자의 &lt;이름, 이메일&gt; 쌍
  to : [name: string, email: string]      수신자 &lt;이름, 이메일&gt; 쌍의 목록
  subject: string                         이메일 제목
  body: string                            이메일 본문
  is_read: Boolean                        수신자가 메세지를 읽었는지 여부
}</code></pre><h3 id="분산-메일-서버-아키텍처">분산 메일 서버 아키텍처</h3>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/5022db2a-ecf1-46e0-a62e-4cedd133e82f/image.png" alt=""></p>
<ul>
<li>웹메일 : 사용자는 웹브라우저를 사용해 메일을 받고 보낸다.</li>
<li>웹서버 : 웹서버는 사용자가 이용하는 요청/응답 서비스로, 로그인, 가입, 사용자 프로파일 등에 대한 관리 기능을 담당한다. 본 설계안의 경우 이메일 발송, 폴더 목록 확인, 폴더 내 모든 메세지 확인 등의 모든 이메일 API 요청은 전부 웹서버를 통한다.</li>
<li>실시간 서버 : 실시간 서버는 새로운 이메일 내역을 클라이언트에 실시간으로 전달하는 역할을 한다. 실시간 서버는 지속성 연결을 맺고 유지해야 하므로 상태 유지 서버다. 실시간 통신 지원 방안으로는 롱 풀링, 웹소켓 등이 있다.</li>
<li>메타데이터 데이터베이스 : 이메일 제목, 본문, 발신인, 수신인 목록 등의 메타 데이터를 저장하는 데이터베이스이다.</li>
<li>첨부 파일 저장소 : 아마존 S3 같은 객체 저장소를 사용할 것이다. 카산드라 같은 컬럼 기반 NoSQL 데이터베이스는 이 용도로는 적당하지 않을 것 같다.<ul>
<li>카산드라가 BLOB 자료형을 지원하고 해당 자료형이 지원하는 데이터의 최대 크기가 2GB이긴 하지만 실질적으로는 1MB 이상의 파일을 지원 못함</li>
<li>카산드라에 첨부 파일을 저장하면 레코드 캐시를 사용하기 어렵다. 첨부 파일이 너무 많은 메모리를 잡아먹을 것이다.</li>
</ul>
</li>
<li>분산 캐시 : 최근에 수신된 이메일은 자주 읽을 가능성이 높으므로 클라이언트로 하여금 메모리에 캐시해 두도록 하면 메일을 표시하는 시간을 많이 줄일 수 있다. 리스트 같은 다양한 기능을 제공하는 데다 규모 확장도 용이하므로 본 설계안에서는 레디스를 활용한다.</li>
<li>검색 저장소 : 검색 저장소는 분산 문서 저장소다. 고속 텍스트 검색을 지원하는 역 인덱스를 자료 구조로 사용한다. </li>
</ul>
<p>분산 메일 서버에서 가장 중요한 작업 흐름은 아래와 같다.</p>
<h3 id="1-이메일-전송-절차">1) 이메일 전송 절차</h3>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/48ab47bf-311b-4dc1-af54-2fdff797751f/image.png" alt=""></p>
<ol>
<li>사용자가 웹메일 환경에서 메일을 작성한 다음 전송 버튼을 누른다. 요청은 로드밸런서로 전송된다.</li>
<li>로드밸런서는 처리율 제한 한도를 넘지 않는 선에서 요청을 웹서버로 전달한다.</li>
<li>웹 서버는 다음 역할을 담당한다.<ul>
<li>기본적인 이메일 검증 : 이메일 크기 한도처럼 사전에 미리 정의된 규칙을 사용하여 수신된 이메일을 검사한다.</li>
<li>수신자 이메일 주소 도메인이 송신자 이메일 주소 도메인과 같은지 검사 : 같다면 웹 서버는 이메일 내용의 스팸 여부와 바이러스 감염 여부를 검사한다. 검사를 문제없이 통과한 이메일은 송신인의 &#39;보낸 편지함&#39;과 수신인의 &#39;받은 편지함&#39;에 저장된다. 수신인 측 클라이언트는 RESTful API 를 사용하여 이메일을 바로 가져올 수 있으며, 4단계 이후 수행할 필요는 없다.</li>
</ul>
</li>
<li>메세지 큐
a. 기본적인 검증을 통과한 이메일은 외부 전송 큐로 전달된다. 큐에 넣기에 첨부 파일이 너무 큰 이메일은 객체 저장소에 첨부파일을 따로 저장하고 큐에 전달하는 이메일 안에는 해당 저장 위치에 대한 참조 정보만 보관한다.
b. 기본적인 검증에 실패한 이메일은 에러 큐에 보관한다.</li>
<li>외부 전송 담당 SMTP 작업 프로세스는 외부 전송 큐에서 메세지를 꺼내어 이메일의 스팸 및 바이러스 감염 여부를 확인한다.</li>
<li>검증 절차를 통과한 이메일은 저장소 계층 내의 &#39;보낸 편지함&#39;에 저장된다.</li>
<li>외부 전송 담당 SMTP 작업 프로세스가 수신자의 메일 서버로 메일을 전송한다.</li>
</ol>
<p>분산 메세지 큐는 비동기적 메일 처리를 가능하게 하는 핵심 컴포넌트이고 웹서버에서 외부 전송 담당 SMTP 프로세스를 분리함으로써 전송용 SMTP 프로세스의 규모를 독립적으로 조정할 수 있게 한다.</p>
<p>외부 전송 큐의 크기를 모니터링할 때는 각별히 주의해야 하는데, 만일 메일이 처리되지 않고 큐에 오랫동안 머물러있다면 다음과 같은 문제가 있을 수 있다.</p>
<ul>
<li>수신자 측 메일 서버에 장애 발생: 나중에 메일을 다시 전송해야 한다. 지수적 백오프가 좋은 전략일 수 있다.</li>
<li>이메일을 보낼 큐의 소비자 수가 불충분 : 더 많은 소비자를 추가하여 처리 시간을 단축하는 방법을 생각해볼 수 있다.<h3 id="이메일-수신-절차">이메일 수신 절차</h3>
<img src="https://velog.velcdn.com/images/ye_suri_106/post/43898bc1-fa99-4529-ad47-7adab33303a5/image.png" alt=""></li>
</ul>
<ol>
<li>이메일이 SMTP 로드밸런서에 도착한다.</li>
<li>로드밸런서는 트래픽을 여러 SMTP 서버로 분산한다. SMTP 연결에는 이메일 수락 정책을 구성하여 적용할 수 있다. 예를 들어 유효하지 않은 이메일은 반송하도록 하면 불필요한 이메일 처리를 피할 수 있다.</li>
<li>이메일의 첨부 파일이 큐에 들어가기 너무 큰 경우에는 첨부 파일 저장소(S3)에 보관한다.</li>
<li>이메일을 수신 이메일 큐에 넣는다. 이 큐는 메일 처리 작업 프로세스와 SMTP 서버 간의 결합도를 낮추어 각자 독립적으로 규모 확장이 가능하도록 한다. 갑자기 수신되는 이메일의 양이 폭증하는 경우 버퍼 역할도 한다.</li>
<li>메일 처리 작업 프로세스(worker)는 스팸 메일을 걸러내고 바이러스를 차단하는 등의 다양한 역할을 한다. 아래의 절차들은 검증 작업이 끝난 이메일을 대상으로 한다.</li>
<li>이메일을 메일 저장소, 캐시, 객체 저장소 등에 보관한다.</li>
<li>수신자가 온라인 상태인 경우 이메일을 실시간 서버로 전달한다.</li>
<li>실시간 서버는 수신자 클라이언트가 새 이메일을 실시간으로 받을 수 있도록 하는 웹소켓 서버다.</li>
<li>오프라인 상태 사용자의 이메일은 저장소 계층에 보관한다. 해당 사용자가 온라인 상태가 되면 웹메일 클라이언트는 웹 서버에 RESTful AP를 통해 연결한다.</li>
<li>웹 서버는 새로운 이메일을 저장소 계층에서 가져와 클라이언트에 반환한다.</li>
</ol>
<h2 id="3단계-상세-설계">3단계 상세 설계</h2>
<p>몇 가지 핵심 요소를 더 자세히 알아보자.</p>
<h3 id="메타데이터-데이터베이스">메타데이터 데이터베이스</h3>
<p>이메일 메타데이터의 특성을 알아보고 올바른 데이터베이스와 데이터 모델을 고르는 문제, 그리고 이메일 타래 지원 방안에 대해 알아보자.</p>
<ul>
<li>이메일 헤더는 일반적으로 작고, 빈번하게 이용된다.</li>
<li>이메일 본문의 크기는 작은 것부터 큰 것까지 다양하지만 사용 빈도는 낮다. 일반적으로 사용자는 이메일을 한 번만 읽는다.</li>
<li>이메일 가져오기, 읽은 메일로 표시, 검색 등의 이메일 관련 작업은 사용자별로 격리 수행되어야 한다. 즉, 어떤 사용자의 이메일은 해당 사용자만 읽을 수 있어야 하고, 그 이메일에 대한 작업도 그 사용자만이 수행할 수 있어야 한다.</li>
<li>데이터의 신선도는 데이터 사용 패턴에 영향을 미친다. 사용자는 보통 최근 메일만 읽는다. 만들어진 지 16일 이하 데이터에 발생하는 읽기 질의 비율은 전체 질의의 82%에 달한다.</li>
<li>데이터의 높은 안정성이 보장되어야 한다. 데이터 손실은 용납되지 않는다.<h3 id="올바른-데이터베이스의-선정">올바른 데이터베이스의 선정</h3>
가능한 모든 선택지는 아래와 같다.</li>
<li>관계형 데이터베이스<ul>
<li>관계형 데이터베이스를 고르는 주된 동기는 이메일을 효율적으로 검색이 가능하기 때문이다. </li>
<li>이메일 헤더와 본문에 대한 인덱스를 만들어두면 간단한 검색 질의 빠르게 처리 가능하다.   - 하지만 관계형 데이터베이스는 데이터 크기가 작을 때 적합하기 때문에 바람직하지 않다.</li>
</ul>
</li>
<li>분산 객체 저장소<ul>
<li>이메일의 원시 데이터를 그대로 아마존 S3 같은 객체 저장소에 보관하는 것이다. 객체 저장소는 백업 데이터를 보관하기에는 좋지만 이메일의 읽음 표시, 키워드 검색, 이메일 타래 등의 기능을 구현하기에는 그다지 좋지 않다.</li>
</ul>
</li>
<li>NoSQL 데이터베이스<ul>
<li>지메일은 구글 빅테이블을 저장소로 사용한다. 따라서 충분히 실현 가능한 방안이다.</li>
<li>하지만 빅테이블은 오픈소스로 공개되어 있지 않기 때문에 어떻게 구현이 내부적으로 되어있는지는 모른다.</li>
<li>카산드라가 좋은 대안이 될 수도 있지만 대형 이메일 서비스 제공 업체 가운데 카산드라를 쓰는 곳은 없다.</li>
</ul>
</li>
</ul>
<p>그냥 결론적으로는 본 설계안이 필요로 하는 기능을 완벽히 지원하는 데이터베이스는 없다. 대형 이메일 서비스 업체는 보통 자체적으로 데이터베이스 시스템을 만들어서 사용한다.</p>
<h3 id="데이터-모델">데이터 모델</h3>
<p>데이터를 저장하는 한가지 방법은 user_id를 파티션 키로 사용하여 특정한 사용자의 데이터는 항상 같은 샤드에 보관하는 것이다. 이 데이터 모델의 한 가지 문제는 메세지를 여러 사용자와 공유할 수 없다는 것이다. 하지만 이는 요구사항과 관계 없으므로 신경 쓰지 않아도 된다.</p>
<p>그럼 이제 테이블을 정의해보자. 기본 키는 파티션 키와 클러스터 키의 두 가지 부분으로 구성된다.</p>
<ul>
<li>파티션 키 : 데이터를 여러 노드에 분산하는 구실을 한다. 일반적으로 통용 되는 규칙은 데이터가 모든 노드에 균등하게 분산되도록 하는 파티션 키를 골라야 한다는 것이다.</li>
<li>클러스터 키 : 같은 파티션에 속한 데이터를 정렬하는 구실을 한다.</li>
</ul>
<p>이메일 서비스의 데이터 계층은 다음과 같은 질의를 지원해야 한다.</p>
<ul>
<li>주어진 사용자의 모든 폴더를 구한다.</li>
<li>특정 폴더 내의 모든 이메일을 표시한다.</li>
<li>메일을 새로 만들거나, 삭제하거나, 가져온다.</li>
<li>이미 읽은 메일 전부, 또는 아직 읽지 않은 메일 전부를 가져온다.</li>
<li>보너스 점수를 받을 수 있는 질의 : 이메일 타래를 전부 가져온다.</li>
</ul>
<blockquote>
<p>질의 1: 특정 사용자의 모든 폴더 질의</p>
</blockquote>
<p>아래에서 볼 수 있듯, 파티션 키는 user_id다. 따라서 어떤 사용자의 모든 폴더는 같은 파티션 안에 있다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/85d7413e-972d-4f58-9788-b3434198943f/image.png" alt=""></p>
<blockquote>
<p>질의 2: 특정 폴더에 속한 모든 이메일 표시</p>
</blockquote>
<p>사용자가 자기 메일 폴더를 열면 이메일은 가장 최근 이메일부터 오래된 것 순서로 정렬되어 표시된다. 같은 폴더에 속한 모든 이메일이 같은 파티션에 속하도록 하려면 &lt;user_id, folder_id&gt; 형태의 복합 파티션 키를 사용해야 한다.</p>
<p>email_id의 xkdlqdms TIMEUUID로 이메일을 시간순으로 정렬하는데 사용하는 클러스터 키이다.</p>
<blockquote>
<p>질의 3: 이메일 생성/삭제/수신</p>
</blockquote>
<p>이 질의를 지원하기 위해서는 두 테이블이 필요하다. 다음과 같은 간단한 질의를 통해 특정 이메일의 상세 정보를 가져올 수 있다.</p>
<p>SELECT * FROM emails_by_user WHERE email_id = 123;</p>
<p>한 이메일에는 여러 첨부 파일이 있을 수 있다. email_id와 filename 필드를 같이 사용하면 모든 첨부 파일을 질의할 수 있다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/fe8ebbe1-2838-4bc3-bc5a-9095ee5efd81/image.png" alt=""></p>
<blockquote>
<p>질의 4: 읽은, 또는 읽지 않은 모든 메일</p>
</blockquote>
<p>관계형 데이터베이스로 도메인 모델을 구현하는 경우, 읽은 메일 전부는 다음과 같이 질의 가능하다.</p>
<pre><code>SELECT * FROM emails_by_folder
WHERE user_id = &lt;user_id&gt; and folder_id = &lt;folder_id&gt; and is_read = true
ORDER BY email_id</code></pre><p>읽지 않은 메일을 전부 가져오는 것도 비슷하다.
하지만 본 설계안의 데이터 모델은 NoSQL이다. NoSQL 데이터베이스는 보통 파티션 키와 클러스터 키에 대한 질의만 허용한다. emails_by_folder 테이블의 is_read 필드는 이에 해당하지 않으므로, 대부분의 NoSQL 데이터베이스는 위의 질의문을 실행하지 못한다.</p>
<p>이 문제를 해결하는 방법은 주어진 폴더에 속한 모든 메세지를 가져온 다음 애플리케이션 단에서 필터링을 수행하는 것이다. 하지만 대규모 서비스에서는 안티 패턴이다.</p>
<p>따라서 이 문제는 NoSQL 데이터베이스 테이블을 비정규화하여 해결하는 것이 보통이다. 즉, emails_by_folder 테이블을 아래처럼 두 테이블로 분할하는 것이다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/141333a7-9a24-4ac7-8952-ae1f38666b19/image.png" alt=""></p>
<ul>
<li>read_emails : 읽은 상태의 모든 이메일을 보관하는 테이블</li>
<li>unread_emails : 읽지 않은 모든 이메일을 보관하는 테이블</li>
</ul>
<p>읽지 않은 메일을 읽은 메일로 변경하려면 해당 이메일을 unread_emails 테이블에서 삭제한 다음, read_emails 테이블로 옮기면 된다.</p>
<p>또한 특정 폴더 안에 읽지 않은 모든 메일을 가져오는 질의는 아래와 같이 작성하면 된다.</p>
<pre><code>SELECT * FROM unread_emails
WHERE user_id = &lt;user_id&gt; and folder_id = &lt;folder_id&gt;
ORDER BY email_id</code></pre><h3 id="일관성-문제">일관성 문제</h3>
<p>높은 가용성을 달성하기 위해 다중화에 의존하는 분산 데이터베이스는 데이터 일관성과 가용성 사이에서 타협적인 결정을 내릴 수 밖에 없다.
이메일 시스템의 경우에는 데이터의 정확성이 아주 중요하므로, 모든 메일함은 반드시 하나의 주 사본을 통해 서비스된다고 가정해야 한다.
따라서 장애가 발생하면 클라이언트는 다른 사본을 통해 주 사본이 복원될 때까지 동기화/갱신 작업을 완료할 수 없다. 데이터 일관성을 위해 가용성을 희생하는 것이다.</p>
<h3 id="이메일-전송-가능성">이메일 전송 가능성</h3>
<p>메일 서버를 구상하고 이메일을 보내는 것은 쉽지만 특정 사용자의 메일함에 실제로 메일이 전달되도록 하는 것은 어려운 문제다.</p>
<p>이메일 전송 가능성을 높이기 위해서는 다음과 같은 요소들을 고려해야 한다.</p>
<ul>
<li>전용 IP : 이메일을 보낼 때는 전용 IP 주소를 사용한다.</li>
<li>범주화 : 범주가 다른 이메일은 다른 IP 주소를 통해 보내라.</li>
<li>발신인 평판: 새로운 이메일 서버의 IP 주소는 사용 빈도를 서서히 올리는 것이 좋다. </li>
<li>스팸 발송자의 신속한 차단 : 스팸을 뿌리는 사용자는 서버 평판을 심각하게 훼손하기 전에 시스템에서 신속히 차단해야 한다.</li>
<li>피드백 처리 : 불만 신고가 접수되는 비율을 낮추고 스팸 계정을 신속히 차단하기 위해서는 ISP 측에서의 피드백을 쉽게 받아 처리할 수 있는 경로를 만드는 것이 중요하다.<h3 id="검색">검색</h3>
기본적인 이메일 검색은 보통 이메일 제목이나 본문에 특정 키워드가 포함되었는지 찾는 것을 뜻한다. 검색 기능을 제공하려면 이메일이 전송, 수신, 삭제될 때마다 색인(인덱싱) 작업을 수행해야 한다.
그에 반해 검색은 사용자가 &#39;검색&#39; 버튼을 누를 때만 실행된다. 따라서 이메일 시스템의 검색 기능에서는 쓰기 연산이 읽기 연산보다 훨씬 많이 발생한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/8cfd194f-750b-44f9-8978-3f116a064656/image.png" alt="">
검색 기능을 지원하기 위해서는 엘라스틱서치를 이용하는 방법과 데이터 저장소에 내장된 기본 검색 기능을 활용하는 방안의 두 가지 선택지를 지금부터 비교해보자.</p>
<blockquote>
<p>방안 1: 엘라스틱 서치</p>
</blockquote>
<p>엘라스틱 서치 기술을 활용해 검색 기능을 구현할 경우 아래와 같은 설계안이 나온다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/1e2a928c-a1bb-46fb-856c-7b14589763e7/image.png" alt=""></p>
<p>질의가 대부분 사용자의 이메일 서버에서 실행되므로 user_id를 파티션 키로 사용하여 같은 사용자의 이메일은 같은 노드에 묶어 놓는다.</p>
<p>본 설계안은 카프카를 활용하여 색인 작업을 시작하는 서비스와 실제로 색인을 수행할 서비스 사이의 결합도를 낮추는 방안을 채택했다.
엘라스텍 서치를 사용할 경우 한 가지 까다로운 문제는 주 이메일 저장소와 동기화를 맞추는 부분이다.</p>
<blockquote>
<p>방안 2: 맞춤형 검색 솔루션</p>
</blockquote>
<p>대규모 이메일 서비스 사업자는 보통 자기 제품에 고유한 요구사항을 만족시키기 위해 검색 엔진을 자체적으로 개발해 사용한다. 하지만 이메일 검색 엔진의 설계는 아주 복잡한 과제이기 때문에, 여기서는 자세히 다루지는 않는다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/5c1e7571-b51b-458d-a558-7b45fc6163d6/image.png" alt="">
두 가지 방안을 간략하게 비교해본 표는 위와 같다.</p>
<p>소규모의 이메일 시스템을 구축하는 경우에는 엘라스틱 서치가 좋은 선택지다.
통합하기 쉽고 엔지니어링에 많은 노력이 필요하지도 않다. 대규모 시스템을 구축하는 경우에도 일래스틱서치를 사용할 수는 있겠지만 이메일 검색 인프라를 개발하고 관리하는 전담 팀이 필요할 수 있다. </p>
<p>지메일이나 아웃룩 규모의 이메일 시스템을 지원하려면 독립적인 검색 전용 시스템을 두기보다는 데이터 베이스에 내장된 전용 검색 솔루션을 사용하는 것이 바람직할 수도 있다.</p>
<h3 id="규모-확장성-및-가용성">규모 확장성 및 가용성</h3>
<p>각 사용자의 데이터 접근 패턴은 다른 사용자와 무관하므로, 시스템의 대부분 컴포넌트는 수평적으로 규모 확장이 가능할 것으로 기대할 수 있다.
가용성을 향상시키기 위해서는 데이터를 여러 데이터센터에 다중화하는 것이 필요하다. 사용자는 네트워크 토폴로지 측면에서 보았을 때 자신과 물리적으로 가까운 메일 서버와 통신한다. 장애 때문에 네트워크 파티션(network partition), 즉 통신이 불가능한 네트워크 영역이 생기게 되면 사용자는 다른 데이터센터에 보관된 메시지를 이용한다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/e300d62b-07f3-491b-9d4d-34b9380c4921/image.png" alt=""></p>
<h2 id="4단계-마무리">4단계 마무리</h2>
<p>면접장에서 시간이 된다면 아래 추가로 논의해볼 만한 주제도 언급하면 좋을 것 같다.</p>
<ul>
<li>결합 내성(fault tolerance): 시스템의 많은 부분에 장애가 발생할 수 있다.
노드 장애, 네트워크 문제, 이벤트 전달 지연 등의 문제에 어떻게 대처할지 살펴보면 좋을 것이다.</li>
<li>규정 준수(compliance): 이메일 서비스는 전 세계 다양한 시스템과 연동해야 하고 각 나라에는 준수해야 할 법규가 있다. 예를 들어 유럽에서는 GDPR(General Data Protection Regulation) 기준에 따라 개인 식별 정보(Personally Identifiable Information, PII)를 처리하고 저장해야 한다. 합법적 감청(legal intercept)은 이 분야의 또 다른 대표적 특징이다.</li>
<li>보안(security): 이메일 보안은 중요하다. 이메일에는 민감한 정보가 포함되기 때문이다. 지메일은 피싱이나 멀웨어 공격을 방지하는 피싱 방지(phish-ing protection), 안전하지 않은 사이트를 경고하는 안전 브라우징(safe browsing), 보안 결함이 있는 첨부 파일에 대한 사전 경고(proactive alert), 의심스러운 로그인 시도를 차단하는 계정 안전(account safety), 송신자가 메시지에 대한 보안 정책을 설정할 수 있도록 하는 기밀 모드(confidential mode), 타인이 이메일 내용을 엿보지 못하도록 하는 이메일 암호화(email encryption) 등의 보안 관련 기능을 제공한다.</li>
<li>최적화(optimization): 때로는 같은 이메일이 여러 수신자에게 전송되기 때문에 똑같은 첨부 파일이 그룹 이메일 객체 저장소(S3)에 여러 번 저장되는 경우가 있다. 저장하기 전에 저장소에 이미 동일한 첨부 파일이 있는지 확인하면 저장 연산 실행 비용을 최적화할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[4장 분산 메세지 큐 (1)]]></title>
            <link>https://velog.io/@ye_suri_106/4%EC%9E%A5-%EB%B6%84%EC%82%B0-%EB%A9%94%EC%84%B8%EC%A7%80-%ED%81%90-1</link>
            <guid>https://velog.io/@ye_suri_106/4%EC%9E%A5-%EB%B6%84%EC%82%B0-%EB%A9%94%EC%84%B8%EC%A7%80-%ED%81%90-1</guid>
            <pubDate>Mon, 24 Jun 2024 13:02:36 GMT</pubDate>
            <description><![CDATA[<p>이번 장에서는 시스템 설계 면접에서 자주 마주하는 주제인, 분산 메시지 큐 설계에 대해 알아본다. <strong>현대적 소프트웨어 아키텍처를 따르는 시스템은 잘 정의된 인터페이스를 경계로 나뉜 작고 독립적인 블록들로 구성</strong>된다.</p>
<p>메세지 큐는 이 블록 사이의 통신과 조율을 담당한다. 그럼 메세지 큐를 사용했을 때의 장점은 무엇일까 ?</p>
<ul>
<li>결합도 완화(decoupling): 메시지 큐를 사용하면 컴포넌트 사이의 강한 결합이 사라지므로 각각을 독립적으로 갱신할 수 있다.</li>
<li>규모 확장성 개선: 메시지 큐에 데이터를 생산하는 생산자(producer)와 큐에서 메시지를 소비하는 소비자(consumer) 시스템 규모를 트래픽 부하에 맞게 독립적으로 늘릴 수 있다. 예를 들어 트래픽이 많이 몰리는 시간에는 더 많은 소비자를 추가하여 처리 용량을 늘릴 수 있다.</li>
<li>가용성 개선: 시스템의 특정 컴포넌트에 장애가 발생해도 다른 컴포넌트는 큐와 계속 상호작용을 이어갈 수 있다.</li>
<li>성능 개선: 메시지 큐를 사용하면 비동기 통신이 쉽게 가능하다. 생산자는 응답을 기다리지 않고도 메시지를 보낼 수 있고, 소비자는 읽을 메시지가 있을 때만 해당 메시지를 소비하면 된다. 서로를 기다릴 필요가 없다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/988a3bf5-6e34-49ca-9560-77167e878081/image.png" alt=""></p>
<h3 id="메시지-큐-대-이벤트-스트리밍-플랫폼">메시지 큐 대 이벤트 스트리밍 플랫폼</h3>
<p>엄밀하게 말하면 아파치 카프카(Apache Kafka)나 펄사(Pulsar)는 메시지 큐가 아니라 이벤트 스트리밍 플랫폼(event streaming platorm)이다. 하지만 메시지 큐(RocketMQ, ActiveMQ, RabbitMQ, ZeroMQ 등)와 이벤트 스트리밍 플랫폼(카프카, 펄사) 사이의 차이는 지원하는 기능이 서로 수렴하면서 점차 희미해지고 있다. </p>
<p>예를 들어 전형적인 메시지 큐 RabbitMQ는 옵션으로 제공되는 스트리밍 기능을 추가하면 메시지를 반복적으로 소비할 수 있는 동시에 데이터의 장기 보관도 가능하다. 그리고 그 기능은 데이터 추가(append)만 가능한 로그(1og)를 통해 구현되어 있는데, 이벤트 스트리밍 플랫폼 구현과 유사하다.
아파치 펄사는 기본적으로 카프카의 경쟁자이지만, 분산 메시지 큐로도 사용 이 가능할 정도로 유연하고 성능도 좋다.</p>
<p>이번 장에서는 데이터 장기 보관(long data retention), 메시지 반복 소비(re-peated consumption of messages) 등의 부가 기능을 갖춘 분산 메시지 큐를 설계해 볼 것이다. 지금 언급한 부가 기능은 통상적으로는 이벤트 스트리밍 플랫 폼에서만 이용 가능하다.</p>
<h2 id="1단계-문제-이해-및-설계-범위-확정">1단계 문제 이해 및 설계 범위 확정</h2>
<p>메세지 큐의 기본 기능은, 생산자는 메세지를 큐에 보내고, 소비자는 큐에서 메세지를 꺼낼 수 있으면 된다. 하지만 이 기본 기능 이외에도 성능, 메세지 전달 방식, 데이터 보관 기간 등 고려할 사항은 다양하다. 적절한 질문을 통해 요구사항을 분명히 밝히고 설계 범위를 좁혀야 한다.</p>
<h3 id="기능-요구사항">기능 요구사항</h3>
<ul>
<li>생산자는 메시지 큐에 메시지를 보낼 수 있어야 한다.</li>
<li>소비자는 메시지 큐를 통해 메시지를 수신할 수 있어야 한다.</li>
<li>메시지는 반복적으로 수신할 수도 있어야 하고, 단 한 번만 수신하도록 설정 될 수도 있어야 한다.</li>
<li>오래된 이력 데이터는 삭제될 수 있다.</li>
<li>메시지 크기는 킬로바이트 수준이다.</li>
<li>메시지가 생산된 순서대로 소비자에게 전달할 수 있어야 한다.</li>
<li>메시지 전달 방식은 최소 한 번, 최대 한 번, 정확히 한 번 가운데 설정할 수 있어야 한다.</li>
</ul>
<h3 id="비기능-요구사항">비기능 요구사항</h3>
<ul>
<li>높은 대역폭과 낮은 전송 지연 가운데 하나를 설정으로 선택 가능하게 하는 기능</li>
<li>규모 확장성. 이 시스템은 특성상 분산 시스템일 수밖에 없다. 메시지 양이 급증해도 처리 가능해야 한다.</li>
<li>지속성 및 내구성(persisteney and durability). 데이터는 디스크에 지속적으 로 보관되어야 하며 여러 노드에 복제되어야 한다.</li>
</ul>
<h3 id="전통적-메시지-큐와-다른-점">전통적 메시지 큐와 다른 점</h3>
<p>RabbitMQ와 같은 전통적인 메시지 큐는 <strong>이벤트 스트리밍 플랫폼처럼 메시지 보관 문제를 중요하게 다루지 않는다.</strong> 전통적인 큐는 메시지가 소비자에 전달되기 충분한 기간 동안만 메모리에 보관한다. 처리 용량을 넘어선 메시지는 디스크에 보관하긴 하는데 이벤트 스트리밍 플랫폼이 감당하는 용량보다는 아주 낮은 수준이다. 전통적인 메시지 큐는 메시지 전달 순서도 보존하지 않는다. 생산된 순서와 소비되는 순서는 다를 수 있다. 그런 차이를 감안하면 설계는 크게 단순해질 수 있다.</p>
<h2 id="2단계-개략적-설계안-제시-및-동의-구하기">2단계 개략적 설계안 제시 및 동의 구하기</h2>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/3fca65d5-dd2e-46a7-9ba0-770838aa38af/image.png" alt="">
메세지 큐의 기본 기능은 아래와 같다.</p>
<ul>
<li>생산자는 메시지를 메시지 큐에 발행</li>
<li>소비자는 큐를 구독(subscribe)하고 구독한 메시지를 소비</li>
<li>메시지 큐는 생산자와 소비자 사이의 결합을 느슨하게 하는 서비스로, 생산자와 소비자의 독립적인 운영 및 규모 확장을 가능하게 하는 역할 담당</li>
<li>생산자와 소비자는 모두 클라이언트/서버 모델 관점에서 보면 클라이언트고 서버 역할을 하는 것은 메시지 큐이며 이 클라이언트와 서버는 네트워크를 통해 통신</li>
</ul>
<h3 id="메세지-모델">메세지 모델</h3>
<p>가장 널리 쓰이는 메시지 모델은 일대일(point-to-point)과 발행-구독(publish-subscribe) 모델이다.</p>
<blockquote>
<p>일대일 모델</p>
</blockquote>
<p>이 모델은 전통적인 메시지 큐에서 흔히 발견되는 모델이다. 일대일 모델에서 큐에 전송된 메시지는 오직 한 소비자가 가져갈 수 있다. 소비자가 아무리 많아도 각 메시지는 오직 한 소비자만 가져갈 수 있다. 아래 그림을 보면 메세지 A를 가져가는 것은 소비자 1뿐이다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/6a211a1e-e44c-4535-9aed-79a48f3713f9/image.png" alt=""></p>
<p>어떤 소비자가 메시지를 가져갔다는 사실을 큐에 알리면(acknowledge) 해당 메시지는 큐에서 삭제된다. 이 모델은 데이터 보관(data retention)을 지원하지 않는다. 반면 본 설계안은 메시지를 두 주 동안은 보관할 수 있도록 하는 지속성 계층(persistence layer)를 포함하며, 해당 계층을 통해 메시지가 반복적으로 소비될 수 있도록 한다.</p>
<p>비록 본 설계안은 일대일 모델도 지원할 수 있기는 하지만, 그 기능은 발행-구독 모델 쪽에 좀 더 자연스럽게 부합한다.</p>
<blockquote>
<p>발행 - 구독 모델</p>
</blockquote>
<p>발행-구독 모델을 설명하려면 토픽(topic)이라는 새로운 개념을 도입해야 한다. 토픽은 메시지를 주제별로 정리하는 데 사용된다. 각 토픽은 메시지 큐 서비스 전반에 고유한 이름을 가진다.</p>
<p>메시지를 보내고 받을 때는 토픽에 보내고 받게 된다.</p>
<p>이 모델에서 토픽에 전달된 메시지는 해당 토픽을 구독하는 모든 소비자에 전달된다. 아래 그림을 보면 메시지 A는 소비자 1과 2 모두에 전달된다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/00ad4c6c-f51b-47c3-83b4-f8078bd1741b/image.png" alt=""></p>
<p>본 설계안이 제시할 분산 메시지 큐는 방금 살펴본 두 가지 모델을 전부 지원 한다. 발행•구독 모델은 토픽을 통해 구현할 수 있고, 일대일 모델은 소비자 그룹(consumer group)을 통해 지원할 수 있다.</p>
<h3 id="토픽-파티션-브로커">토픽, 파티션, 브로커</h3>
<p>앞서 언급했듯이 메시지는 토픽에 보관된다. 토픽에 보관되는 데이터의 양이 커져서 서버 한 대로 감당하기 힘든 상황이 벌어지면 어떻게 될까?</p>
<p>이 문제를 해결하는 한 가지 방법은 파티션(partition), 즉 샤딩(sharding) 기법을 활용하는 것이다. 아래 그림 같이, 토픽을 여러 파티션으로 분할한 다음에 메시지를 모든 파티션에 균등하게 나눠 보낸다. 파티션은 토픽에 보낼 메시지의 작은 부분집합으로 생각하면 좋다. 파티션은 메시지 큐 클러스터 내의 서버에 고르게 분산 배치한다. 파티션을 유지하는 서버는 보통 브로커(Broker)라 부른다. 파티션을 브로커에 분산하는 것이 높은 규모 확장성을 달성하는 비결이다. 토픽의 용량을 확장하고 싶으면 파티션 개수를 늘리면 되기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/db6f5285-8dfd-4ae8-932a-d95c92de41dc/image.png" alt=""></p>
<p>각 토픽 파티션은 FIFO(first in, finst Out) 큐처럼 동작한다. 같은 파티션 안에서는 메시지 순서가 유지된다는 뜻이다. 파티션 내에서의 메시지 위치는 오프셋(offset)이라고 한다.</p>
<p>생산자가 보낸 메시지는 해당 토픽의 파티션 가운데 하나로 보내진다. 메시지에는 사용자 ID 같은 키를 붙일 수 있는데, 같은 키를 가진 모든 메시지는 같은 파티션으로 보내진다. 키가 없는 메시지는 무작위로 선택된 파티션으로 전송된다.</p>
<p>토픽을 구독하는 소비자는 하나 이상의 파티션에서 데이터를 가져오게 된다. 토픽을 구독하는 소비자가 어럿인 경우, 각 구독자는 해당 토픽을 구성하는 파티션의 일부를 담당하게 된다. 이 소비자들을 해당 토픽의 소비자 그룹 (consumer group)이라 부른다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/41826892-8f5d-452b-ab9d-f3f501808baa/image.png" alt=""></p>
<h3 id="소비자-그룹">소비자 그룹</h3>
<p>앞서 언급한 대로 본 설계안은 일대일 모델과 발행-구독 모델을 전부 지원해야 한다. 소비자 그룹 내 소비자는 토픽에서 메시지를 소비하기 위해 서로 협력 한다.</p>
<p>하나의 소비자 그룹은 여러 토픽을 구독할 수 있고 오프셋을 별도로 관리한다. 예를 들어, 큐 용레에 따라 과금(billing)용 그룹, 회계(accounting)용 그룹 등으로 나눌 수 있을 것이다.</p>
<p>같은 그룹 내의 소비자는 메시지를 병렬로 소비할 수 있다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/6af89685-6517-49a0-ad46-f7df0df0040d/image.png" alt=""></p>
<ul>
<li>소비자 그룹 1은 토픽 A를 구독한다.</li>
<li>소비자 그룹 2는 토픽 A와 토픽 B를 구독한다.</li>
<li>토픽 A는 그룹-1과 그룹-2가 구독하므로, 해당 토픽 내 메시지는 그룹-1과 그룹-2 내의 소비자에게 전달된다. 따라서 발행-구독 모델을 지원한다.</li>
</ul>
<p>🚨 하지만 문제가 하나 있다. 데이터를 병렬로 읽으면 대역폭(throughput) 측면에서는 좋지만 같은 파티션 안에 있는 메시지를 순서대로 소비할 수는 없다. 가령 소비자-1과 소비자-2가 같은 파티션-1의 메시지를 읽어야 한다고 하자. 파티션-1 내의 메시지 소비 순서를 보장할 수 없게 된다.</p>
<p>한 가지 제약사항을 추가하면 이 문제는 해결할 수 있다. 즉, 어떤 파티션의 메시지는 한 그룹 안에서는 오직 한 소비자만 읽을 수 있도록 하는 것이다. 다만 그 경우, 그룹 내 소비자의 수가 구독하는 토픽의 파티션 수보다 크면 어떤 소비자는 해당 토픽에서 데이터를 읽지 못하게 된다. 예를 들어 위의 그림의 그룹-2에 있는 소비자-3은 토픽 B의 메시지를 수신할 수 없다. 같은 그룹 내의 소비자-4가 이미 소비하도록 되어 있기 때문이다.
이 제약사항을 도입한 후에 모든 소비자를 같은 소비자 그룹에 두면 같은 파 티션의 메시지는 오직 한 소비자만 가져갈 수 있으므로 결국 일대일 모델에 수렴하게 된다. 파티션은 가장 작은 저장 단위이므로 미리 충분한 파티션을 할당해 두면 파티션의 수를 동적으로 늘리는 일은 피할 수 있다. 처리 용량을 늘리 려면 그냥 소비자를 더 추가하면 된다.</p>
<h3 id="개략적-설계안">개략적 설계안</h3>
<p>다음 그림은 수정된 개략적 설계안이다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/b99e1ea3-5729-4fbe-97c8-e15215b85924/image.png" alt=""></p>
<blockquote>
<p>클라이언트</p>
</blockquote>
<ul>
<li>생산자: 메시지를 특정 토픽으로 보낸다.</li>
<li>소비자 그룹: 토픽을 구독하고 메시지를 소비한다.</li>
</ul>
<blockquote>
<p>핵심 서비스 및 저장소</p>
</blockquote>
<ul>
<li><p>브로커: 파티션들을 유지한다. 하나의 파티션은 특정 토픽에 대한 메시지의 부분 집합을 유지한다.</p>
</li>
<li><p>저장소</p>
<ul>
<li>데이터 저장소: 메시지는 파티션 내 데이터 저장소에 보관된다.</li>
<li>상태 저장소: 소비자 상태는 이 저장소에 유지된다.</li>
<li>메타데이터 저장소: 토픽 설정, 토픽 속성(property) 등은 이 저장소에 유지된다.</li>
</ul>
</li>
<li><p>조정 서비스(coordination service)</p>
<ul>
<li>서비스 탐색(service cliscovery): 어떤 브로커가 살아 있는지 알려준다.</li>
<li>리더 선출(leader election): 브로커 가운데 하나는 컨트롤러 역할을 담당 해야 하며, 한 클러스터에는 반드시 활성 상태 컨트롤러가 하나 있어야 한다. 이 컨트롤러가 파티션 배치를 책임진다.</li>
<li>아파치 주키퍼(Apache ZooKeeper)나 etcd가 보통 컨트롤러 선출을 담당 하는 컴포넌트로 널리 이용된다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[3장 구글 맵 (2)]]></title>
            <link>https://velog.io/@ye_suri_106/3%EC%9E%A5-%EA%B5%AC%EA%B8%80-%EB%A7%B5-2</link>
            <guid>https://velog.io/@ye_suri_106/3%EC%9E%A5-%EA%B5%AC%EA%B8%80-%EB%A7%B5-2</guid>
            <pubDate>Mon, 17 Jun 2024 10:10:14 GMT</pubDate>
            <description><![CDATA[<h1 id="3단계-상세-설계">3단계 상세 설계</h1>
<p>이번 절에서는 우선 데이터 모델부터 살펴본다. 그런 다음 위치 서비스, 경로 안내 서비스, 지도 표시에 대한 보다 상세한 설계를 진행할 것이다.</p>
<h2 id="데이터-모델">데이터 모델</h2>
<p>본 설계안이 다루는 시스템은 다음 네 가지 데이터를 취급한다.</p>
<ol>
<li>경로 안내 타일</li>
<li>사용자 위치</li>
<li>지오코딩 데이터</li>
<li>미리 계산해 둔 지도 타일 데이터</li>
</ol>
<h3 id="경로-안내-타일">경로 안내 타일</h3>
<p>앞서, 애초에 필요한 도로 데이터로는 외부 사업자나 기관이 제공한 것을 이용한다고 밝힌 바 있다. 이 데이터의 용량은 수 테라바이트에 달하며, 애플리케이션이 지속적으로 수집한 사용자 위치 데이터를 통해 끊임없이 개선된다.</p>
<p>이 데이터는 방대한 양의 도로 및 그 메타데이터(이름, 관할구, 위도, 경도 등 의 도로 부속 정보)로 구성된다. 그래프 자료 구조 형태로 가공되지 않은 데이터이므로, 주어진 상태 그대로는 경로 안내 알고리즘의 입력으로 활용할 수 없다. </p>
<p>그러므로 <strong>경로 안내 타일 처리 서비스(routing tile processing service)</strong> 라 불리는 오프라인 데이터 가공 파이프라인을 주기적으로 실행하여 경로 안내 타일로 변환한다. 도로 데이터에 발생한 새로운 변경사항을 반영하기 위해서다.</p>
<p>지도 101절에서 살펴보았지만, 경로 안내 타일을 만들 때는 해상도를 달리 하여 세 벌을 만든다. 각 타일에는 그래프의 노드와 선분으로 표현된 해당 지역 내 교차로와 도로 정보가 들어 있다. 다른 타일의 도로와 연결되는 경우에는 해당 타일에 대한 참조 정보도 포함된다. 경로 안내 알고리즘은 이들 타일이 모인 결과로 만들어지는 도로망 데이터를 점진적으로 소비한다.</p>
<p>그렇다면 경로 안내 타일 처리 서비스는 가공 결과로 만든 타일을 어디에 저장해야 할까? 그래프 데이터는 메모리에 인접 리스트(adiacency list) 형태로 보관하는 것이 일반적이다. 하지만 본 설계안이 다루는 타일 데이터는 메모리에 두기에는 양이 너무 많다. 그래프의 노드와 선을 데이터베이스 레코드로 저장하는 것도 방법이겠지만 비용이 많이 든다. 게다가 경로 안내 타일의 경우 데이터베이스가 제공하는 기능이 필요 없다는 것도 문제다.</p>
<p><strong>경로 안내 타일을 저장하는 효율적 방법은 S3 같은 객체 저장소(obiect stor-age)에 파일을 보관하고 그 파일을 이용할 경로 안내 서비스에서 적극적으로 캐싱하는 것</strong>이다. 인접 리스트를 이진 파일(binary file) 형태로 직렬화(serial-ize) 해주는 고성능 소프트웨어 패키지는 많다. <strong>타일을 객체 저장소에 보관할 때는 지오해시 기준으로 분류해 두는 것이 좋다.</strong> 그러면 위도와 경도가 주어졌 을 때 타일을 신속하게 찾을 수 있다.</p>
<h3 id="사용자-위치-데이터">사용자 위치 데이터</h3>
<p>사용자의 위치 정보는 아주 값진 데이터다. 이 데이터는 도로 데이터 및 경로 안내 타일을 갱신하는 데 이용되며, 실시간 교통 상황 데이터나 교통 상황 이력 데이터베이스를 구축하는 데도 활용된다. 아울러 데이터 스트림 프로세싱 서비스는 이 위치 데이터를 처리하여 지도 데이터를 갱신한다.</p>
<p>사용자 위치 데이터를 저장하려면 엄청난 양의 쓰기 연산을 잘 처리할 수 있으면서 수평적 규모 확장이 가능한 데이터베이스가 필요하다. 카산드라는 그 기준을 잘 만족시키는 후보다.
해당 데이터베이스의 레코드는 다음과 같은 형태다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/1e2f8b09-6ddd-40a2-9250-ac1810af9847/image.png" alt=""></p>
<h3 id="지오코딩-데이터베이스">지오코딩 데이터베이스</h3>
<p>이 데이터베이스에는 주소를 위도/경도 쌍으로 변환하는 정보를 보관한다. 레디스처럼 빠른 읽기 연산을 제공하는 키값 저장소가 이 용도에 적당한데, 읽기 연산은 빈번한 반면 쓰기 연산은 드물게 발생하기 때문이다. 출발지와 목적지 주소는 경로 계획 서비스에 전달하기 전에 이 데이터베이스를 통해 위도/경도 쌍으로 변환되어야 한다.</p>
<h3 id="미리-만들어-둔-지도-이미지">미리 만들어 둔 지도 이미지</h3>
<p>단말이 특정 영역의 지도를 요청하면 <strong>인근 도로 정보를 취합하여 모든 도로 및 관련 상세 정보가 포함된 이미지를 만들어 내야 한다.</strong> 계산 자원을 많이 사용 할 뿐 아니라 같은 이미지를 중복 요청하는 경우가 많으므로 이미지는 한 번만 계산하고 그 결과는 캐시해 두는 전략을 쓰는 것이 좋다. </p>
<p>이미지는 지도 표시 에 사용하는 확대 수준별로 미리 만들어 두고 CDN을 통해 전송한다. CDN 원 본 서버로는 아마존 S3 같은 클라우드 저장소를 활용한다.</p>
<hr>
<h2 id="서비스">서비스</h2>
<p>데이터 모델을 살펴봤으니 이제 구글 맵 구현에 가장 중요한 위치 서비스, 지도 표시 서비스, 경로 안내 서비스를 살펴보자 !</p>
<h3 id="위치-서비스">위치 서비스</h3>
<p>데이터베이스 설계 및 사용자 위치 정보가 이용되는 방식에 초점을 맞추어 상세 설계를 진행하겠다.</p>
<p>사용자 위치 데이터 저장에는 위에서 설명했듯이, 키-값 저장소를 활용한다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/4e78b433-b9f0-427e-b2a5-e4d7853e3e1d/image.png" alt=""></p>
<p><strong>초당 백만 건의 위치 정보 업데이트가 발생한다는 점을 감안하면 쓰기 연산 지원에 탁월한 데이터베이스가 필요하다.</strong> </p>
<p>=&gt; <strong>NoSQL 키 값 데이터베이스나 열•중심 데이터베이스(column-oriented datalase)가 그런 요구사항에 적합</strong>하다. 또 한 가지 유의할 사항은, 사용자 위치는 계속 변화하며 일단 변경되고 나면 이전 정보는 바로 무용해지고 말기 때문에, 데이터 일관성(consistency)보다는 가용성(availability)이 더 중요하다는 점이다. </p>
<p>CAP 정리(theorem)에 따르면 일관성(Consistency), 가용성(Availability), 분할 내성(Partition tolerance) 모두를 만족 시킬 방법은 없다.
그러므로 주어진 요구사항에 근거하여, 본 설계안은 가용성과 분할내성 두 가지를 만족시키는 데 집중한다. 그리고 이 요구사항에 가장 적합한 데이터베이스 가운데 하나는 카산드라다. 높은 가용성을 보장하면 서도 막대한 규모의 연산을 감당할 수 있도록 해 줄 것이다.</p>
<p>데이터베이스 키로는 (user_id, timestamp)의 조합을 사용하며, 해당 키에 매 달리는 값으로는 위도/경도 쌍을 저장한다. 이때 user_id는 파티션 키(partition key)이며 timestamp는 클리스터링 키(clustering key)로 활용한다. user_id를 파티션 키로 사용하는 것은 특정 사용자의 최근 위치를 신속히 읽어 내기 위해서다. 같은 파티션 키를 갖는 데이터는 함께 저장되며 클러스터링 키 값에 따라 정렬된다. 이렇게 해 두면 특정 사용자의 특정 기간 내 위치도 효율적으로 읽어낼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/6d15f594-79f3-4a5c-a94c-2c72b4cd337d/image.png" alt=""></p>
<h3 id="사용자-위치-데이터는-어떻게-이용되는가">사용자 위치 데이터는 어떻게 이용되는가</h3>
<p>사용자 위치는 쓰임새가 다양한 중요 데이터다. 가령 이 데이터를 활용하면 새로 개설되었거나 폐쇄된 도로를 감지할 수 있다. 지도 데이터의 정확성을 점차로 개선하는 입력으로도 활용될 수 있다. 실시간 교통 현황을 파악하는 입력이 될 수도 있다.</p>
<p>이런 다양한 기능들을 지원하기 위해서 <strong>사용자 위치를 데이터베이스에 기록하는 것과 별도로 카프카와 같은 메시지 큐에 로깅한다.</strong> 카프카는 응답 지연이 낮고 많은 데이터를 동시에 처리할 수 있는 데이터 스트리밍 플랫폼으로, 실시간 데이터 피드(data feed)를 지원하기 위해 고안되었다. 아래 그림은 카프카를 활용하여 개선한 설계안이다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/e25e9862-94bf-4917-84a0-a1c1226b6e3d/image.png" alt=""></p>
<p>개별 서비스는 카프카를 통해 전달되는 사용자 위치 데이터 스트림을 각자 용도에 맞게 활용한다. 예를 들어 실시간 교통 상황 서비스는 해당 스트림을 통해 읽은 데이터로 실시간 교통량 데이터베이스를 갱신한다. 경로 안내 타일 처리 서비스는 해당 데이터를 활용해 새로 열린 도로나 폐쇄된 도로를 탐지하고 해당 변경 내역을 객체 저장소의 경로 안내 타일에 반영함으로써 점진적으로 지도의 품질을 개선한다.</p>
<h3 id="지도-표시">지도 표시</h3>
<p>이번 절에서는 지도 타일을 미리 만들어 놓는 방법과 지도 표시 최적화 기법을 살펴본다.</p>
<p><strong>지도 타일 사전 계산</strong>
앞서 언급했듯이 사용자가 보는 지도 크기나 확대 수준에 맞는 세부사항을 보여주기 위해서는 확대 수준별 지도 타일을 미리 만들어 둘 필요가 있다. 구글 맵은 21단계로 지도를 확대할 수 있으며 본 설계안도 마찬가지다.</p>
<p>확대 수준 0은 세계 전부를 256 x 256픽셀짜리 타일 하나로 표현한다.
확대 수준을 1단계 올릴 때마다 해당 수준을 위한 전체 타일 수는 <strong>동서 방향으로 두 배, 남북 방향으로 두 배 늘어난다.</strong> 각 타일 크기는 여전히 256 x 256픽 셀이다. 아래 그림에서 보듯, 확대 수준 1에 필요한 타일은 2 x 2장으로, 그 전 부를 합친 해상도는 512 x 512픽셀이다. </p>
<p>확대 수준 2에 필요한 타일은 4 x 4장 으로, 그 전부를 합친 해상도는 1024 x 1024 픽셀이다. <strong>확대 수준을 1단계 늘릴 때마다 해당 수준 전부를 합친 해상도는 그 이전 수준 대비 4배씩 늘어난다.</strong> 이 늘어난 해상도 덕에 사용자에게 더 상세한 정보를 제공할 수 있다. 아울러 클라이언트는 해당 정보를 제공하기 위한 타일을 다운 받는데 많은 네트워크 대역폭을 소진하지 않고도 클라이언트에 설정된 확대 수준에 최적인 크기의 지도를 표시할 수 있다. 화면에 한번에 표시 가능한 지도 타일 개수는 달라지지 않기 때문이다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/b4caef3e-40f7-4aa4-add4-fddfb36eb748/image.png" alt=""></p>
<p><strong>최적화: 벡터 사용</strong>
지도 표시에 WebGL 기술을 채택하면 어떤 혜택이 있을까? 네트워크를 통해 이미지를 전송하는 대신 경로(path)와 다각형(polygon) 등의 벡터(vector) 정보를 보내는 것이다. 클라이언트는 수신된 경로와 다각형 정보를 통해 지도를 그려내면 된다.</p>
<p><strong>벡터 타일의 한 가지 분명한 장점은 이미지에 비해 월등한 압축률</strong>이다. 따라서 네트워크 대역폭을 많이 아낄 수 있다.
그다지 뚜렷하진 않지만 기대할 수 있는 또 다른 장점은 훨씬 매끄러운 지도 확대 경험이다. 래스터 방식 이미지(rasterized image)를 사용하면 클라이언트가 확대 수준을 높이는 순간에 이미지가 늘어지고(stretch) 픽셀이 도드라져 보이는 문제가 있다. 시각 효과 측면에서는 상당히 거슬릴 수 있다. 하지만 벡터화 된 이미지를 사용하면 클라이언트는 각 요소 크기를 적절하게 조정할 수 있어서 훨씬 매끄러운 확대 경험을 제공할 수 있다.</p>
<h3 id="경로-안내-서비스">경로 안내 서비스</h3>
<p>이제 경로 안내 서비스를 좀 더 자세히 살펴보자. 이 기능은 가장 빠른 경로를 안내하는 역할을 담당한다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/d82111c0-2acc-46ed-91c0-bceda36ad788/image.png" alt=""></p>
<p>해당 그림에 등장하는 각 컴포넌트를 살펴보자.</p>
<p><strong>지오코딩 서비스</strong>
우선, 주소를 위도와 경도 쌍으로 바꿔주는 서비스가 필요하다. 주소의 표현 방식은 다양할 수 있다는 점을 고려해야 한다. 즉, 장소 이름으로 나타낸 주소도 있을 수 있고 지번 형태로 나타낸 주소도 있을 수 있다.</p>
<p>다음은 구글 지오코딩 API의 요청/응답 사례다.</p>
<pre><code>요청:
https://maps.googleapis.com/maps/api/geocode/json?address=1600+Amphit
heatre+Parkway, +Mountain+View, +CA</code></pre><pre><code>JSON 응답:
{
  &quot;results&quot; : [
    {
    &quot;formatted_address&quot; : &quot;1600 Amphitheatre Parkway, MountainView, CA 94043, USA&quot;,
    &quot;geometry&quot;: {
    &quot;location&quot; : {
      &quot;lat&quot;: 37.4224764,
      &quot;Ing&quot; : - 122.0842499
    },
    &quot;location_type&quot; : &quot;ROOFTOP&quot;,
    &quot;viewport&quot; : {
      &quot;northeast&quot;: {
        &quot;lat&quot;: 37.4238253802915,
        &quot;Ung&quot; : - 122.0829009197085
       }, 
      &quot;southwest&quot;: {
        &quot;lat&quot; : 37.4211274197085,
        &quot;Ing&quot; : - 122.0855988802915
      }
    },
    &quot;place_id&quot; : &quot;ChIJZeUgeAK6j4ARbn5u_WAGQWA&quot;,
    &quot;plus code&quot;: {
        &quot;compound_code&quot;: &quot;CWC8+W5 Mountain View, California,
        United States&quot;,
        &quot;global_ code&quot;: &quot;849VCWC8+W5&quot;
        },
        &quot;types&quot; : [ &quot;street_address&quot; ]
        }
    }
  ],
  &quot;status&quot; : &quot;OK&quot;
}
</code></pre><p>경로 안내 서비스는 이 서비스를 호출하여 출발지와 목적지 주소를 위도 경도 쌍으로 변환한 뒤 추후 다른 서비스 호출에 이용한다.</p>
<p><strong>경고 계획 서비스</strong></p>
<p>경로 계획 서비스(route planner selvice)는 현재 교통 상황과 도로 상태에 입각하여 이동 시간 측면에서 최적화된 경로를 제안하는 역할을 담당한다. 뒤이어 설명할 다른 서비스들과 통신하여 결과를 만들어 낸다.</p>
<p><strong>최단 경로 서비스</strong></p>
<p>최단 경로 서비스(shortest path service)는 출발지와 목적지 위도/경도를 입력으로 받아 k개 최단 경로를 반환하는 서비스다. 이때 교통이나 도로 상황은 고려하지 않는다. 다시 말해 도로 구조에만 의존하여 계산을 수행한다. 도로망 그래프는 거의 정적이므로 캐시해 두면 좋다.</p>
<p>최단 경로 서비스는 객체 저장소에 저장된 경로 안내 타일에 대해 A* 경로 탐색 알고리즘의 한 형태를 실행한다.</p>
<ul>
<li>입력으로 출발지와 목적지의 위도/ 정도를 받는다. 이 위치 정보를 지오해시 로 변환한 다음 출발지와 목적지 경로 안내 타일을 얻는다.</li>
<li>출발지 타일에서 시작하여 그래프 자료 구조를 탐색해 나간다. 탐색 범위를 넓히는 과정에서 필요한 주변 타일은 객체 저장소에서(과거에 가져온 적이 있는 경우에는 캐시에서) 가져온다. 같은 지역의 다른 확대 수준 타일로도 연결이 존재할 수 있음에 유의하자. 고속도로만 있는 더 큰 타일로 진입하 거나 할 수 있는 것은 알고리즘이 그런 연결을 선택할 수 있기 때문이다. 최단 경로가 충분히 확보될 때까지 알고리즘은 검색 범위를 계속 확대해 나가면서 필요한 만큼 타일을 가져오는 작업을 반복할 것이다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/e21e986b-c3f8-4f02-b24d-ce51dca9d0cd/image.png" alt=""></p>
<p><strong>예상 도착 시간 서비스</strong></p>
<p>경로 계획 서비스는 최단 경로 목록을 수신하면 예상 도착 시간 서비스 (ETA service)를 호출하여 그 정로 각각에 대한 소요 시간 추정치를 구한다. 예상 도 착 시간 서비스는 기계 학습을 활용해 현재 교통 상황 및 과거 이력에 근거하여 예상 도착 시간을 계산한다.</p>
<p>이때 까다로운 문제는 실시간 교통 상황 데이터만 필요한 게 아니라 앞으로 10분에서 20분 뒤에 교통 상황이 어떻게 달라질지도 예측해야 한다는 것이다. 이런 문제는 알고리즘 차원에서 풀어야 하며 이번 절에서는 상세하게 다루지 않는다.</p>
<p><strong>순위 결정 서비스</strong>
경로 계획 서비스는 ETA 예상치를 구하고 나면 순위 결정 서비스(ranker)에 관 련 정보를 모두 전달하여 사용자가 정의한 필터링 조건을 적용한다. 유료 도로 제외, 고속도로 제외 등이 그런 필터링 조건의 사례다. 순위 결정 서비스는 필터링이 끝나고 남은 경로를 소요 시간 순으로 정렬하여 최단 시간 경로 k개를 구한 다음 경로 안내 서비스에 결과를 반환한다.</p>
<p><strong>중요 정보 갱신 서비스들</strong>
이 부류의 서비스는 카프카 위치 데이터 스트림을 구독하고 있다가 중요 데이터를 비동기적으로 업데이트하여 그 상태를 항상 최신으로 유지하는 역할을 담당한다. 실시간 교통 정보 데이터베이스나 경로 안내 타일 등이 그 사례다.</p>
<p>경로 안내 타일 처리 서비스는 도로 데이터에 새로 발견된 도로, 폐쇄되었음이 확인된 도로 정보를 반영하여 경로 안내 타일을 지속적으로 갱신한다. 그 덕에 최단 경로 서비스는 더 정확한 결과를 낼 수 있게 된다.</p>
<p>실시간 교통 상황 서비스는 활성화 상태 사용자가 보내는 위치 데이터 스트림에서 교통 상황 정보를 추출한다. 그 결과로 찾아낸 정보는 실시간 교통 상황 데이터베이스에 반영되며, 예상 도착 시간 서비스가 더욱 정확한 결과를 내는 데 쓰인다.</p>
<p><strong>적응형 ETA와 경로 변경</strong></p>
<p>현 설계안은 적응형 ETA와 경로 변경을 허용하지 않는다. 이 문제를 해결하려면 서버는 현재 경로 안내를 받고 있는 모든 사용자를 추적하면서 교통 상황이 달라질 때 마다 각 사용자의 ETA를 변경해 주어야 한다. 그러려면 다음 중요한 질문에 답할 수 있어야 한다.</p>
<ul>
<li>현재 경로 안내를 받고 있는 사용자는 어떻게 추적하나?</li>
<li>수백만 경로 가운데 교통 상황 변화에 영향을 받는 경로와 사용자를 효율적으로 가려낼 방법은 무엇인가?</li>
</ul>
<p>간단하긴 하지만 그다지 효율적이지 않은 방법부터 살펴보자. 사용자 user_1이 안내 받은 경로가 아래 그림 같이 경로 안내 타일 r_1, r_2, r_3, …, r로 구성되어 있다고 하자.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/7f796e0b-e259-46c3-925b-82bbac01956c/image.png" alt=""></p>
<p>경로 안내 서비스를 받고 있는 사용자와 그 경로 정보를 데이터베이스에 저장한다고 하면 그 형상은 아마 다음과 같을 것이다.</p>
<pre><code>user_1: r_1, r_2, r_3, ..., r_k 
user_2: r_4, r_6, r_9, ..., r_n 
user_3: r_2, r_8, r_9, ..., r_m
...
user_n: r_2, r_10, r_21, ..., r_1</code></pre><p>이때 경로 안내 타일 r_2에서 교통사고가 발생했다고 하자. 어떤 사용자가 영향을 받는지 알아내려면 위의 레코드를 전수 조사해서 어떤 사용자가 해당 타일을 가지고 있는지 알아보아야 한다.</p>
<pre><code>user_1: r_1, _**r_2**_, r_3, ..., r_k
user_2: r_4, r_6, r_9, ..., r_n 
user_3: _**r_2**_, r_8, r_9, ..., r_m
...
user_n: _**r_2**_, r_10, r_21, .... r_1</code></pre><p>이 테이블에 보관된 레코드 수가 n이고 안내되는 경로의 평균 길이가 m이라고하면 교통 상황 변화에 영향 받는 모든 사용자 검색의 시간 복잡도는 O(n x m) 일 것이다.</p>
<p>이 검색 속도를 더 높일 방법은 없을까? 다른 접근법을 알아보자. </p>
<p><strong>경로 안내를 받는 사용자 각각의 현재 경로 안내 타일, 그 타일을 포함하는 상위 타일(즉, 확대 수준이 더 낮은 타일), 그 상위 타일의 상위 타일을 출발지와 목적지가 모두 포함된 타일을 찾을 때까지 재귀적으로 더하여 보관하는 것</strong>이다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/f71fce78-137d-4f5b-a750-0568e6c379d8/image.png" alt=""></p>
<p>그 결과를 데이터베이스에 보관한다고 하면 레코드 하나의 형태는 다음과 같을 것이다.</p>
<pre><code>user_1, r_1, super(r_1), super(super(r_1)), ...</code></pre><p>이렇게 해 두면, <strong>어떤 타일의 교통 상황이 변했을 때 경로 안내 ETA가 달라지는 사용자는, 해당 사용자의 데이터베이스 레코드 마지막 타일에 그 타일이 속하는 사용자다.</strong> 그 이외의 사용자에게는 아무 영향이 없다. 검색 시간 복잡도가 O(n)으로 줄어들기 때문에 좀 더 효율적이다.</p>
<p>그러나 이 접근법은 교통 상황이 개선되었을 때 해야 하는 일까지 해결해주지는 않는다. 예를 들어 경로 안내 타일 2의 교통 상황이 회복되어서 사용자가 옛날 경로로 돌아가도 된다고 하자. 경로 재설정이 가능하다는 사실을 어떻게 감지하고 알릴 것인가? 한 가지 방안은 현재 경로 안내를 받는 사용자가 이용 가능한 경로의 ETA를 주기적으로 재계산하여 더 짧은 ETA를 갖는 경로가 발견되면 알리는 것이다.</p>
<p><strong>전송 프로토콜</strong></p>
<p>경로 안내 중에 경로의 상황이 변경될 수 있으므로, 데이터를 모바일 클라이언트에 전송할 안정적인 방법이 필요하다. 이 경우에 서버에서 클라이언트로 데이터를 보내는 데 활용할 수 있는 프로토콜로는 모바일 푸시 알림(mobile push noification), 롱 폴링(long polling), 웹소켓(webSocket), 서버 전송 이벤트(Server-Sent Events, SSE) 등이 있다.</p>
<ul>
<li>모바일 푸시 알림은 보낼 수 있는 메시지 크기가 매우 제한적이므로 (iOS의 경우에는 최대 4,096바이트) 사용하지 않는 게 바람직하다. 게다가 웹 애플리케이션은 지원하지 않는다.</li>
<li>웹소켓은 서버에 주는 부담이 크지 않아서 일반적으로 롱 폴링보다 좋은 방안으로 본다.</li>
<li>모바일 푸시 알림과 롱 폴링을 지원하지 않기로 하였으므로 남은 선택지는 웹소켓과 SSE다. 두 방법 모두 괜찮은 방법이긴 하지만 본 설계안에서는 웹 소켓을 사용할 것이다. 양방향 통신을 지원하기 때문인데, 가렁 패키지나 상품이 목적지에 가까워졌을 때는 실시간 양방향 통신이 필요한 경우도 있기 때문이다.</li>
</ul>
<p>이 모든걸 합친 설계도는 아래와 같다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/40f2cde9-3ab9-4c12-a98a-5d74c8cbec5e/image.png" alt=""></p>
<h2 id="4단계-마무리">4단계 마무리</h2>
<p>이번 장에서 우리는 위치 갱신, ETA, 경로 계획, 지도 표시 등의 핵심 기능을 지 원하는 단순화된 구글 맵 애플리케이션을 설계해 보았다. 이 시스템의 확장에 관심이 있다면, 기업 고객 대상으로 중간 경유지 설정 기능을 제공하는 것을 고려해 보면 좋을 것이다. 예를 들어 고객이 하나가 아닌 여러 목적지를 입력하면 그 모두를 어떤 순서로 방문해야 가장 빨리 경유할 수 있을지 실시간 교통 상황을 고려하여 안내하는 것이다. 이런 기능을 제공하면 도어대시(Door Dash), 우버(Uber), 리프트(Lyft) 같은 배달 서비스에 아주 유용할 것이다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/2b81c488-cb09-4c6a-b723-b8474fff605a/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[3장 구글 맵 (1)]]></title>
            <link>https://velog.io/@ye_suri_106/3%EC%9E%A5-%EA%B5%AC%EA%B8%80-%EB%A7%B5-1</link>
            <guid>https://velog.io/@ye_suri_106/3%EC%9E%A5-%EA%B5%AC%EA%B8%80-%EB%A7%B5-1</guid>
            <pubDate>Mon, 17 Jun 2024 09:08:10 GMT</pubDate>
            <description><![CDATA[<p>이번 장에서는 기존에 우리가 사용하는 구글 맵 보다는 단순한 형태의 구글 맵을 설계해보도록 한다.
구글 맵은 엄청나게 복잡한 제품이므로, 설계에 앞서 어떤 기능에 초점을 맞추어야 하는지 확인해야 한다.</p>
<h2 id="1단계-문제-이해-및-설계-범위-확정">1단계 문제 이해 및 설계 범위 확정</h2>
<h3 id="기능-요구-사항">기능 요구 사항</h3>
<ul>
<li>사용자 위치 갱신</li>
<li>경로 안내 서비스 (ETA - 예상 도착 시간 서비스 포함)</li>
<li>지도 표시</li>
</ul>
<h3 id="비기능-요구사항-및-제약-사항">비기능 요구사항 및 제약 사항</h3>
<ul>
<li>정확도 : 사용자에게 잘못된 경로를 안내하면 안된다.</li>
<li>부드러운 경로 표시 : 클라이언트를 통해 제공되는 경로 안내 용도의 지도는 화면에 아주 부드럽게 표시되고 갱신되어야 한다.</li>
<li>데이터 및 배터리 사용량 : 클라이언트는 가능한 한 최소한의 데이터와 배터리를 사용해야 한다. 모바일 단말에 아주 중요한 요구사항이다.</li>
<li>일반적으로 널리 통용되는 가용성 및 규모 확장성 요구사항을 만족해야 한다.</li>
</ul>
<p>설계 하기전 관련된 몇가지 기본 개념 및 용어를 먼저 살펴보자.</p>
<h3 id="지도-101">지도 101</h3>
<blockquote>
<p><strong>측위 시스템</strong></p>
</blockquote>
<p>지구는 축을 중심으로 회전하는 구이다. <strong>측위 시스템</strong>은 <strong>이 구 표면 상의 위치를 표현하는 체계</strong>를 말한다.
이 시스템에서 위도는 주어진 위치가 얼마나 북쪽/남쪽인지를 나타내며, 경도는 얼마나 동쪽/서쪽인지를 나타낸다.</p>
<blockquote>
<p><strong>3차원 위치의 2차원 변환</strong></p>
</blockquote>
<p>3차원 구 위의 위치를 2차원 평면에 대응시키는 절차를 <code>지도 투영법</code> 또는 <code>도법</code>이라고 부른다.
도법은 다양하며, 그 각각은 다른 도법과는 차별되는 장단점을 갖는다. 하지만 거의 모든 투영법은 실제 지형의 기하학적 특성을 왜곡한다는 공통점을 갖는다.
아래는 대표적인 도법 4개의 차이점을 나타내는 그림이다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/c844685e-ba7c-45d9-950a-13c640913bcd/image.png" alt="">
구글 맵은 메르카토르 도법을 조금 변경한 웹 메르카토르 도법을 택하고 있다.</p>
<blockquote>
<p><strong>지오코딩</strong></p>
</blockquote>
<p>지오코딩은 주소를 지리적 측위 시스템의 좌표로 변화하는 프로세스이다.
지오코딩을 수행하는 한 가지 방법은 <code>인터폴레이션</code> 이다. GIS와 같은 다양한 시스템이 제공하는 데이터를 결합한다는 뜻이다.
GIS는 도로망을 지리적 좌표 공간에 대응시키는 방법을 제공하는 여러 시스템 가운데 하나이다.</p>
<blockquote>
<p><strong>지오해싱</strong></p>
</blockquote>
<p><code>지오해싱</code> 은 <strong>지도 위 특정 영역을 영문자와 숫자로 구성된 짧은 문자열에 대응시키는 인코딩 체계</strong>이다. 이는 이전 장에서도 소개 했던 바가 있기 때문에, 간단하게 그림만 첨부한다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/9f116c73-3f5e-4663-bb47-eefb63d31180/image.png" alt=""></p>
<blockquote>
<p><strong>지도 표시</strong></p>
</blockquote>
<p>지도를 화면에 표시하는데 가장 기본이 되는 개념은 타일이다. 지도 전부를 하나의 이미지로 표시하는 대신, 작은 타일로 쪼개어 표시하는 것이다.</p>
<p>클라이언트는 사용자가 보려는 영역에 관계된 타일만 다운받아 모자이크처럼 이어 붙인 다음 화면에 뿌린다.</p>
<p>지도의 확대/축소를 지원하려면 확대 수준에 따라 다른 종류의 타일을 준비해야한다. 클라이언트는 현재 클라이언트가 보려는 지도 확대 수준에 근거하여 어떤 크기의 타일을 가져올지 고른다.</p>
<blockquote>
<p><strong>경로 안내 알고리즘을 위한 도로 데이터 처리</strong></p>
</blockquote>
<p>대부분의 경로 탐색 알고리즘은 다익스트라 알고리즘이나 A* 경로 탐색 알고리즘의 변종이다. 모든 경로 탐색 알고리즘은 교차로를 노드로, 도로는 노드를 잇는 선으로 표현하는 그래프 자료 구조를 가정한다는 것이다.</p>
<p>대부분의 경로 탐색 알고리즘의 성능은 주어진 그래프 크기에 아주 민감하다. 따라서 본 설계안이 가정하는 규모에서 좋은 성능을 보이려면 그래프를 관리 가능 단위로 분할할 필요가 있다.</p>
<p>전 세계 도로망을 더 작은 단위로 분할하는 방법 가운데 하나는 지도 표시에 사용하는 타일 기반 분할법과 아주 유사하다. 지오해싱과 비슷한 분할 기술을 적용하여 세계를 작은 격자로 나누고, 각 격자 안의 도로망을 노드와 선으로 구성된 그래프 자료 구조로 변환한다.</p>
<p>이 때 각 격자는 경로 안내 타일이라고 부르고, 이 타일은 도로로 여결된 다른 타일에 대한 참조를 유지한다. 그래야 경로 탐색 알고리즘이 연결된 타일들을 지나갈 때 보일 더 큰 도로망 그래프를 만들어 낼 수 있다.</p>
<p>도로망을 언제든 불러올 수 있는 경로 안내 타일로 분할해 놓으면 경로 탐색 알고리즘이 동작하는 데 필요한 메모리 요구량을 낮출 수 있을 뿐 아니라 한번에 처리해야 하는 경로의 양이 줄어들고, 필요한 만큼만 불러오면 되기 때문에 경로 탐색 성능도 좋아진다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/f0d93479-0511-4c74-932c-3364f3fc20ce/image.png" alt=""></p>
<blockquote>
<p>계층적 경로 안내 타일</p>
</blockquote>
<p>경로 안내가 효과적으로 동작하려면 필요한 수준의 구체성을 갖춘 도로 데이터가 필요하다. 보통 구체성 정도를 상, 중, 하로 구분하여 세 가지 종류의 경로 안내 타일을 준비한다.</p>
<p>가장 구체성이 <code>높은 타일(상)</code> 의 경우 그 크기는 아주 작으며, 이런 타일에는 지방도 데이터만 둔다.
그 다음 레벨의 <code>타일(중)</code> 은 더 넓은 지역을 커버하며, 규모가 비교적 큰 관할구를 잇는 간선 도로 데이터만 둔다. 
마지막으로 구체성이 가장 낮은 <code>타일(하)</code> 은 그보다 더 큰 영역을 커버하며, 그런 타일에는 도시와 주를 연결하는 주요 고속도로 데이터만 둔다. 
각 타일에는 다른 정밀도 타일로 연결되는 선이 있을 수 있다. 예를 들어 지방도 A에서 고속도로 F로 진입하는 경로를 표시하려면 도로 A의 특정 지점을 나타내는 노드에서 도로 F의 특정 지점을 나타내는 노드 사이에 연결선이 있어야만 한다.</p>
<h3 id="개략적-규모-추정">개략적 규모 추정</h3>
<p>이제 기본적인 지식은 습득하였으니 풀어야 할 문제 규모를 간단히 추정해보자. 설계 초점이 모바일 단말이므로, 데이터 사용량과 배터리 효율을 중요하게 따져 봐야 한다.
추정에 앞서 도량형 변환 규칙 몇 가지를 참고 삼아 정리해두겠다.</p>
<ul>
<li>1피트 = 0.3048미터</li>
<li>1킬로미터 = 0.6214마일</li>
<li>1킬로 = 1,000미터</li>
</ul>
<blockquote>
<p>저장소 사용량</p>
</blockquote>
<p>다음 세 가지 종류의 데이터를 저장해야 한다.</p>
<ul>
<li><strong>세계 지도</strong> : 상세한 저장 용량 계산식은 잠시 후 다룬다.</li>
<li><strong>메타데이터</strong> : 각 지도 타일의 메타데이터는 크기가 아주 작아서 무시해도 지장이 없을 정도이므로, 본 추정에서는 제외한다.</li>
<li><strong>도로 정보</strong> : 면접관과의 문답을 통해, 외부에서 받은 수 TB 용량의 도로 데이터를 보유하고 있음을 확인한 바 있다. 이 데이터를 경로 안내 타일로 변환하여야 한다. 변환 결과의 용량도 비슷할 것이다.</li>
</ul>
<p><strong>세계 지도</strong>
지원하는 확대 수준별로 지도 타일을 한 벌씩 두어야 한다. 그 타일 전부를 보관하는 데 필요한 용량을 가늠하려면 <em><strong>최대 확대 수준, 즉 지도를 최대한 확대하여 보는 데 필요한 타일 개수를 따져보면 좋다.</strong></em></p>
<p>지도를 확대할 때마다 하나의 타일을 네 장의 타일로 펼친다고 가정하자. 세계 지도를 21번 확대하여 볼 수 있으려면 최대 확대 수준을 대상으로 했을 때 약 4.4조 개의 타일이 필요하다. </p>
<p>한 장의 타일이 256 X 256 픽셀 압축 PNG 파일이라면 한 장당 100KB의 저장 공간이 필요하므로, 최대 확대 시 필요 타일을 전부 저장하는 데는 총 4.4조 X 100KB = 440PB 만큼의 저장 공간이 필요할 것이다.</p>
<p>하지만 지구 표면 가운데 90%는 인간이 살지 않는 자연 그대로의 바다, 사막, 호수, 산간 지역임에 유의하자. 이들 지역의 이미지는 아주 높은 비율로 압축할 수 있으므로, 보수적으로 보아 80%에서 90% 가량의 저장 용량을 절감할 수 있다. 따라서 저장 공간 요구량은 44PB에서 88PB 가량으로 줄어든다. 어림하여 50PB 정도가 필요하다고 보고 넘어가자.</p>
<p><strong>서버 대역폭</strong>
서버 대역폭을 추정하기 위해서는 어떤 유형의 요청을 처리해야 하는지 살펴봐야 한다. 서버가 처리해야하는 요청은 크게 두 가지다.</p>
<p>첫 번째는 <strong>경로 안내 요청으로, 클라이언트가 경로 안내 세션을 시작할 때 전송하는 메세지다</strong>. 두 번째는 <strong>위치 갱신 요청</strong>이다.
클라이언트가 경로 안내를 진행하는 동안 변경된 사용자 위치를 전송하는 메세지다.</p>
<p>이런 경로 안내 요청을 처리하기 위한 서버 대역폭을 분석해보자. DAU는 10억이고, 각 사용자는 경로 안내 기능을 평균적으로 주당 35분 사용한다고 하자. 이는 환산하면 주당 350억 분, 즉 하루에 50억 분이다.</p>
<p>단순한 접근법 하나는 GPS 좌표를 매초 전송하는 것인데, 그렇게 하면 하루에 3000억 건의 요청이 발생하고 (50억분 X 60) 이는 3백만 QPS에 해당한다.
하지만 클라이언트가 매초 새로운 GPS 좌표를 보낼 필요가 없을 수도 있다.
가령 이들 요청을 클라이언트 쪽에서 모아 두었다가 덜 자주 보내도록 하면(가령 15초나 30초마다 한 번씩) 쓰기 QPS를 낮출 수 있을 것이다.</p>
<p>얼마나 자주 보내면 좋을지는 사용자의 이동 속도 등 다양한 요건에 좌우된다. 가령 사용자가 꽉 막힌 도로 한가운데 있다면 GPS 위치 업데이트를 그렇게 자주 보낼 필요는 없을 것이다. 본 설계안의 경우에는 GPS 위치 변경 내역은 모아 두었다가 15초 마다 한 번씩 서버로 보낸다고 가정하겠다. 이렇게 하면 QPS는 20만으로 줄어든다.</p>
<p>최대 QPS는 평균치의 다섯 배 가량으로 가정하겠다. 따라서 위치 정보 갱신 요청 최대 QPS는 200,000 X 5 = 1백만이다.</p>
<h2 id="2단계-계략적-설계안-제시-및-동의-구하기">2단계 계략적 설계안 제시 및 동의 구하기</h2>
<p>이제 구글 맵에 대해 많은 것을 배웠으니 개략적 설계안을 제안해보도록 하자.</p>
<h3 id="개략적-설계안">개략적 설계안</h3>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/f7fad566-0134-4c38-b6ec-5a6474615c26/image.png" alt=""></p>
<p>이 개략적 설계안은 다음 세 가지 기능을 제공한다.</p>
<ol>
<li>위치 서비스</li>
<li>경로 안내 서비스</li>
<li>지도 표시 서비스</li>
</ol>
<h3 id="위치-서비스">위치 서비스</h3>
<p>위치 서비스는 사용자의 위치를 기록하는 역할을 담당한다.</p>
<p>본 기본 설계 안은 클라이언트가 t초마다 자기 위치를 전송한다고 가정한다. 여기서 t는 설정이 가능한 값이다. 이렇게 주기적으로 위치 정보를 전송하면 몇 가지 좋은 점이 있다.</p>
<p>첫 번째는 해당 데이터 스트림을 활용하여 시스템을 점차 개선할 수 있다는 점이다. 실시간 교통 상황을 모니터링하는 용도로 활용할 수 있고, 새로 만들어진 도로나 폐쇄된 도로를 탐지할 수도 있고, 사용자 행동 양태를 분석하여 개인화된 경험을 제공하는데 활용할 수도 있다.</p>
<p>두 번째 장점은 클라이언트가 보내는 위치 정보가 거의 실시간 정보에 가까우므로 ETA를 좀 더 정확하게 산출할 수 있고, 교통 상황에 따라 다른 경로를 안내할 수도 있다는 점이다.</p>
<p>하지만 사용자의 위치가 바뀔 때마다 그 즉시 서버로 전송해야만 할까 ? 아마 아닐 것이다. 위치 이력을 클라이언트에 버퍼링해 두었다가 일괄 요청하면 전송 빈도를 줄일 수 있다.</p>
<p>위치 변경 내역은 매초 측정하긴 하지만 서버로는 15초마다 한번 보내놓도록 설정해놓은 사례이다. 구글 맵과 같은 시스템은 이렇게 해서 위치 갱신 요청 빈도를 줄여도 여전히 많은 쓰기 요청을 처리해야만 한다. 따라서 아주 높은 쓰기 요청 빈도에 최적화되어 있고 규모 확장이 용이한 카산드라 같은 데이터베이스가 필요하다.
또한 카프카 같은 스트림 처리 엔진을 활용하여 위치 데이터를 로깅해야 할 수도 있다.</p>
<p>통신 프로토콜로는 HTTP를 keep-alive 옵션과 함께 사용하면 효율을 높일 수 있으므로 좋은 선택이 될 것이다.</p>
<pre><code>POST /v1/locations
인자:
locs: JSON으로 인코딩한 (위도, 경도, 시각) 순서쌍 배열</code></pre><h3 id="경로-안내-서비스">경로 안내 서비스</h3>
<p>이 컴포넌트는 A에서 B 지점으로 가는 합리적으로 빠른 경로를 찾아 주는 역할을 담당한다. 결과를 얻는 데 드는 시간 지연은 어느 정도 감내할 수 있다.
계산된 경로는 최단 시간 경로일 필요는 없으나 정확도는 보장되어야 한다.</p>
<p>앞에 그림에서도 봤듯이, 사용자가 보낸 경로 안내 HTTP 요청은 로드밸런서를 거쳐 서비스에 도달한다. 이 요청에는 출발지와 목적지가 인자로 전달된다.
해당 API 요청은 대략 다음과 같은 형태이다.</p>
<pre><code>GET /v1/nav?origin=1355+market+street, SF&amp;destination=Disneyland</code></pre><p>그 결과로 만들어지는 경로 안내 결과는 아래와 같다.</p>
<pre><code>{
  &#39;distance&#39;: {&#39;text&#39;: &#39;0.2 mi&#39;, &#39;value&#39;: 259},
  &#39;duration&#39;: {&#39;text&#39;: &#39;1 min&#39;, &#39;value&#39;: 83},
  &#39;end location&#39;: {&#39;lat&#39;: 37.4038943, &#39;Ing&#39;: -121.9410454},
  &#39;html instructions&#39;: &#39;Head &lt;b&gt;northeast&lt;/b&gt; on &lt;b&gt;Brandon St&lt;/b&gt;
  toward &lt;b&gt;Lumin Way&lt;/b&gt;&lt;div style=&quot;font-size:0.9em&quot;&gt; Restricted usage road&lt;/div&gt;&#39;,
  &#39;polyline&#39;: {&#39;points&#39;: &#39;_fhcFjbhgVuAwDsCal&#39;},
  &#39;start location&#39;: {&#39;lat&#39;: 37.4027165, &#39;Ing&#39;: -121.94358095,
  &#39;geocoded_waypoints&#39;: [
  {
  &quot;geocoder_status&quot; : &quot;OK&quot;,
  &quot;partial_match&quot; : true,
  &quot;place_id&quot; : &quot;ChIJwZNMtilfawwRO2aVVVX2yKg&quot;,
  &quot;types&quot; : [ &quot;locality&#39;, &quot;political&quot; ]
  },
  {
  &quot;geocoder status&quot; : &quot;OK&quot;&#39;
  &quot;partial_match&quot; : true,
  &quot;place_id&quot; : &quot;ChIJ3aPgQGtXawwRLYeiBMUi7bM&quot;
  &quot;types&quot; : [ &quot;locality&quot;, &quot;political&quot; ]
  }
  ],
  &#39;travel mode&#39;: &#39;DRIVING&#39;
}</code></pre><p>지금까지는 경로 재탐색이나 교통 상황 변화 같은 문제는 고려하지 않았따. 이런 문제들을 상세 설계를 진행하면서 살펴볼 적응형 ETA를 통해 해결할 수 있다.</p>
<h3 id="지도-표시">지도 표시</h3>
<p>대략적으로 시스템 규모를 추정할 때 알아본 바에 따르면, 확대 수준별로 한 벌씩 지도 타일을 저장하려면 수백 PB가 필요하다. 그 모두를 클라이언트가 가지고 있는 것은 실용적인 방법이 아니다. 클라이언트의 위치 및 현재 클라이언트가 보는 확대 수준에 따라 필요한 타일을 서버에서 가져오는 접근법이 바람직하다.</p>
<p>클라이언트가 지도 타일을 서버에서 가져오는 시나리오는 다음과 같이 생각해볼 수 있다.</p>
<ul>
<li>사용자가 지도를 확대 또는 이동시켜주며 주변을 탐색</li>
<li>경로 안내가 진행되는 동안 사용자의 위치가 현재 지도 타일을 벗어나 인접한 타일로 이동한다.</li>
</ul>
<p>다량의 지도 타일 데이터를 서버에서 효과적으로 가져오려면 어떻게 해야 하는지 알아보자.</p>
<p><strong>선택지 1</strong>
이 방법은 <strong>클라이언트의 위치, 현재 클라이언트가 보는 지도의 확대 수준에 근 거하여 필요한 지도 타일을 즉석에서 만드는 방안</strong>이다. 사용자 위치 및 확대 수준의 조합은 무한하다는 점을 감안하면, 이 방안에는 몇 가지 심각한 문제가 있다.</p>
<ul>
<li>모든 지도 타일을 동적으로 만들어야 하는 서버 클러스터에 심각한 부하가
걸린다.</li>
<li>캐시를 활용하기가 어렵다.</li>
</ul>
<p><strong>선택지2</strong>
다른 방법은 <strong>확대 수준별로 미리 만들어 둔 지도 타일을 클라이언트에 전달하 기만 하는 방법이다.</strong> 각 지도 타일이 담당하는 지리적 영역은 지오해싱 같은 분할법을 사용해 만든 고정된 사각형 격자로 표현되므로 정적이다. 클라이언트는 지도 타일이 필요할 경우 현재 확대 수준에 근거하여 필요한 지도 타일 집합을 결정한다. 그런 다음 그 각 위치를 지오해시 URL로 변환한다.
이렇게 미리 만들어 둔 정적 이미지는 CDN을 통해 다음 그림과 같이 서비스한다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/fb096e56-dfca-44a4-9747-7c45fbfc5f6e/image.png" alt=""></p>
<p>위 다이어그램에서 모바일 단말 사용자는 지도 타일 요청을 CDN에 보낸다. 해당 타일이 CDN을 통해 서비스된 적이 없는 경우, CDN은 원본 서버에서 해당 파일을 가져와 그 사본을 캐시에 보관한 다음 사용자에게 반환한다. </p>
<p>그 뒤로 다른 사용자가 같은 파일을 요청하면 CDN은 캐시에 보관한 사본을 서비스하 며, 원본 서버는 다시 접촉하지 않는다.</p>
<p>이 접근법은 규모 확장이 용이하고 성능 측면에서도 유리하다. 사용자에게 가장 가까운 POP(Point of Presence)에서 파일을 서비스하기 때문이다. 
<img src="https://velog.velcdn.com/images/ye_suri_106/post/41e20997-ab37-4ef1-9d8a-6978dddcf38e/image.png" alt=""></p>
<p>지도 타일은 정적이므로 캐시를 통해 서비스하기에 아주 적합하다.</p>
<p>모바일 데이터 사용량을 줄이는 것은 중요하다. 그러니 경로 안내를 진행하 는 동안 클라이언트가 일반적으로 필요로 할 데이터 양을 계산해 보자. </p>
<p>다음 쪽의 계산 결과는 클라이언트 측 캐시는 고려하지 않았음에 유의하자. 사람들은 같은 길을 일상적으로 이용하는 경향이 있으므로, 클라이언트에 캐시를 두 면 데이터 사용량을 많이 줄일 수 있을 것이다.</p>
<hr>
<blockquote>
<p><strong>데이터 사용량</strong></p>
</blockquote>
<p>사용자가 30km/h 속도로 이동 중이며, 한 이미지가 200mx 200m 영역을 표 현하도록 확대하여 지도를 표시하고 있는 상황이라고 하자. (이미지 하나 는 256x256픽셀 이미지이며 평균 이미지 크기는 100KB이다.) 1kmx 1km 영역을 표현하려면 이미지 25장이 필요하며, 이는 저장 용량으로 환산하면 2.5MB(25X 100KB)이다. 그러므로 30km/h 속도로 이동한다고 하면 시간당
75MB의 데이터가 소진되며(30x2.5MB), 이는 분당 1.25MB에 해당한다.</p>
<blockquote>
<p><strong>CDN을 통해 서비스 되는 트래픽 규모</strong></p>
</blockquote>
<p>앞서 언급하였듯, 우리는 매일 50억 분(minutes) 가량의 경로 안내를 처리한다. 이는 50억 x 1.25MB = 6.25PB/일에 해당하는 양이다. 그러므로 초당 전송해야 하는 지도 데이터의 양은 62,500MB가 된다.</p>
<p>CDN을 사용하면 이 지도 이미지는 전 세계에 흩어져 있는 POP를 통해 제공될 것이다. 전 세계에 200개의 POP가 있다고 하자. 각 POP는 수백 MB 정도의 트래픽만 처 리하면 될 것이다 (62,500/200)</p>
<hr>
<p>이제 앞에서 간단히 언급하기는 했지만 자세히 살펴보지는 않았던 마지막 한 가지 사항을 조금 더 구체적으로 알아보자. </p>
<p>클라이언트는 CDN에서 지도 타일을 가져올 URL을 어떻게 결정할까? 앞 절에서 살펴본 두 가지 선택지 가운데 후자를 이용할 것이다. 따라서 지도 타일은 이미 정의된 격자에 맞게 확대 수준별로 한 벌씩 미리 만들어 둔 것을 사용하게 된다.</p>
<p>지오해시를 사용해 격자를 나누므로 모든 격자는 고유한 지오해시 값을 갖는다. 따라서 위도/경도로 표현된 클라이언트의 위치 및 현재 지도 확대 수준  입력으로 화면에 표시할 지도 타일에 대응되는 지오해시는 아주 쉽게 계산해 낼 수 있다. </p>
<p>그 계산은 지도를 화면에 표시할 클라이언트가 수행하며, 그 해당 지오해시 및 URL로 CDN에서 지도 타일을 가져오면 된다. </p>
<p>예를 들어 구글 본사가 속한 지도 타일 이미지를 가져오는 URL은 다음과 비슷한 형태일 것이다. </p>
<pre><code>https://cdn.map-provider.com/tiles/9q9hvu.png </code></pre><p>앞에서 서술한 대로 지오해시 계산은 클라이언트가 수행해도 된다. 하지만 해당 알고리즘을 클라이언트에 구현해 놓으면 지원해야 할 플랫폼이 많을 때 문제가 될 수 있음은 주의하자. </p>
<p>모바일 앱 업데이트 배포는 시간도 많이 걸리고 때로는 위험한 프로세스다. 따라서 앞으로도 오랫동안 맵 타일 인코딩에는 지오해싱을 사용하리라는 보장이 있어야 한다. 혹시라도 다른 인코딩 방안으로 교체해야 한다면 많은 노력이 필요하며 위험성도 만만치 않을 것이다.</p>
<p>고려해 볼 만한 다른 한 가지 선택지는 주어진 위도 경도 및 확대 수준을 타일 URL로 변환하는 알고리즘 구현을 별도 서비스에 두는 것이다. 이 서비스는 위도, 경도, 현재 확대 수준을 입력으로 하여 타일 URL을 계산하는 역할만 담당하는 아주 간단한 서비스이다. 운영 유연성이 높아지는 이점이 있으므로 고려할 가치가 있다. 장단점을 면접관과 논의해 보면 아주 흥미로울 것이다.</p>
<p>사용자가 새로운 위치로 이동하거나 확대 수준을 변경하면 지도 타일 서비스는 어떤 타일이 필요한지 결정하여 해당 타일들을 가져오는 데 필요한 URL 집합을 계산해 낸다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/554f2b20-932a-4cb7-a799-e2f2813e3db3/image.png" alt=""></p>
<ol>
<li>모바일 사용자가 타일 URL들을 가져오기 위해 지도 타일 서비스를 호출한다. 이 요청은 로드밸런서로 전달된다.</li>
<li>로드 밸런서는 해당 요청을 지도 타일 서비스로 전달한다.</li>
<li>지도 타일 서비스는 클라이언트에 반환한다. 표시할 타일 하나와 8개의 주변 타일이 응답에 포함된다.</li>
<li>모바일 클라이언트는 해당 타일을 CDN을 통해 다운로드한다.</li>
</ol>
<p>지도 타일을 미리 계산해 두는 방법에 대해서는 상세 설계를 진행하면서 좀 더 자세히 알아본다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[1장 근접성 서비스 (2)]]></title>
            <link>https://velog.io/@ye_suri_106/1%EC%9E%A5-%EA%B7%BC%EC%A0%91%EC%84%B1-%EC%84%9C%EB%B9%84%EC%8A%A4-2</link>
            <guid>https://velog.io/@ye_suri_106/1%EC%9E%A5-%EA%B7%BC%EC%A0%91%EC%84%B1-%EC%84%9C%EB%B9%84%EC%8A%A4-2</guid>
            <pubDate>Tue, 21 May 2024 09:07:10 GMT</pubDate>
            <description><![CDATA[<hr>
<p>지난 포스팅에 이어 작성해보고자 한다 !</p>
<h1 id="3단계-상세-설계">3단계 상세 설계</h1>
<p>시스템의 전반적인 형태를 파악했으니, 이제 그 가운데 몇 부분을 좀 더 상세히 살펴보자.</p>
<ul>
<li>데이터베이스 규모 확장</li>
<li>캐시</li>
<li>지역 및 가용성 구역</li>
<li>시간대 또는 사업장 유형에 따른 검색</li>
<li>최종 아키텍처 다이어그램</li>
</ul>
<h2 id="데이터베이스의-규모-확장성">데이터베이스의 규모 확장성</h2>
<p>먼저 본 설계에서 가장 중요한 두 가지 테이블인 사업장 (business) 테이블과 지리 정보 색인 (geospatial index) 테이블의 규모 확장성을 살펴보겠다.</p>
<h3 id="사업장-테이블">사업장 테이블</h3>
<p>사업장 테이블 데이터는 한 서버에 담을 수 없을 수도 있다. 따라서 샤딩을 적용하기 좋은 후보다 이 테이블을 샤딩하는 가장 간단한 방법은 사업장 ID를 기준으로 하는 것이다. 모든 샤드에 부하를 고르게 분산할 수 있을 뿐 아니라 운영적 측면에서 보자면 관리하기도 쉽다.</p>
<h3 id="지리-정보-색인-테이블">지리 정보 색인 테이블</h3>
<p>지오해시나 쿼드트리 둘 다 널리 사용되지만 본 설계안에서는 좀 더 단순한 지오해시를 사용하도록 하겠다. 지오해시 테이블 구성 방법은 두 가지다.</p>
<p><code>방안 1</code> : 각각의 지오해시에 연결되는 모든 사업장 ID를 JSON 배열로 만들어 같은 열에 저장하는 방법이다. 따라서 특정한 지오해시에 속한 모든 사업장 ID가 한 열에 보관된다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/ce854a56-488a-40a2-bc36-c1d612bbc29e/image.png" alt=""></p>
<p><code>방안 2</code> : 같은 지오해시에 속한 사업장 ID 각각을 별도 열로 저장하는 방안이다. 따라서 사업장마다 한 개 레코드가 필요하다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/d528462e-27e8-4382-b9ab-1a29fdda8960/image.png" alt="">
다음 표는 이 테이블에 지오해시와 사업장 ID를 저장한 사례다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/d28c8714-c2d4-4e40-9255-6871b9fdbfe7/image.png" alt=""></p>
<p><em><strong>추천: 두 번째 방안을 추천하는데 그 이유는 다음과 같다.</strong></em></p>
<p>방안 1의 경우 사업장 정보를 갱신하려면 일단 JSON 배열을 읽은 다음 갱신 할 사업장 ID를 찾아내야 한다. 새 사업장을 등록해야 하는 경우에도 같은 사업장 정보가 이미 있는지 확인을 위해 데이터를 전부 살펴야 한다. 또한 병렬적으로 실행되는 갱신 연산 결과로 데이터가 소실되는 경우를 막기 위해 락을 사용해야 한다. 따져야 할 경계 조건이 너무 많다.</p>
<p>하지만 방안 2의 경우에는 지오해시와 사업장 ID 칼럼을 합친 (geohash, business_id)를 복합 키(compound key)로 사용하면 사업장 정보를 추가하고 삭제하기가 쉽다. 락을 사용할 필요가 없기 때문이다.</p>
<h3 id="지리-정보-색인의-규모-확장">지리 정보 색인의 규모 확장</h3>
<p>지리 정보 색인의 규모를 확장할 때 테이블에 보관되는 데이터의 실제 크기를 고려하지 않고 성급하게 샤딩 방법을 결정하는 실수를 혼히 저지르곤 한다. </p>
<p>지금 살펴보는 설계안의 경우 지리 정보 색인 테이블 구축에 필요한 전체 데이터 양은 많지 않다(쿼드트리 색인 전부를 보관하는데 불과 1.71G의 메모리가 필요하며 지오해시의 경우도 비슷하다). </p>
<p>따라서 색인 전부를 최신 데이터베이스 서버 한 대에 충분히 수용할 수 있다. 하지만 읽기 연산의 빈도가 높다면 서비 한 대의 CPU와 네트워크 대역폭으로는 요청 전부를 감당하지 못할 수도 있다.</p>
<p>그런 상황에서는 여러 데이터베이스 서버로 부하를 분산해야 한다.
관계형 데이터베이스 서버의 경우 부하 분산에는 두 가지 전략이 흔히 사용 된다. </p>
<ul>
<li>읽기 연산을 지원할 사본 데이터베이스 서버를 늘리는 것</li>
<li>다른 하나는 사딩을 적용하는 것이다.</li>
</ul>
<p>많은 엔지니어가 면접 시에 샤딩을 이야기하고 싶어 한다. 하지만 지오해시 테이블은 샤딩이 까다로우므로, 이야기하지 않는 것이 좋다. <strong>샤딩 로직을 애플 리케이션 계층(application layer)에 구현해야 하기 때문이다.</strong> 물론 샤닝이 유일한 선택지인 경우도 있다. 하지만 이번 설계안의 경우에는 데이터 전부를 서버 한 대에 담을 수 있으므로 여러 서버로 샤딩해야 할 강한 기술적 필요성은 없다.</p>
<p>따라서 이번 설계안에서는 읽기 부하를 나눌 사본 데이터베이스 서버를 두는 방법이 더 좋을 것이다. 개발도 쉽고 관리도 간편하다. 이런 이유에서 지리 정보 색인 테이블의 규모를 확장할 때는 사본 데이터베이스 활용을 추천한다.</p>
<h2 id="캐시">캐시</h2>
<p>캐시 계층 도입 전에는 이런 질문을 먼저 던져야 한다. 정말 필요한가? 정말 좋은 결과로 이어지리라는 결론을 쉽게 내리기는 어려울 것이다.</p>
<ul>
<li>처리 부하가 읽기 중심이고 데이터베이스 크기는 상대적으로 작아서 모든 데이터는 한 대 데이터베이스 서버에 수용 가능하다. 이 경우 질의문 처리 성능은 I/O에 좌우되지 않으므로 메모리 캐시를 사용할 때와 비슷하다.</li>
<li>읽기 성능이 병목이라면 사본 데이터베이스를 증설해서 읽기 대역폭을 늘릴 수 있다.</li>
</ul>
<p>면접관과 캐시 도입을 의논할 때는 벤치마킹과 미용 분석에 각별히 주의해야한다.</p>
<h3 id="캐시-키">캐시 키</h3>
<p>가장 직관적인 캐시 키는 사용자 위치의 위도 경도 정보다. 하지만 여기에는 몇가지 문제가 있다.</p>
<ul>
<li>사용자의 전화기에서 반환되는 위치 정보는 추정치일 뿐 아주 정확하진 않다. 설사 전혀 움직이지 않는다고 해도, 그 정보는 측정할 때마다 조금씩 달라질 것이다.</li>
<li>사용자가 이동하면 해당 위도 및 경도 정보도 미세하게 변경된다. 대부분의 애플리케이션에 이 변화는 아무런 의미가 없다.</li>
</ul>
<p>따라서 사용자의 위치 정보는 캐시 키로는 적절치 않다. 위치가 조금 달라지더라도 변화가 없어야 이상적이다.
지오해시나 쿼드트리는 이 문제를 효과적으로 해결한다. 같은 격자 내 모든 사업장이 같은 해시 값을 갖도록 만들 수 있기 때문이다.</p>
<h3 id="캐시-데이터-유형">캐시 데이터 유형</h3>
<p>아래 표 데이터는 캐시에 보관하면 시스템의 성능을 전반적으로 향상시킬 수 있다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/a490ab5b-d97c-4ae6-863c-0a7c55e8dd90/image.png" alt=""></p>
<blockquote>
<p>격자 내 사업장 ID</p>
</blockquote>
<p>사업장 정보는 상대적으로 안정적이라 자주 변경되지 않는다. 따라서 특정 지오해시에 해당하는 사업장 ID 목록을 미리 계산한 다음 레디스 같은 키-값 저장소에 캐시할 수 있다. 캐시를 활용하여 주변 사업장을 검색하는 구체적 사례를 한번 살펴보자.</p>
<ol>
<li>주어진 지오해시에 대응되는 사업장 목록은 다음 질의를 통해 구할 수 있다.<pre><code> SELECT business_id FRoM geohash_index WHERE geohash LIKE
{:geohash}%</code></pre></li>
<li>주어진 지오해시에 대응되는 사업장 목록을 요청 받으면 일단 캐시를 먼저 조회한다. 캐시에 없는 경우에는 위의 질의를 사용하여 사업장 목록을 데이터베이스에서 가져온 다음 캐시에 보관한다.<pre><code> public List&lt;String&gt; getNearbyBusinessIds (String geohash) {
     String cacheKey = hash(geohash);
     List&lt;String&gt; listOfBusinessIds = Redis.get(cacheKey);
     if (listOfBusinessIds == null ) {
         listOfBusinessIds = 위 SQL 질의를 실행하여 구한다;
         Cache.set(cacheKey, listOfBusinessIds, &quot;1d&quot;);
       }
       return listOfBusinessIds;
     }</code></pre></li>
</ol>
<p>새로운 사업장을 추가하거나, 기존 사업장 정보를 편집하거나, 아예 삭제하는 경우에는 데이터베이스를 갱신하고 캐시에 보관된 항목은 무효화 한다.
이 연산의 빈도는 상대적으로 낮아서 락을 사용할 필요가 없으므로, 사업장 정보 갱신은 구현하기 쉽다.</p>
<p>주어진 요구사항에 따르면 사용자는 다음 네 가지 검색 반경 가운데 하나를 고를 수 있다. 500m, 1km, 2km 그리고 5km. 이 검색 반경은 각각 지오해시 길이4,5,5,6에 해당한다. 이 각각에 대한 주변 사업장 검색 결과를 신속하게 제공하려면 이 세 가지 정밀도(4,5,6) 전부에 대한 검색 결과를 레디스에 캐시해 두어야 한다.</p>
<p>앞서 언급한 대로, 사업장 개수는 200m이고 각각의 사업장은 주어진 정밀도의 격자 하나에 대응될 것이다. 따라서 필요한 메모리 요구량은 다음과 같다.</p>
<ul>
<li>레디스 저장소에 값(value)을 저장하기 위한 필요 공간: 8바이트 X 200m x
3가지 정밀도 =~ 5GB</li>
<li>레디스 저장소에 키(key)를 저장하기 위한 필요 공간: 무시할 만한 수준</li>
<li>따라서 전체 메모리 요구량은 대략 5GB</li>
</ul>
<p>메모리 요구량으로만 보면 서비 한 대로도 충분할 것 같다. 하지만 고가용성 (high availability)을 보장하고 대륙 경계를 넘는 트래픽의 전송지연(latency) 을 방지하기 위해서는 레디스 클러스터를 전 세계에 각 지역별로 두고, 동일한 데이터를 각 지역에 중복해서 저장해 두어야 한다. 이런 종류의 레디스 캐시를 최종 설계 도면에서는 지오해시(Geohash)라고 지칭한다.</p>
<h2 id="지역-및-가용성-구역">지역 및 가용성 구역</h2>
<p>지금까지 살펴본 위치 기반 서비스는 여러 지역과 가용성 구역에 설치한다.</p>
<ul>
<li><p>사용자와 시스템 사이의 물리적 거리를 최소한으로 줄일 수 있다. 미국 서부(US West) 사용자는 해당 지역 데이터센터로 연결될 것이고, 유럽 사용자는 유럽 데이터센터로 연결될 것이다.</p>
</li>
<li><p>트래픽을 인구에 따라 고르게 분산하는 유연성을 확보할 수 있다. 일본과 한국 같은 지역은 인구 밀도가 아주 높나. 그런 국가는 별도 지역으로 빼거 나, 아예 한 지역 안에서도 여러 가용성 구역을 활용하여 부하를 분산시키는 것이 바람직할 수 있다.</p>
</li>
<li><p>그 지역의 사생활 보호법(privacy law)에 맞는 운영이 가능하다. 어떤 국가는 사용자 데이터를 해당 국가 이외의 지역으로 전송하지 못하도록 한다. 그런 경우에는 해당 국가를 별도 지역으로 빼고, 해당 국가에서 발생하는 모든 트래픽은 DNS 라우팅을 통해 해당 지역 내 서비스가 처리하도록 해야 한다.</p>
<h2 id="추가-질문-시간대-혹은-사업장-유형별-검색">추가 질문: 시간대, 혹은 사업장 유형별 검색</h2>
<p>면접관이 이런 추가 질문을 던질 수도 있다. 지금 영업 중인 사업장, 혹은 식당 정보만 받아오고 싶다면 어떻게 해야 하겠는가?</p>
<blockquote>
<p>지원자: 지오해시나 쿼드트리 같은 메커니즘을 통해 전 세계를 작은 격자들로
분할하면 검색 결과로 얻어지는 사업장 수는 상대적으로 적습니다. 그러니 일단은 근처 사업장 ID부터 전부 확보한 다음 그 사업장 정보를 전부 추출해서 영업시간이나 사업장 유형에 따라 필터링하면 되겠죠.
물론 그러려면 영업시간이나 사업장 유형 같은 정보는 사업장 정보 테
이분에 이미 보관되어 있어야 합니다.</p>
</blockquote>
</li>
</ul>
<h2 id="최종-설계도">최종 설계도</h2>
<p>이 모두를 한 도면에 정리하면 다음과 같다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/2f9e8324-9acc-483d-b527-8d171b2eaac7/image.png" alt=""></p>
<h3 id="주변-사업장-검색">주변 사업장 검색</h3>
<ol>
<li>엘프에서 주변 반경 500미터 내 모든 식당을 찾는 경우를 생각해 보자. 우선 클라이언트 앱은 사용자의 위치(위도와 경도 정보)와 검색 반경(500미터)을 로드밸린서로 전송한다.</li>
<li>로드밸런서는 해당 요청을 LBS로 보낸다.</li>
<li>주어진 사용자 위치와 반경 정보에 의거하여, LBS는 검색 요건을 만족할 지오해시 길이를 계산한다. 이전 표에 따르면 500미터 정밀도의 지오해시 길이는 6이다.</li>
<li>LBS는 인접한 지오해시를 계산한 다음 목록에 추가한다. 결과는 아래와 같을 것이다.<pre><code>list_of_geohashes = [my_geohash, neighbor1_geohash, neighbor2_
geohash, ..., neighbor8_geohash]</code></pre></li>
<li>List_of_geohashes 내에 있는 지오해시 각각에 대해 LBS는 &#39;지오해시&#39; 레디스 서버를 호출하여 해당 지오해시에 대응하는 모든 사업장 ID를 추출한다. 지오해시별로 사업장 ID 목록을 가져오는 연산을 병렬적으로 수행하면 검색 결과를 내는 지연시간을 줄일 수 있다.</li>
<li>반환된 사업장 ID들을 가지고 &#39;사업장 정보&#39; 레디스 서버를 조회하여 각 사업장의 상세 정보를 취득한다. 해당 상세 정보에 의거하여 사업장과 사용자 간 거리를 확실하게 계산하고, 우선순위를 매긴 다음 클라이언트 앱에 반환한다.<h3 id="사업장-정보-조회-갱신-추가-그리고-삭제">사업장 정보 조회, 갱신, 추가 그리고 삭제</h3>
모든 사업장 정보 관련 API는 LBS와는 분리되어 있다. 사업장 상세 정보를 확인하기 위해 사업장 정보 서비스는 우선 해당 데이터가 &#39;사업장 정보&#39; 레디스 서버에 기록되어 있는지 살핀다. </li>
</ol>
<p>캐시되어 있는 경우에는 해당 데이터를 읽어 클라이언트로 반환한다. 캐시에 없는 경우에는 데이터베이스 클러스터에서 사업장 정보를 읽어 캐시에 저장한 다음 반환한다. 뒤이은 요청은 캐시로 처리할 수 있도록 하기 위함이다.</p>
<p>새로 추가하거나 갱신한 정보는 다음날 반영된다는 것을 사업장과 합의하였으므로, 캐시에 보관된 정보 갱신은 밤사이 작업을 돌려서 처리할 수 있다.</p>
<h1 id="4단계-마무리">4단계 마무리</h1>
<p>이번 장에서 우리는 주변 검색 기능의 핵심인 근접성 서비스를 설계해 보았다.
지리 정보 색인 기법을 활용하는 전형적인 LBS 서비스다. 이번 장에서는 다음 몇 가지 색인 방안을 살펴보았다.</p>
<p>• 2차원 검색
• 균등 분할 격자
• 지오해시
• 퀴드트리
• 구글 S2</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[1장 근접성 서비스 (1)]]></title>
            <link>https://velog.io/@ye_suri_106/1%EC%9E%A5-%EA%B7%BC%EC%A0%91%EC%84%B1-%EC%84%9C%EB%B9%84%EC%8A%A4-1</link>
            <guid>https://velog.io/@ye_suri_106/1%EC%9E%A5-%EA%B7%BC%EC%A0%91%EC%84%B1-%EC%84%9C%EB%B9%84%EC%8A%A4-1</guid>
            <pubDate>Tue, 21 May 2024 08:17:39 GMT</pubDate>
            <description><![CDATA[<hr>
<p><code>근접성 서비스</code> 는 음식점, 호텔 등 현재 위치에서 가까운 시설을 찾는 데 이용되며, 엘프 앱의 경우는 주변에 있는 좋은 식당 검색, 구글 맵의 경우에는 가까운 k개 주유소 검색 등의 기능 구현에 이용된다.</p>
<h1 id="1단계-문제-이해-및-설계-범위-확정">1단계 문제 이해 및 설계 범위 확정</h1>
<h2 id="기능-요구사항">기능 요구사항</h2>
<ul>
<li>사용자의 위치(경도와 위도 쌍)와 검색 반경 정보에 매치되는 사업장 목록을 반환</li>
<li>사업장 소유주가 사업장 정보를 추가, 삭제, 갱신할 수 있도록 하되, 그 정보가 검색 결과에 실시간으로 반영될 필요는 없다고 가정</li>
<li>고객은 사업장의 상세 정보를 살필 수 있어야 함</li>
</ul>
<h2 id="비기능-요구사항">비기능 요구사항</h2>
<p>방금 살펴본 사업 요구사항으로부터 다음과 같은 비기능 요구사항을 도출할 수 있다.</p>
<ul>
<li>낮은 응답 지연 (latency) : 사용자는 주변 사업장을 신속히 검색할 수 있어야 한다.</li>
<li>데이터 보호 (data privacy) : 사용자의 위치는 민감한 정보이기 때문에, 위치 기반 서비스 (LBS) 를 설계할 때는 언제나 데이터 사생활 보호 법안을 준수하도록 해야 한다.</li>
<li>고가용성 및 규모 확장성 요구사항 : 인구 밀집 지역에서 이용자가 집중되는 시간에 트래픽이 급증해도 감당할 수 있도록 시스템을 설계해야 한다.</li>
</ul>
<h3 id="개략적-규모-추정">개략적 규모 추정</h3>
<p>시스템의 규모가 어느 정도이며 어떤 수준의 도전적 과제를 해결해야 하는지 결정하기 위해, 개략적인 추정을 해보도록 하자.</p>
<p>DAU는 1억명, 등록된 사업장 수는 2억이라고 하자.</p>
<ul>
<li>한 사용자는 하루에 5회 검색을 시도한다고 가정한다.<ul>
<li>QPS (Query per second) : (1억 X 5) / 10^5 = 5,000</li>
</ul>
</li>
</ul>
<h1 id="2단계-개략적-설계안-제시-및-동의-구하기">2단계 개략적 설계안 제시 및 동의 구하기</h1>
<p>이번 절에서는 다음 내용을 논의한다.</p>
<ul>
<li>API 설계</li>
<li>개략적 설계안</li>
<li>주변 사업장 검색 알고리즘</li>
<li>데이터 모델</li>
</ul>
<h2 id="api-설계">API 설계</h2>
<p>RESTful API 관례에 따르는 간단한 API를 만들어 보도록 하겠다.</p>
<p><code>GET /V1/search/nearby</code></p>
<ul>
<li>특정 검색 기준에 맞는 사업장 목록을 반환하는 API</li>
<li>보통 페이지 단위로 나눠 반환</li>
<li>request params : latitude(검색할 위도, decimal), longitude(검색할 경도, decimal), radius(선택적 인자, 생략할 경우 기본값은 5000M, int)</li>
<li>response body<pre><code>{
  &quot;total&quot; : 10,
  &quot;businesses&quot; : [{business object}]
}</code></pre></li>
<li>위의 business object, 즉 각 사업장을 표현하는 객체는 검색 결과 페이지에 표시된 모든 정보를 포함한다.</li>
</ul>
<h3 id="사업장-관련-api">사업장 관련 API</h3>
<p>다음 표는 사업장 객체 관련 API 목록이다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/994a8e26-6c76-4993-8967-5bc72522e5ba/image.png" alt=""></p>
<h2 id="데이터-모델">데이터 모델</h2>
<p>이번 절에서는 읽기/쓰기 비율 및 스키마 설계에 대해 알아본다.</p>
<h3 id="읽기쓰기-비율">읽기/쓰기 비율</h3>
<p>읽기 연산은 굉장히 자주 수행되는데, 다음 두 기능의 이용 빈도가 높기 때문이다.</p>
<ul>
<li>주변 사업장 검색</li>
<li>사업장 정보 확인</li>
</ul>
<p>한편 쓰기 연산 실행 빈도는 낮은데, 사업장 정보를 추가하거나 삭제, 편집하는 행위는 빈번하지 않기 때문이다.
읽기 연산이 압도적인 시스템에는 MySQL 같은 관계형 데이터베이스가 바람직할 수 있다.</p>
<h3 id="데이터-스키마">데이터 스키마</h3>
<p>이 시스템의 핵심되는 테이블은 business 테이블과 지리적 위치 색인 테이블이다.</p>
<h2 id="개략적-설계">개략적 설계</h2>
<p>이 시스템은 위치 기반 서비스와 사업장 관련 서비스 두 부분으로 구성된다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/105dc4f4-de58-49e3-a81c-da5c4bdd78f0/image.png" alt=""></p>
<h3 id="로드밸런서">로드밸런서</h3>
<p>로드밸런서는 유입 트래픽을 자동으로 여러 서비스에 분산시키는 컴포넌트이다. 통상적으로 로드밸런서를 사용하는 회사는 로드밸런서에 단일 DNS 진입점(entry point)을 지정하고, URL 경로를 분석하여 어느 서비스에 트래픽을 전달할지 결정한다.</p>
<h3 id="위치-기반-서비스-lbs">위치 기반 서비스 (LBS)</h3>
<p>LBS는 시스템의 핵심 부분으로, 주어진 위치와 반경 정보를 이용해 주변 사업장을 검색한다.</p>
<ul>
<li>쓰기 요청이 없는, 읽기 요청만 빈번하게 발생하는 서비스이다.</li>
<li>QPS가 높다. 특히 특정 시간대의 인구 밀집 지역일수록 그 경향이 심하다.</li>
<li>무상태 서비스이므로 수평적 규모 확장이 쉽다.</li>
</ul>
<h3 id="사업장-서비스">사업장 서비스</h3>
<ul>
<li>사업장 소유주가 사업장 정보를 생성, 갱신, 삭제한다. 기본적으로 쓰기 요청이며, QPS는 높지 않다.</li>
<li>고객이 사업장 정보를 조회한다. 특정 시간대에 QPS가 높아진다.</li>
</ul>
<h3 id="데이터베이스-클러스터">데이터베이스 클러스터</h3>
<p>데이터베이스 클러스터는 주-부 데이터베이스 형태로 구성할 수 있다. 해당 구성에서 주 데이터베이스는 쓰기 요청을 처리하며, 부 데이터베이스, 즉 사본 데이터베이스는 읽기 요청을 처리한다.
데이터는 일단 주 데이터베이스에 기록된 다음에 사본 데이터베이스로 복사된다. 복제에 걸리는 시간 지연 때문에 주 데이터베이스 데이터와 사본 데이터베이스 데이터 사이에는 차이가 있을 수 있다.</p>
<h3 id="주변-사업장-검색-알고리즘">주변 사업장 검색 알고리즘</h3>
<p>실제로 많은 회사가 레디스 지오해시나 PostGIS 확장을 설치한 포스트그레스 데이터베이스를 활용한다.
이런 데이터베이스의 이름을 나열하기보다는 지리적 위치 색인이 어떻게 동작하는지 설명함으로써 문제 풀이 능력과 기술적 지식을 갖추었음을 보이는 것이 좋다.</p>
<p>다음으로는 주변 사업장 검색 방법들을 살펴볼 것이다. 몇 가지 방안을 훑어보고 그 이면의 사고 프로세스를 검토한 다음, 각 방안에 어떤 타협적 측면이 존재하는지 논의할 것이다.</p>
<blockquote>
<p>방안 1: 2차원 검색</p>
</blockquote>
<p>주어진 반경으로 그린 원 안에 놓인 사업장을 검색하는 방법이다.
이 절차를 유사 SQL 질의문으로 옮기면 다음과 같다.</p>
<pre><code>SELECT business_id, latitude, longitude,
FROM business
WHERE (latitude BETWEEN {:my_lat} - radius AND {:my_lat} + redius)
AND
(longitude BETWEEN {:my_long} - radius AND {:my_long} + radius)</code></pre><p>이 질의는 근데 데이터를 전부 읽어야 하므로 효율적이지 않다. 데이터가 2차원적이므로 칼럼별로 가져온 결과도 엄청난 결과이다.</p>
<p>이 방안의 문제는 데이터베이스 색인으로는 오직 한 차원의 검색 속도만 개선할 수 있다는 것이다.</p>
<p>2차원 데이터를 한 차원에 대응시킬 방법이 있을까 ? 있다.
그전에 우선 색인을 만드는 방법들부터 살펴보자.
크게 보자면 지리적 정보에 색인을 만드는 방법은 두 종류다. 아래 그림 중 강조 표시한 알고리즘은 업계에서 널리 사용되는 것들로, 이번 장에서 살펴볼 내용이다.</p>
<ul>
<li>해시 기반 방안 : 균등격자, 지오해시, 카르테시안 계층</li>
<li>트리 기반 방안 : 퀴드트리, 구글 S2, R 트리
<img src="https://velog.velcdn.com/images/ye_suri_106/post/dbf90167-ef6b-45ec-ac17-3cea8cd250eb/image.png" alt=""></li>
</ul>
<p>각 색인법의 구현 방법은 서로 다르지만 개략적 아이디어는 같다. 즉, <strong>지도를 작은 영역으로 분할하고 고속 검색이 가능하도록 색인을 만드는 것</strong>이다.</p>
<blockquote>
<p>방안 2: 균등 격자</p>
</blockquote>
<p>그림과 같이 작은 격자 또는 구획으로 나누는 단순한 접근법이다. 이렇게 하면 하나의 격자는 여러 사업장에 담을 수 있고, 하나의 사업장은 오직 한 격자 안에만 속하게 된다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/0d9ef5ff-66a2-4225-93c2-7a70f42de904/image.png" alt=""></p>
<p>이 방법은 동작은 하지만 중요한 문제가 있다. 사업장의 분포가 균등하지 않다는 것이다.</p>
<blockquote>
<p>방안 3: 지오해시</p>
</blockquote>
<p>지오해시는 균등 격자보다 나은 방안이다. 지오해시는 2차원의 위도 경도 데이터를 1차원 문자열로 변환한다.
지오해시 알고리즘은 비트를 하나씩 늘려가면서 재귀적으로 세계를 더 작은 격자로 분할해 나간다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/54db11c5-87b1-4191-9212-353ddd361598/image.png" alt=""></p>
<p>위처럼 계속해서 분할해나가는 것을 원하는 정밀도가 얻어질 때까지 반복한다.
최적 정밀도를 정하는 것은 사용자가 지정한 반경으로 그린 원을 덮는, 최소 크기 격자를 만드는 지오해시 길이를 구해야 한다.</p>
<p>지오해시는 총 12단계 정밀도를 갖고, 지오해시 길이에 따른 격자 너비 X 높이가 있다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/26d8e359-3d23-4747-ae5c-1fe8c5c16d2a/image.png" alt=""></p>
<p>아래 표는 위의 지오해시 길이와 반경 사이의 관계를 보여준다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/bdc4da9c-ad25-43fb-9666-2ef3e2daa72f/image.png" alt=""></p>
<h3 id="격자-가장자리-관련-이슈">격자 가장자리 관련 이슈</h3>
<p>지오해시는 해시값의 공통 접두어(prefix)가 긴 격자들이 서로 더 가깝게 놓이도록 보장한다. 그림을 보면 인접한 모든 격자가 공통 접두어 9q8zn을 갖고 있음을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/d66186bf-8f3d-40c0-b171-baf8471e405d/image.png" alt=""></p>
<blockquote>
<p>격자 가장자리 이슈 1</p>
</blockquote>
<p>하지만 그 역은 참이 아니다. 가렁 아주 가까운 두 위치가 어떤 공통 접두어도 갖지 않는 일이 발생할 수 있다. 두 지점이 적도의 다른 쪽에 놓이거나, 자오선상의 다른 반쪽에 놓이는 경우다. </p>
<p>예를 들어 프랑스 라 호슈 샬레라는 곳은 지오해시 u000으로 표현되는데, 이 위치는 지오해시 ezzz 값을 갖는 포메홀(Pomerol)이라는 지역에서 불과 30km 떨어져 있다. 이 두 지오해시 사이에는 어떤 공통 접두어도 없다.</p>
<p>이 문제점 때문에 아래와 같이 단순한 접두어 기반 SQL 질의문을 사용하면 주변 모든 사업장을 가져올 수 없다.</p>
<pre><code>    SELECT * FROM geohash_index WHERE geohash LIKE &#39;9q8zn%&#39;</code></pre><blockquote>
<p>격자 가장자리 이슈 2</p>
</blockquote>
<p>또 다른 문제점은 두 지점이 공통 접두어 길이는 길지만 서로 다른 격자에 놓이는 경우이다.
가장 흔히 사용되는 해결책은 현재 격자를 비롯한 인접한 모든 격자의 모든 사 업장 정보를 가져오는 것이다. 특정 지오해시의 주변 지오해시를 찾는 것은 상수 시간(constant time)에 가능한 연산이.</p>
<blockquote>
<p>표시할 사업장이 충분하지 않은 경우</p>
</blockquote>
<p>이제 보너스 문제 하나를 더 살펴보자. 현재 격자와 주변 격자를 다 살펴보아도 표시할 사업장을 충분히 발견할 수 없는 경우에는 어떻게 해야 하는가?</p>
<p><code>선택지 1</code>: 주어진 반경 내 사업장만 반환한다. 이 방안은 구현하기 쉽지만 단점 도 분명하다. 사용자의 욕구를 만족하기 충분한 수의 사업장 정보를 반환하지 못한다.</p>
<p><code>선택지 2</code>: 검색 반경을 키운다. 지오해시 값의 마지막 비트를 삭제하여 얻은 새 지오해시 값을 사용해 주변 사업장을 검색하는 것이다. 그래도 충분한 사업장 이 없을 경우 또 한 비트를 지워서 범위를 다시 확장한다. 이를 반복하면 원하는 수 이상의 사업장을 얻을 때까지 격자 크기는 확장된다. 아래 그림은 이 과정을 개념적으로 요약한 다이어그램이다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/7156360f-fb9f-4b6d-a422-68700e196c6d/image.png" alt=""></p>
<blockquote>
<p>방안 4: 쿼드트리</p>
</blockquote>
<p>또 한 가지 널리 사용되는 해결책은 쿼드트리(quadltree)다. 쿼드트리는 격자의 내용이 특정 기준을 만족할 때까지 2차원 공간을 재귀적으로 사분면 분할 하는데 흔히 사용되는 자료 구조다. </p>
<p>예를 들자면 격자에 담긴 사업장 수가 100이하가 될 때까지 분할하는 것이다. 여기서 제시한 100이라는 숫자는 예일 뿐이며, 실제 수치는 사업적 필요에 따라 결정하면 된다. 쿼드트리를 사용한다는 것은 결국 질의에 답하는 데 사용될 트리 구조를 메모리 안에 만드는 것이다.</p>
<p>쿼드트리는 메모리 안에 놓이는 자료 구조일 뿐 데이터베이스가 아니라는 것에 유의하자. 이 자료 구조는 각각의 LBS 서버에 존재해야 하며, 서버가 시작하는 시점에 구축된다.</p>
<p>아래 그림은 세계를 쿼드트리를 사용해 분할하는 과정을 개념적으로 요약한 것이다. 전 세계에 200m(m = million, 즉 백만)개의 사업장이 있다고 가정했다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/986339d3-e89c-4de9-ba7e-c01ceedfdea4/image.png" alt="">
<img src="https://velog.velcdn.com/images/ye_suri_106/post/76d14f9d-fff9-4740-9062-2714e94bc525/image.png" alt=""></p>
<p>이 트리의 루트 노드 (root node)는 세계 전체 지도를 나타낸다. 이 루트 노드를 사분면 각각을 나타내는 하위 노드로, 어떤 노드의 사업장도 100개를 넘지 않을 때까지 재귀적으로 분할한다.
그 과정을 의사 코드(pseudo code)로 나타내면 다음과 같다.</p>
<pre><code>public void buildQuadtree (TreeNode node) {
    if (countNumber0fBusinessesInCurrentGrid(node) &gt; 100) {
    node.subdivide();
    for (TreeNode child : node.getChildren ()) {
        buildQuadtree(child);
    }
  }
}</code></pre><h3 id="쿼드트리-전부를-저장하는-데-얼마나-많은-메모리가-필요한가">쿼드트리 전부를 저장하는 데 얼마나 많은 메모리가 필요한가?</h3>
<p>이 질문에 답하려면 어떤 데이터가 쿼드트리에 보관되는지 살펴봐야 한다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/99ab8fa7-8441-4260-b5f1-33839a39ba78/image.png" alt="">
트리 구축 프로세스가 한 격자에 허용되는 사업장 수의 최댓값에 좌우되기는 하지만 그 값은 트리 안에 저장하지 않아도 된다. 데이터베이스 레코드가 이미 그 최댓값을 고려하여 분할되어 있기 때문이다.</p>
<h3 id="쿼드트리로-주변-사업장을-검색하려면">쿼드트리로 주변 사업장을 검색하려면?</h3>
<ol>
<li>메모리에 쿼드트리 인덱스를 구축한다.</li>
<li>검색 시작점이 포함된 말단 노드를 만날 때까지, 트리의 루트 노드부터 탐색한다. 해당 노드에 100개 사업장이 있는 경우에는 해당 노드만 반환한다.
그렇지 않은 경우에는 충분한 사업장 수가 확보될 때까지 인접 노드도 추가한다.</li>
</ol>
<h3 id="쿼드트리-운영-시-고려사항">쿼드트리 운영 시 고려사항</h3>
<p>앞서 설명한 대로, 200m개 사업장을 갖는 쿼드트리를 구축하는 데는 몇 분이 소요된다. 따라서 <strong>서버를 시작하는 순간에 트리를 구축하면 서버 시작 시간이 길어질 수 있다는 점을 따져 봐야 한다.</strong> 이것은 운영상 중요한 문제다. <strong>쿼드트리를 만들고 있는 동안 서버는 트래픽을 처리할 수 없기 때문이다.</strong> 따라서 새로운 버전의 서버 소프트웨어를 릴리스 할 때는 동시에 너무 많은 서버에 배포 하지 않도록 조심해야 한다. 그렇게 해야만 서버 클러스터의 상당 부분이 동시에 꺼져서 서비스 품질이 저하되는 일을 막을 수 있다. 
청/녹 배포(blue/green deployment)방안, 즉 프로덕션 환경의 절반가량을 항상 실제 서비스가 아닌 신규 이미지 테스트에만 사용하고, 테스트에 통과한 경우 네트워크 설정을 조정하여 테스트 환경과 실제 서비스 환경을 맞바꾸는 배포 전략을 택하는 경우에는 새 서버 소프트웨어를 테스트 환경의 모든 서버에 동시 배포하면 200m개 사업장 정보를 데이터베이스에서 동시에 읽게 되어 시스템에 큰 부하가 가 해질 수 있다는 점을 유의해야 한다. 사용 불가능한 배포 방안은 아니지만 설계가 복잡해질 수 있다. 면접 시에 반드시 그 사실을 언급하도록 하자.</p>
<p>운영에 고려할 또 한 가지는 시간이 흘러 사업장이 추가/삭제되었을 때 퀴드 트리를 갱신하는 문제다. 가장 쉬운 방법은 점진적으로 갱신하는 것이다. 다시 말해 클러스터 내에 모든 서버를 한 번에 갱신하는 대신 점진적으로 몇 개씩만 갱신하는 것이다. 하지만 그러나 보면 짧은 시간 동안이지만 낡은 데이터가 반환될 수 있다. 그러나 요구사항이 엄격하지 않다면 그 정도는 일반적으로 용인 할 수 있다. 새로 추가하거나 갱신한 사업장 정보는 다음날 반영된다는 식의 협약을 맺어 놓으면 더욱 사소한 문제가 된다. 밤 사이에 캐시를 일괄 갱신하면 된다는 뜻이다. 이 접근법의 한 가지 문제는 수많은 키(key)가 한 번에 무효화되어 캐시 서버에 막대한 부하가 가해질 수 있다는 점이다.</p>
<p>쿼드트리를 실시간으로 갱신하는 것도 가능하다. 하지만 그러면 설계는 복잡해진다. 여러 스레드가 퀴드트리 자료 구조를 동시 접근하는 경우에는 더욱 그렇다. 그런 상황을 처리하려면 모종의 락(lock) 메커니즘을 사용해야 하기 때문이다.</p>
<blockquote>
<p>방안 5: 구글 S2</p>
</blockquote>
<p>이 부분은 면접 시 언급하기 복잡하기 때문에 생략.</p>
<h3 id="추천">추천</h3>
<p>주변 사업장을 효과적으로 검색하는 데 사용할 수 있는 솔루션 몇 가지를 지금까지 살펴보았다. 지오해시, 쿼드트리, S2 등이 있고 아래 표는 어느 회사가 어떤 기술을 택했는지 요약한 것이다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/0e01d4ea-e209-4403-956e-c5044409e7e6/image.png" alt="">
면접 시에는 지오해시나 쿼드트리 가운데 하나를 선택하길 추천한다. S2는 면접 시간 동안에 분명하게 설명하기 까다롭다.</p>
<h2 id="지오해시-vs-쿼드트리">지오해시 VS 쿼드트리</h2>
<h3 id="지오해시">지오해시</h3>
<ul>
<li>구현과 사용이 쉽다. 트리를 구축할 필요가 없다.</li>
<li>지정 반경 이내 사업장 검색을 지원한다.</li>
<li>정밀도를 고정하면 적자 크기도 고정된다. 인구 밀도에 따라 동적으로 격자 크기를 조정할 수는 없다. 그러려면 더욱 복잡한 논리를 적용해야 한다.</li>
<li>색인 갱신이 쉽다. 예를 들어 색인에서 사업장 하나를 삭제하러면, 지오해시 값과 사업장 식별자가 같은 열 하나를 제거하기만 하면 된다. 그림과 같이 하면 된다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/5ab23c1a-25fd-471b-81e5-d0acda21695c/image.png" alt=""></li>
</ul>
<h3 id="쿼드트리">쿼드트리</h3>
<ul>
<li>구현하기가 살짝 더 까다롭다. 트리 구축이 필요해서다.</li>
<li>k번째로 가까운 사업장까지의 목록을 구할 수 있다. 때로 사용자는 검색 반 경에 상관없이 내 위치에서 가까운 사업장 K개를 찾기를 원한다. 예를 들어 여행 도중에 차량 연료가 떨어져 간다면 거리에 상관없이 가까운 주유소를 찾아야 한다. 비록 가까이에 있지 않더라도, 가장 근거리의 1개의 주유소만 찾으면 된다. 그런 연산에는 쿼드트리가 적당한데 하위 노드 분할 과정이 숫자 k에 기반하는 데다 k개 사업장을 찾을 때까지 검색 범위를 자동으로 조정할 수 있기 때문이다.
인구 밀도에 따라 격자 크기를 동적으로 조정할 수 있다(그림 1.15의 덴버
지역 사례 참조).</li>
<li>지오해시보다 색인 갱신은 까다롭다. 퀴드트리는 말 그대로 트리 형태 자료구조다. 사업장 정보를 삭제하려면 루트 노드부터 말단 노드까지 트리를 순회해야 한다. 예를 들어 ID= 2인 사업장을 삭제하려면 그림과 같이 루트 노드부터 말단까지 탐색해야 한다. 따라서 색인 갱신 시간 복잡도는 O(logn)이다. 한편 다중 스레드(multi-thread)를 지원해야 하는 경우에는 그 구현은 더욱 복잡해질 수 있다. 락(lock)을 사용해야 하기 때문이다.
또한 트리의 균형을 다시 맞추는 소위 리밸런싱(relalancing)이 필요하다면 구현은 더욱 복잡해진다. 가렁 말단 노드에 새로운 사업장을 추가할 수 없는 경우에는 리밸런싱을 해야 한다. 한 가지 해결책은 말단 노드가 담당해야 하는 구간의 크기를 필요한 양보다 크게 잡는 것이다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/890f96f0-2d81-4b8c-b53f-932e26c3e836/image.png" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[4장 처리율 제한 장치의 설계]]></title>
            <link>https://velog.io/@ye_suri_106/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</link>
            <guid>https://velog.io/@ye_suri_106/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</guid>
            <pubDate>Tue, 12 Mar 2024 02:25:02 GMT</pubDate>
            <description><![CDATA[<p>네트워크 시스템에서 <strong>처리율 제한 장치(rate limiter)</strong>는 <strong>클라이언트 또는 서비스가 보내는 트래픽의 처리율(rate)을 제어하기 위한 장치입니다.</strong></p>
<p>HTTP를 예로 들면 이 장치는 특정 기간 내에 전송되는 클라이언트의 요청 횟수를 제한합니다. API 요청 횟수가 제한 장치에 정의된 임계치를 넘어서면 추가로 도달한 모든 호출은 처리가 중단(block)됩니다. 몇 가지 사례는 아래와 같습니다.</p>
<ul>
<li>사용자는 초당 2회 이상 새 글을 올릴 수 없다.</li>
<li>같은 IP 주소로는 하루에 10개 이상의 계정을 생성할 수 없다.</li>
<li>같은 디바이스로는 주당 5회 이상 리워드를 요청할 수 없다.</li>
</ul>
<p>이번 장에서는 바로 이 처리율 제한 장치를 설계하는 방식에 대해 알아봅니다. 설계에 앞서, API에 처리율 제한 장치를 두면 좋은 점은 살펴볼까요 ?</p>
<ul>
<li>Dos(Denial of Service) 공격에 의한 자원 고갈을 방지할 수 있다. 대형 IT 기업들이 공개한 거의 대부분의 API는 어떤 형태로든 처리율 제한 장치를 가지고 있다. 예를 들어 트위터는 3시간 동안 300개의 트윗만 올릴 수 있도록 제한하고, 구글 독스는 API는 사용자당 분당 300회의 read 요청만 허용한다. 처리율 제한 장치는 추가 요청에 대해선 처리를 중단함으로써 Dos 공격을 방지한다.</li>
<li>비용을 절감한다. 추가 요청에 대한 처리를 제한하면 서버를 많이 두지 않아도 되고, 우선순위가 높은 API에 더 많은 자원을 할당할 수 있다. 아울러 처리율 제한은 제3자(third-party) API에 사용료를 지불하고 있는 회사들에게는 아주 중요하다. 예를 들어, 신용을 확인하거나, 신용카드 결제를 하거나, 건강 상태를 확인하거나 하기 위해 호출하는 API에 대한 과금이 횟수에 따라 이루어진다면, 그 횟수를 제한할 수 있어야 비용을 절감할 수 있을 것이다.</li>
<li>서버 과부하를 막는다. 봇(bot)에서 오는 트래픽이나 사용자의 잘못된 이용 패턴으로 유발된 트래픽을 걸러내는데 처리율 제한 장치를 활용할 수 있다.</li>
</ul>
<h2 id="1단계---문제-이해-및-설계-범위-확정">1단계 - 문제 이해 및 설계 범위 확정</h2>
<p>처리율 제한 장치를 구현하는데는 여러 가지 알고리즘을 사용할 수 있습니다. 하지만 그 각각은 고유의 장단점을 가지고 있기 때문에, 면접관과 소통해서 여러 조건들을 확인하고 그에 맞게 어떤 제한 장치를 구현해 할지 결정해야 합니다.</p>
<p>다음과 같은 요구사항이 주어졌다고 가정해봅시다.</p>
<h3 id="요구-사항">요구 사항</h3>
<ul>
<li>설정된 처리율을 초과하는 요청은 정확하게 제한한다.</li>
<li>낮은 응답시간 - 이 처리율 제한 장치는 HTTP 응답시간에 나쁜 영향을 주어서는 안된다.</li>
<li>가능한 적은 메모리를 써야한다.</li>
<li>분산형 처리율 제한 - 하나의 처리율 제한 장치를 여러 서버나 프로세스에서 공유할 수 있어야 한다.</li>
<li>예외 처리 - 요청이 제한되었을 때는 그 사실을 사용자에게 분명하게 보여주어야 한다.</li>
<li>높은 결함 감내성 - 제한 장치에 장애가 생기더라도 전체 시스템의 영향을 주어서는 안된다.</li>
</ul>
<h2 id="2단계---개략적-설계안-제시-및-동의-구하기">2단계 - 개략적 설계안 제시 및 동의 구하기</h2>
<p>일단 일을 복잡하게 만들기 보다는 기본적인 클라이언트-서버 통신 모델을 사용해봅시다.</p>
<h3 id="처리율-제한-장치는-어디에-둘-것인가">처리율 제한 장치는 어디에 둘 것인가?</h3>
<p>직관적으로 보자면 이 장치는 당연히 클라이언트 측에 둘 수도 있고, 서버 측에 둘 수도 있습니다.</p>
<ul>
<li>클라이언트 측에 둔다면<ul>
<li>일반적으로 클라이언트는 처리율 제한을 안정적으로 걸 수 있는 장소가 못 됩니다. 왜냐하면 클라이언트 요청은 쉽게 위변조가 가능하기 때문입니다. 모든 클라이언트의 구현을 통제하는 것도 어려울 수 있습니다.</li>
</ul>
</li>
<li>서버 측에 둔다면<ul>
<li>서버 측에 둔다는 설계는 또 크게 두 가지로 나눌 수 있습니다. API 서버에 둘 수 있고, 또 다른 방법은 처리율 제한 미들웨어를 만들어 해당 미들웨어가 API 서버 앞단에 있어서 API 서버로 가는 요청을 통제하도록 하는 것입니다.</li>
<li>API 서버의 처리율이 초당 2개의 요청으로 제한된 상황이라면, 클라이언트가 3번째 요청을 보냈을 때 앞에 2개의 요청은 API 서버로 전송이 되지만 세번째 요청은 처리율 제한 미들웨어에 의해 가로막히고 클라이언트트는 HTTP 상태 코드 429(too many requests)가 날아갈 것입니다.</li>
</ul>
</li>
</ul>
<p>폭넓게 채택된 기술인 클라우드 마이크로 서비스의 경우, 처리율 제한 장치는 보통 <strong>API 게이트웨이</strong>라 불리는 컴포넌트에 구현됩니다. </p>
<p>API 게이트웨이는 처리율 제한, SSL 종단, 사용자 인증, IP 허용 목록 관리 등을 지원하는 완전 위탁관리형 서비스, 즉 클라우드 업체가 유지 보수를 담당하는 서비스입니다. 하지만 일단은 API 게이트웨이가 처리율 제한을 지원하는 미들웨어라는 점만 기억하면 됩니다.</p>
<p>처리율 제한 기능을 설계할 때 중요하게 따져야하는 또 하나가 바로 어디에 둘 것인지 입니다. 서버에 둬야 하는지, 게이트웨이에 두어야하는지.. 정답은 당연히 없습니다. 현재 기술 스택이나 엔지니어링의 인력, 우선순위, 목표에 따라 달라지는데 다만 일반적으로 적용될 수 있는 몇 가지 지침은 있습니다 !</p>
<ul>
<li>프로그래밍 언어, 캐시 서비스 등 현재 사용하고 있는 기술 스택을 점검해봅니다. 현재 사용하는 프로그래밍 언어가 서버 측 구현을 지원하기 충분할 정도로 효율이 높은지 확인해봐야 합니다.</li>
<li>사업 필요에 맞는 처리율 제한 알고리즘을 찾아야 합니다. 서버 측에서 모든 것을 구현하기로 했으면, 알고리즘은 자유롭게 선택 가능하지만 제 3 사업자가 제공하는 게이트웨이를 사용하기로 했다면 선택지는 제한될 수 있습니다.</li>
<li>여러분의 설계가 마이크로서비스에 기반하고 있고, 사용자 인증이나 IP 허용목록 관리 등을 처리하기 위해 API 게이트웨이를 이미 설계에 포함시켰다면 처리율 제한 기능 또한 게이트웨이에 포함시켜야 할 수도 있습니다.</li>
<li>처리율 제한 서비스를 직접 만드는건 시간이 걸리는 작업이기 때문에, 인력이 부족하다면 상용 API 게이트웨이를 쓰는 것이 바람직합니다.</li>
</ul>
<h3 id="처리율-제한-알고리즘">처리율 제한 알고리즘</h3>
<p>처리율 제한을 실현하는 알고리즘은 여러 가지인데, 다 다른 장단점을 가지고 있기 때문에 적절히 잘 선택해야합니다. </p>
<p>다만 여기서는 알고리즘에 초점을 맞추지는 않고 각각의 특성을 이해하는 한편 용례에 맞는 알고리즘 조합을 찾는데 도움이 되도록 개략적으로 설명하고 있습니다.</p>
<ol>
<li><strong>토큰 버킷 알고리즘</strong></li>
</ol>
<p>토큰 버킷 알고리즘은 처리율 제한에 폭넓게 이용되고 있습니다. 간단하고, 알고리즘에 대한 세간의 이해도도 높은 편이며 인터넷 기업들이 보편적으로 사용하고 있습니다. 아마존과 스트라이프가 API 요청을 통제(trorie)하기 위해 이 알고리즘을 사용합니다.</p>
<p><strong>토큰 버킷 알고리즘의 동작 원리</strong>는 다음과 같습니다.</p>
<p>• 토큰 버킷은 지정된 용량을 갖는 컨테이너입니다. 이 버킷에는 사전 설정된 양의 토큰이 주기적으로 채워집니다. 토큰이 꽉 찬 버킷에는 더 이상의 토큰은 추가되지 않습니다. </p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/33b84072-1dc4-4914-a202-fdb47b8e8f11/image.png" alt=""></p>
<p>위의 그림은 예제는 용량이 4인 버킷입니다. 토큰 공급기(refiller)는 이 버킷에 매초 2개의 토큰을 추가합니다. 버킷이 가득 차면 추가 로 공급된 토큰은 버려집니다(overflow).</p>
<p>• 각 요청은 처리될 때마다 하나의 토큰을 사용합니다. 요청이 도착하면 토큰이 있는지 검사하고 토큰이 있으면 버킷에서 토큰 하나를 꺼내, 요청을 처리하고 토큰이 없는 경우 해당 요청은 버려집니다. 아래 사진은 그 과정을 보여줍니다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/756e79d6-302f-4f19-b5ac-1977db330d4b/image.png" alt=""></p>
<p>아래 그림은 토큰을 어떻게 버킷에서 꺼내고, 토큰 공급기는 어떻게 동작하며, 처리 제한 로직은 어떻게 작동하는지를 보여줍니다. 이 예시에서 토큰 버킷의 크기는 4이고, 토큰 공급률은 분당 4입니다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/52ae4fe1-3e8c-4947-80d2-dc2b8546bc9c/image.png" alt=""></p>
<p>토큰 버킷은 2개의 인자를 받습니다.</p>
<ul>
<li>버킷 크기: 버킷에 담을 수 있는 최대 토큰의 수</li>
<li>토큰 공급률: 초당 몇 개의 토큰이 버킷에 공급되는지</li>
</ul>
<p>버킷을 몇 개나 사용해야하는지는 공급 제한 규칙에 따라 달라집니다.</p>
<ul>
<li>통상적으로, API 엔드포인트마다 별도의 버킷을 둡니다. 사용자마자 하루에 한 번 포스팅을 할 수 있고, 하루에 한 번 댓글을 달 수 있고, 150명의 친구를 추가할 수 있다고 하면 사용자마다 3개의 버킷을 두어야할 것입니다.</li>
<li>IP 주소별로 처리율을 제한해야한다면 IP 주소마다 하나의 버킷을 둬야할 것입니다.</li>
<li>시스템 처리율을 초당 10,000개 요청으로 제한하고 싶다면, 모든 요청이 하나의 버킷을 공유해야 할 것입니다.</li>
</ul>
<p><strong>장점</strong></p>
<ul>
<li>구현이 쉽습니다.</li>
<li>메모리 사용 측면에서도 효율적입니다.</li>
<li>짧은 시간에 집중되는 트래픽도 처리 가능합니다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>버킷 크기와 토큰 공급률, 두 개의 인자 값을 적절하게 튜닝하는 것은 까다로운 일입니다.</li>
</ul>
<p><strong>참고 !!! 스프링 부트에서도 이 알고리즘을 사용해서 처리율 제한이 가능합니다 !</strong></p>
<p><a href="https://velog.io/@dongvelop/Spring-Boot-Bucket4j%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%84%9C%EB%B2%84-%EC%B8%A1-%EC%B2%98%EB%A6%AC%EC%9C%A8-%EC%A0%9C%ED%95%9C-%EC%9E%A5%EC%B9%98-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0">[Spring Boot] Bucket4j를 이용해 서버 측 처리율 제한 장치 구축하기</a></p>
<ol start="2">
<li><strong>누출 버킷 알고리즘</strong></li>
</ol>
<p>토큰 버킷 알고리즘과 비슷하지만 요청 처리율이 고정되어 있다는것이 다릅니다. 그리고 누출 버킷 알고리즘은 보통 FIFO 큐로 구현됩니다. 동작 원리는 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/86e0d0f1-46f6-41d0-af95-66ba92dbbbbc/image.png" alt=""></p>
<ul>
<li>요청이 도착하면 큐가 가득 차 있는지 확인한 뒤, 빈 자리가 있으면 큐에 요청을 추가합니다.</li>
<li>큐가 가득 차 있는 경우에는 새 요청은 버립니다.</li>
<li>지정된 시간마다 큐에서 요청을 꺼내서 처리합니다.</li>
</ul>
<p>누출 버킷 알고리즘은 두 개의 인자를 갖습니다.</p>
<ul>
<li>버킷 크기: 큐 사이즈와 같은 값.</li>
<li>처리율: 지정된 시간당 몇 개의 항목을 처리할지 지정하는 값. 보통 초 단위</li>
</ul>
<p><strong>장점</strong></p>
<ul>
<li>큐의 크기가 제한되어 메모리 사용량 측면에서 효율적입니다</li>
<li>고정된 처리율을 갖고 있어서 안정된 출력(stable outflow rate)가 필요한 경우에 적합합니다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>단시간에 많은 트래픽이 몰리면 큐에 요청들이 쌓이고, 요청을 제때 처리하지 못하면 최신 요청들이 버려집니다.</li>
<li>토큰 버킷과 마찬가지로 두 개의 인자를 튜닝하는것이 까다롭습니다.</li>
</ul>
<ol start="3">
<li><strong>고정 윈도 카운터 알고리즘</strong></li>
</ol>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/e3542e66-f2a6-4c66-9764-427d5f14862d/image.png" alt=""></p>
<ul>
<li>타임라인을 고정된 간격의 윈도(window)로 나누고, 각 윈도마다 카운터를 붙입니다.</li>
<li>요청이 접수될 때마다 카운터의 값은 1씩 증가합니다.</li>
<li>이 카운터의 값이 사전에 설정된 임계치에 도달하면 새로운 요청은 새 윈도가 열릴때까지 버려집니다.</li>
</ul>
<p>위의 예제를 해석하자면 타임라인은 1초이고, 시스템은 초당 3개까지의 요청을 허용합니다. 만약 3개 이상의 요청이 밀려오면 초과분은 버려집니다.</p>
<p>이 알고리즘은 문제는 경계 부근에 순간적으로 많은 트래픽이 집중된 경우 할당된 양보다 더 많은 요청이 처리될 수 있다는 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/1f6c66f3-1fe4-4487-ae84-86a434ad01af/image.png" alt=""></p>
<p>위 예제는 분당 최대 5개의 요청을 허용하는 시스템입니다.</p>
<p>매분마다 카운터가 초기화 되는데, 이 예제를 보면 2:00:00와 2:01:00 사이에 5개의 요청이 들어왔고, 2:01:00과 2:02:00 사이에 또 5개의 요청이 들어왔습니다. 윈도 위치를 옮겨서 보면 2:00:30초부터 2:01:30초까지 10개의 요청이 몰려 처리해버리게 됩니다. 이는 2:01:00초에 카운터가 초기화되서 이런 현상이 벌어진 것이고, 분당 최대 5개 처리하는 허용 한도의 2배를 처리해버리게 되는 것입니다.</p>
<p><strong>장점</strong></p>
<ul>
<li>메모리 효율이 좋습니다.</li>
<li>이해하기 쉽습니다.</li>
<li>윈도가 닫히는 시점에 카운터를 초기화하는 방식은 특정한 트래픽 패턴을 처리하기에 적합합니다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>위와 같이 윈도 경계 부근에서 일시적으로 많은 트래픽이 몰리는 경우, 설정했던 처리 한도보다 많은 양의 요청을 처리하게 됩니다.</li>
</ul>
<ol start="4">
<li><strong>이동 윈도 로깅 알고리즘</strong> </li>
</ol>
<p>앞서 살펴본대로 고정 윈도 카운터 알고리즘에는 중대한 문제가 있었습니다. 윈도 경계 부근에 트래픽이 집중되는 경우 시스템에 설정된 한도보다 많은 요청을 처리하게 된다는 것입니다. 이동 윈도 로깅 알고리즘은 이 문제를 해결합니다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/1fa3dfb4-0f79-4a62-9501-b3e508201aae/image.png" alt=""></p>
<ul>
<li>요청의 타임스탬프(timestamp)를 추적한다. 타임스탬프 데이터는 보통 레디스(Redis)의 정렬 집합(sorted set)와 같은 캐시에 보관합니다.</li>
<li>새 요청이 오면 만료된 타임스탬프는 제거합니다.</li>
<li>새 요청의 타임스탬프를 로그에 추가합니다.</li>
<li>로그의 크기가 허용치보다 같거나 작으면 요청을 시스템에 전달하고 그렇지 않은 경우 처리를 거부합니다.</li>
</ul>
<p>위의 그림 예제를 좀 더 자세히 설명하면 아래와 같습니다. 위의 예제는 분당 최대 2회의 요청만을 처리하도록 설정되었습니다.</p>
<ul>
<li>요청이 1:00:01에 도착하였을 때, 로그는 비어 있는 상태다. 따라서 요청은 허용됩니다.</li>
<li>새로운 요청이 1:00:30에 도착합니다. 해당 타임스탬프가 로그에 추가됩니다. 추가 직후 로그의 크기는 2이며, 허용 한도보다 크지 않은 값입니다. 따라서 요청은 시스템에 전달됩니다.</li>
<li>새로운 요청이 1:00:50에 도착합니다. 해당 타임스탬프가 로그에 추가됩니다. 추가 직후 로그의 크기는 3으로, 허용 한도보다 큰 값입니다. 따라서 타임스탬프는 로그에 남지만 요청은 거부됩니다.</li>
<li>새로운 요청이 1:01:40에 도착합니다. [1:00:40, 1:01:40) 범위 안에 있는 요청은 1분 윈도 안에 있는 요청이지만, 1:00:40 이전의 타임스탬프는 전부 만료 된 값입니다. 따라서 두 개의 만료된 타임스탬프 1:00:01과 1:00:30을 로그에서 삭제합니다. 삭제 직후 로그의 크기는 2이고 1:01:40의 신규 요청이 시스템에 전달됩니다.</li>
</ul>
<p><strong>장점</strong></p>
<ul>
<li>처리율 제한 매커니즘은 아주 정교합니다. 어느 순간 윈도를 보더라도, 허용되는 요청의 개수는 시스템의 처리율 한도를 넘지 않습니다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>다량의 메모리를 사용합니다. 거부된 요청의 타임스탬프도 보관되기 때문입니다.</li>
</ul>
<ol start="5">
<li><strong>이동 윈도 카운터 알고리즘</strong></li>
</ol>
<p>고정 윈도 카운터 알고리즘과 이동 윈도 로깅 알고리즘을 결합한 것입니다. 이 알고리즘은 두 가지 접근법이 있지만 여기서는 하나만 다루었습니다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/a1bc7b98-7f44-4acd-89c6-83c9d9d91113/image.png" alt=""></p>
<p>처리율 한도가 분당 5개인데, 이전 1분에서 5개 현재 1분동안 3개의 요청이 왔습니다. 현재 1분의 30%시점(1분 18초정도?)에 요청이 도착한 경우 현재 윈도에 몇 개의 요청이 온 것으로 보고 처리해야 할까요?</p>
<ul>
<li>현재 1분간의 요청 수 + 직전 1분간의 요청 수 * 이동 윈도와 직전 1분이 겹치는 비율</li>
</ul>
<p>⇒ 이 공식에 따라 현재 윈도에 들어 있는 요청은 3+5 * 70% = 6.5개입니다. 올림하거나 내림할 수 있는데 예제에서는 내림해서 6개로 계산하였습니다.</p>
<p>처리율 한도가 분당 7개라하면 현재 1분의 30%시점에 도착한 신규 요청은 시스템으로 전달되지만 처리율 한도가 분당 5개라면 요청을 받을 수 없을 것입니다.</p>
<p><strong>장점</strong></p>
<ul>
<li>메모리 효율이 좋습니다.</li>
<li>이전 시간대의 평균 처리율에 따라 현재 윈도의 상태를 계산하므로 짧은 시간에 몰리는 트래픽에도 잘 대응합니다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>직전 시간대에 도착한 요청이 균등하게 분포되어 있다고 가정한 상태에서 추정치를 계산하기에 다소 느슨하지만, 그렇게 심각하지는 않습니다.</li>
</ul>
<h3 id="개략적인-아키텍쳐">개략적인 아키텍쳐</h3>
<p>처리율 제한 알고리즘의 기본적인 아이디어는 단순합니다. <strong>얼마나 많은 요청이 접수되었는지를 추적할 수 있는 카운터를 추적 대상별로 두고</strong> (사용자별로 추적할 것인가? 아니면 IP 주소별로? 아니면 API 엔드포인트나 서비스 단위로?), 이 <strong>카운터의 값이 어떤 한도를 넘어서면 한도를 넘어 도착한 요청은 거부하는 것</strong>입니다.</p>
<p>그럼 이 카운터는 어디에 보관할까요? 데이터베이스는 디스크접근을 해야하므로 느려서 안됩니다. </p>
<p>따라서 빠른데다가 시간 만료 정책을 지원하는 캐시가 적절합니다. 레디스(Redis)는 처리율 제한 장치를 구현할 때 자주 사용되는 메모리 기반 저장 장치로서, 카운터에 관한 명령어도 지원합니다.(INCR, EXPIRE)</p>
<ul>
<li>INCR : 메모리에 저장된 카운터의 값을 1만큼 증가시킨다.</li>
<li>EXPIRE : 카운터에 타임아웃 값을 설정한다. 설정된 시간이 지나면 카운터는 자동으로 삭제된다.</li>
</ul>
<p>동작 원리를 정리해보면 아래와 같습니다.</p>
<ol>
<li>클라이언트가 처리율 제한 미들웨어에게 요청을 보냅니다.</li>
<li>처리율 제한 미들웨어는 레디스의 지정 버킷에서 카운터를 가져와서 한도에 도달했는지 아닌지를 검사합니다.<ol>
<li>한도 도달 → 요청 거부</li>
<li>한도 도달 not yet → 요청은 API 서버로 전달되고 미들웨어는 카운터의 값을 증가시킨 후 다시 레디스에 저장</li>
</ol>
</li>
</ol>
<h2 id="3단계---상세-설계">3단계 - 상세 설계</h2>
<p>지금까지는 처리율 제한 규칙이 어디에 저장되고 어떻게 처리되는지를 봤습니다. 하지만 이는 아래 두 가지 사항을 알 수 없죠.</p>
<ul>
<li>처리율 제한 규칙은 어떻게 만들어지고 어디에 저장되는가?</li>
<li>처리가 제한된 요청들은 어떻게 처리되는가?</li>
</ul>
<p>이번 절에서는 우선 처리율 제한 규칙에 관한 질문부터 답한 후에 처리가 제한된 요청의 처리 전략을 살펴봅니다. 마지막으로는 분산 환경에서의 처리율 제한 기법에 대해서도 살펴보고, 구체적인 설계와 성능 최적화 방안, 모니터링 방안까지 살펴봅니다.</p>
<h3 id="처리율-제한-규칙">처리율 제한 규칙</h3>
<p>리프트라는 회사는 처리율 제한에 오픈소스를 사용하고 있습니다. 이 컴포넌트를 들여다보고, 어떤 처리율 제한 규칙이 사용되고 있는지 살펴봅시다.</p>
<pre><code class="language-java">domain: messaging
descriptiors:
    - key: message_type
      Value: marketing
      rate_limit:
        unit: day
        requests_per_unit: 5</code></pre>
<p>위의 예제는 시스템이 처리할 수 있는 마케팅 메세지의 최대치를 하루 5개로 제한하고 있습니다. 이런 규칙들은 보통 설정 파일 형태로 디스크에 저장됩니다.(환경 변수를 설정하는 <code>.env</code> 이나 <code>application.yml</code> 파일처럼)</p>
<h3 id="처리율-한도-초과-트래픽의-처리">처리율 한도 초과 트래픽의 처리</h3>
<p>어떤 요청이 한도 제한에 걸리면 API는 HTTP 429 응답코드를 클라이언트에게 보냅니다. </p>
<p>경우에 따라서는 한도 제한에 걸린 메세지를 나중에 처리하기 위해 큐에 보관할 수도 있습니다. 예를 들어 어떤 주문이 시스템 과부하 때문에 한도 제한에 걸렸다면, 해당 주문들을 보관했다가 나중에 처리할 수도 있습니다.</p>
<p><strong>✏️ 처리율 제한 장치가 사용하는 HTTP 헤더</strong></p>
<p>클라이언트에게 자신이 보낸 요청이 처리율 제한에 걸리고 있는지, 처리율 제한에 걸리기까지 얼마나 많은 요청을 보낼 수 있는지 알려주기 위해 HTTP 응답 헤더에 정보를 담아 내려줍니다.</p>
<ul>
<li>X-Ratelimit-Remaining: 윈도 내 남은 처리가능 요청 수</li>
<li>X-Ratelimit-Limit: 매 윈도마다 클라이언트가 전송할 수 있는 요청의 수</li>
<li>X-Ratelimit-Retry-After: 한도 제한에 걸리지 않으려면 몇 초 뒤에 요청을 보내야하는 지 알림</li>
</ul>
<p>즉, 사용자가 너무 많은 요청을 보내면 429 too many requests 오류를 X-Ratelimit-Retry-After 헤더와 함께 반환하도록 합니다.</p>
<h3 id="상세-설계">상세 설계</h3>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/dedd4451-f3d9-40be-8340-df7aff3bd87e/image.png" alt=""></p>
<ul>
<li>처리율 제한 규칙은 디스크에 보관합니다. 작업 프로세스는 수시로 규칙을 디스크에서 읽어서 캐시에 저장합니다.</li>
<li>클라이언트가 요청을 서버에 보내면 요청은 먼저 처리율 제한 미들웨어에 도달합니다.</li>
<li>처리율 제한 미들웨어는 제한 규칙을 캐시에서 가져옵니다. 아울러 카운터 및 마지막 요청의 타임스탬프를 레디스 캐시에서 가져옵니다. 가져온 값들에 근거하여 해당 미들웨어는 다음과 같은 결정을 내립니다.<ul>
<li>해당 요청이 처리율 제한에 걸리지 않은 경우에는 API 서버로 보냅니다.</li>
<li>해당 요청이 처리율 제한에 걸렸다면 429 too many requests 에러를 클라이언테에게 보냅니다. 한편 해당 요청은 그대로 버릴 수도 있고 메세지 큐에 보관할 수도 있습니다.</li>
</ul>
</li>
</ul>
<h3 id="분산-환경에서의-처리율-제한-장치의-구현">분산 환경에서의 처리율 제한 장치의 구현</h3>
<p>단일 서버를 지원하는 처리율 제한 장치가 아니라, 여러 대의 서버와 병렬 스레드를 지원하도록 시스템을 확장하는 것은 또 다른 문제입니다. 다음 두 가지 어려운 문제를 풀어야 합니다.</p>
<p><strong>✏️ 경쟁 조건</strong></p>
<p>처리율 제한 장치는 앞서 계속 설명했듯이 대략 다음과 같이 동작합니다.</p>
<ul>
<li>레디스에서 카운터의 값을 읽는다.</li>
<li>counter+1이 임계치를 넘는지 본다.</li>
<li>넘지 않는다면 레디스에 보관된 카운터 값을 1 증가시킨다.</li>
</ul>
<p>여기서 문제는 counter+1에 있는데 병행성이 심한 환경에서는 아래와 같은 경쟁 조건 이슈가 발생할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/59b70cbe-3009-4933-baf0-b4faf012e54e/image.png" alt=""></p>
<p>레디스에 저장된 변수 counter 값이 3이라고 해봅시다. 그리고 두 개 요청을 처리하는 스레드가 각각 병렬로 counter 값을 읽었으며 그 둘 가운데 어느 쪽도 아직 변경된 값을 저장하지는 않은 상태입니다.</p>
<p>둘 다 다른 요청의 처리 상태는 상관하지 않고 counter에 1을 더한 값을 레디스에 기록 할 것입니다. 그리고 counter의 값은 올바르게 변경되었다고 믿을 것이지만, 사실 counter의 값은 5가 되어야 하는 것이지요.</p>
<p>이를 해결하기 위해서 락(lock)을 걸 수도 있지만 성능이 떨어진다는 문제가 있습니다. 위 설계의 경우에는 락 대신 쓸 수 있는 해결책이 두 가지 있는데, <strong>루아 스크립트(Lua script)나 정렬 집합(sorted set)</strong>이라 불리는 <strong>레디스 자료구조를 사용하여 해결</strong>할 수 있다.</p>
<p><strong>✏️ 동기화 이슈</strong></p>
<p>수백만의 사용자를 지원하려면 하나의 처리율 제한 서버로는 부족할 것.니입다.래그서처리처율 제한 장치 서버를 여러 대 두게 되는데, 이 경우 동기화가 필요해집니다.</p>
<p>이에 대한 한 가지 해결책은 고정 세션을 통해서 같은 클라이언트로부터의 요청은 항상 같은 처리율 제한 장치로 보내는 것입니다. 하지만 이 방법은 규모면에서 확장 가능하지도 않고 유연하지도 않습니다.</p>
<p>더 나은 해결책은 <strong>레디스와 같이 중앙 집중형 데이터 저장소</strong>를 사용하는 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/b20b5433-26d9-448d-81ae-da9a0cf958c0/image.png" alt=""></p>
<p><strong>✏️ 성능 최적화</strong></p>
<p>성능 최적화는 시스템 설계 면접의 단골 주제입니다. 지금까지 살펴본 설계는 두 가지 지점에서 개선이 가능합니다.</p>
<p>우선, 여러 데이터센터를 지원하는 문제는 처리율 제한 장치에 매우 중요한 문제라는 것을 상기해야합니다. 데이터센터에서 멀리 떨어진 사용자를 지원하려다보면 지연시간(latency)이 증가할 수 밖에 없기 때문이죠. 이는 <strong>에지 서버를 사용</strong>해야 합니다.</p>
<p><em>현재 클라우드플레어는 지역적으로 분산된 194곳의 위치에 에지 서버를 설치해두고 있습니다.</em></p>
<p>두번째로 고려해야할 것은, 제한 장치 간에 데이터를 동기화 할 때 최종 일관성 모델을 사용하는 것입니다. 이 일관성 모델에 대해서는 추후 6장에 자세히 설명됩니다.</p>
<ul>
<li><p><strong>근데 궁금하니 간단하게라도 봐볼까요 ?</strong></p>
<p>  일관성 모델은 데이터 일관성의 수준을 결정하는데, 종류가 다양합니다.</p>
<ol>
<li><p>강한 일관성 : 모든 읽기 연산은 가장 최근에 갱신된 결과를 반환</p>
</li>
<li><p>약한 일관성 : 읽기 연산은 가장 최근에 갱신된 결과를 반환하지 못할 수 있음.</p>
</li>
<li><p>결과적 일관성 : 약한 일관성의 한 형태로, 갱신 결과가 결국에는 모든 사본에 반영(즉, 동기화) 되는 모델</p>
<p>AWS S3 데이터 일관성 모델의 특징을 보면</p>
<pre><code>&quot;Amazon S3은 모든 리전의 S3 버킷에 있는 새 객체의 PUT에 대해 한 가지 주의 사항을 제시함으로써 읽기 후 쓰기 일관성을 제공합니다. 주의할 점은 객체를 만들기 전에 (객체가 있는지 찾기 위해) 키 이름에 HEAD 또는 GET 요청을 하는 경우 Amazon S3가 읽기 후 쓰기에 대한 최종 일관성을 제공하는 것입니다. Amazon S3은 모든 리전의 덮어쓰기 PUT 및 DELETE에 대한 최종 일관성을 제공합니다.&quot;</code></pre><p>데이터 변경이 발생했을 때, 시간이 지남에 따라 여러 노드에 전파되면서 당장은 아니지만 최종적으로 일관성이 유지되는 것을 <strong>최종 일관성(Eventual Consistency</strong>)이라고 한다.
결국은 동시성을 제공하지 않고 결과적으로 일관성을 갖는 다는 의미이다.</p>
<p>그러므로 UPDATE 및 DELETE에 대한 최종 일관성을 가지는 S3는 객체를 처음 생성 후 가져올 시에는 일관성 있는 데이터를 제공하나, 삭제 후 가져올 시에는 일관성 없는 결과를 리턴할 수 있다는 특징을 가지게 되는 것입니다.</p>
</li>
</ol>
</li>
</ul>
<p><strong>✏️ 모니터링</strong></p>
<p>처리율 제한 장치를 설치한 이후에는 효과적으로 동작하고 있는지 보기 위해 데이터를 모을 필요가 있습니다. 기본적으로 모니터링을 통해 확인하려는 것은 다음 두 가지 입니다.</p>
<ul>
<li>채택된 처리율 제한 알고리즘이 효과적인가?</li>
<li>정의한 처리율 제한 규칙(파라미터)이 효과적인가?</li>
</ul>
<p>예를 들어 처리율 제한 규칙이 너무 빡빡하게 설정되어 있다면 많은 유효 요청이 처리되지 못하고 버려질 것이다. 그럼 규칙을 다소 완화할 필요가 있다.</p>
<p>깜짝 세일 같은 이벤트 때문에 트래픽이 급증할 때 처리율 제한 장치가 비효율적으로 동작한다면, 그런 트래픽 패턴을 잘 처리할 수 있도록 알고리즘을 바꾸는 것을 생각해 봐야 한다. 이런 상황이라면 토큰 버킷이 적합할 것이다.</p>
<h2 id="4단계---마무리">4단계 - 마무리</h2>
<p>위에서 설명한 부분들 말고도 다음과 같은 부분을 언급해보면 도움이 될 것입니다.</p>
<ul>
<li><p><strong>경성 또는 연성 처리율 제한</strong></p>
<ul>
<li>경성(Hard) 처리율 제한: 요청의 개수는 임계치를 절대 넘을 수 없다.</li>
<li>연성(Soft) 처리율 제한: 요청의 개수는 잠시동안 임계치를 넘을 수 있다.</li>
</ul>
</li>
<li><p><strong>다양한 계층 처리율 제한</strong></p>
<ul>
<li>이번 장에서는 애플리케이션 계층(HTTP: OSI 네트워크 계층도 기준으로 7번 계층)에서의 처리율 제한에 대해서만 살펴보았다. 하지만 다른 계층에서도 처리율 제한이 가능하다.</li>
<li>예를 들어, Iptables를 사용하면 IP 주소(IP는 OSI 기준으로 3번 네트워크 계층)에 처리율 제한을 적용하는 것이 가능하다.</li>
</ul>
</li>
<li><p><strong>처리율 제한 회피 방법. 클라이언트가 최선으로 할 수 있는 설계 방법?</strong></p>
<ul>
<li><p>클라이언트 측 캐시를 사용하여 API 호출 횟수 줄이기</p>
</li>
<li><p>처리율 제한의 임계치를 이해하고, 짧은 시간 동안 너무 많은 메세지를 보내지 않도록 한다.</p>
</li>
<li><p>예외나 에러를 처리하는 코드를 도입하여 클라이언트가 예외적 상황으로부터 우아하게 복구될 수 있도록 한다.</p>
</li>
<li><p>재시도 로직을 구현할 때는 충분한 백오프 시간을 둔다.</p>
<p><a href="https://medium.com/@odysseymoon/spring-webflux%EC%97%90%EC%84%9C-error-%EC%B2%98%EB%A6%AC%EC%99%80-retry-%EC%A0%84%EB%9E%B5-a6bd2c024f6f">Spring WebFlux에서 Error 처리와 Retry 전략</a></p>
</li>
</ul>
</li>
</ul>
<h2 id="참고">참고</h2>
<p><a href="https://hogwart-scholars.tistory.com/entry/Spring-Boot-%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81%EC%97%90%EC%84%9C-%EC%B2%98%EB%A6%AC%EC%9C%A8-%EC%A0%9C%ED%95%9C-%EA%B8%B0%EB%8A%A5%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-4%EA%B0%80%EC%A7%80-%EB%B0%A9%EB%B2%95#Resilience4j-1">[Spring Boot] 자바 스프링에서 처리율 제한 기능을 구현하는 4가지 방법</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring - Builder 패턴이 뭐야?]]></title>
            <link>https://velog.io/@ye_suri_106/Builder-%ED%8C%A8%ED%84%B4%EC%9D%B4-%EB%AD%90%EC%95%BC</link>
            <guid>https://velog.io/@ye_suri_106/Builder-%ED%8C%A8%ED%84%B4%EC%9D%B4-%EB%AD%90%EC%95%BC</guid>
            <pubDate>Mon, 08 May 2023 17:19:40 GMT</pubDate>
            <description><![CDATA[<h3 id="☑️-빌더-패턴이-뭐야-">☑️ 빌더 패턴이 뭐야 ?</h3>
<ul>
<li>빌더 패턴은 <strong>복잡한 객체를 생성하는 방법을 정의하는 클래스</strong>와 <strong>표현하는 방법을 정의하는 클래스</strong>를 <strong>별도로 분리</strong>하여, 서로 다른 표현이라도 이를 생성할 수 있는 동일한 절차를 제공하는 패턴이다.</li>
<li>생성해야하는 객체가 Optional한 속성을 많이 가질 때가 더 좋다.</li>
<li>빌더 패턴은 생성 패턴 중 하나이다.<ul>
<li>생성 패턴은 <strong>인스턴스를 만드는 절차를 추상화하는 패턴</strong>이다.</li>
<li>생성 패턴에 속하는 패턴들은 객체를 생성, 합성하는 방법이나 객체의 표현 방법을 시스템과 분리해준다.</li>
<li>생성 패턴은 다음 두 가지 특징이 있다.<ul>
<li>시스템이 어떤 Concrete Class를 사용하는지에 대한 정보를 캡슐화한다.</li>
<li>이들 클래스의 인스턴스들이 어떻게 만들고 어떻게 결합하는지에 대한 부분을 완전히 가려준다.</li>
</ul>
</li>
<li>다시 말해, 생성 패턴을 이용하면 무엇이 생성되고, 누가 이것을 생성하며 어떻게 생성하는지 결정하는데 유연성을 확보할 수 있게 된다.</li>
</ul>
</li>
</ul>
<br>

<h3 id="☑️-빌더-패턴을-왜-쓰는건데-">☑️ 빌더 패턴을 왜 쓰는건데 ?</h3>
<ul>
<li>빌더 패턴은 객체를 생성할 때 <strong>생성자만 사용할 때 발생할 수 있는 문제를  개선하기 위해</strong> 사용된다.</li>
<li>생성 패턴 말고도 팩토리 메소드 패턴이나 추상 팩토리 패턴에서는 생성해야하는 클래스에 대한 속성 값이 많을 때 아래와 같은 이슈가 발생한다.<ul>
<li>클라이언트가 팩토리 클래스를 호출할 때 파라미터로 넘겨주는 값의 타입, 순서 등에 대한 관리가 어려워져 에러가 발생할 확률이 높아진다.</li>
<li>경우에 따라 필요 없는 파라미터들에 대해서는 팩토리 클래스에 일일이 Null 값을 넘겨줘야 한다.</li>
<li>생성해야 하는 sub class가 무거워지고 복잡해짐에 따라 팩토리 클래스 역시 복잡해진다.</li>
</ul>
</li>
<li>빌더 패턴은 이런 문제들을 해결하기 위해 <strong>별도의 Builder 클래스를 만들어 필수 값에 대해서는 생성자</strong>를 통해, <strong>선택적인 값들에 대해서는 메소드</strong>를 통해 step-by-step으로 값을 입력받은 후에 build() 메소드를 통해 최종적으로 하나의 인스턴스를 return하는 방식이다.<br>

</li>
</ul>
<h3 id="☑️-그럼-이-빌더-패턴-어떻게-써-">☑️ 그럼 이 빌더 패턴 어떻게 써 ?</h3>
<ul>
<li>요구사항을 기반으로 여행 계획을 세우는 앱을 개발한다고 할 때, 다음과 같은 요구사항이 있다.</li>
</ul>
<ol>
<li><strong>요구사항1</strong> : 여행 계획 항목은 이렇게 해주세요.<ol>
<li>여행 제목, 여행 장소, 여행 출발일, 몇박 몇일동안 어디서 머물지, n일차에 하루 계획을 기록</li>
</ol>
</li>
</ol>
<ul>
<li>위의 요구사항1을 만족시키는 도메인은 다음과 같이 구성될 것이다.</li>
</ul>
<pre><code class="language-java">/**
 * 여행 계획
 */
public class TourPlan {
    private String title; // 여행 제목
        private String place; // 여행 장소
    private LocalDate startDate; // 출발일
    private int nights; // n박
    private int days; // m일
    private List&lt;DailyPlan&gt; plans; // m일차 하루 계획
}

/**
 * n일차 하루 계획
 */
public class DailyPlan {
    private int day; // n일차
        private String place; // 갈 곳
    private String doing; // 할 일
}</code></pre>
<ol>
<li><strong>요구사항2</strong> : 여행은 꼭 n박 m일이 아니고, 당일 치기일 수도 있어요 !<ol>
<li>당일 치기는 n박 m일이 필요 없고, 어디서 머물지도 필요없다.</li>
</ol>
</li>
</ol>
<ul>
<li>위와 같이, 필수적인 정보와 선택적인 정보로 Optional한 속성들이 생겼을 때 어떻게 구현하면 될까 ?</li>
</ul>
<h3 id="1-점층적-생성자-패턴">1) 점층적 생성자 패턴</h3>
<ul>
<li>점층적 생성자 패턴을 적용하면 생성자 오버로딩을 통해 구현 가능하다.</li>
</ul>
<pre><code class="language-java">/**
 * 기본 생성자 (필수)
 */
public TourPlan() {
}

/**
 * 일반적인 여행 계획 생성자
 *
 * @param title 여행 제목
 * @param startDate 출발 일
 * @param nights n박
 * @param days m일
 * @param whereToStay 머물 장소
 * @param plans n일차 할 일
 */
public TourPlan(String title, String place, LocalDate startDate, int nights, int days,
    List&lt;DailyPlan&gt; plans) {
    this.title = title;
        this.place = place;
    this.nights = nights;
    this.days = days;
    this.startDate = startDate;
    this.plans = plans;
}

/**
 * 당일치기 여행 계획 생성자
 *
 * @param title 여행 제목
 * @param startDate 출발 일
 * @param plans 1일차 할 일
 */
public TourPlan(String title, String place, LocalDate startDate, List&lt;DailyPlan&gt; plans) {
    this.title = title;
        this.place = place;
    this.startDate = startDate;
    this.plans = plans;
}</code></pre>
<ul>
<li>위와 같이 점층적 생성자 패턴으로 구현하면, Optional한 인자에 따라 새로운 생성자를 만들거나, Null 값으로 채워야하는 문제가 있다.</li>
<li>뭐 Lombok의 @AllArgsConstructor 어노테이션을 사용하면 코드가 길어지는 문제는 해결 가능하지만, 생성자에 이렇게 인자가 많으면 타입과 순서로 발생할 수 있는 에러 가능성이 있다.</li>
</ul>
<pre><code class="language-java">// 순서를 파악이 어렵고, 가독성이 떨어진다.
new TourPlan(&quot;여봉봉과 함께하는 1주년 여행&quot;, &quot;부산&quot;, LocalDate.of(2021,12, 24), 3, 4,
    Collections.singletonList(new DailyPlan(1, &quot;자갈치 시장&quot;, &quot;회떠오기&quot;)));

// 생성자를 만들지 않고 당일치기 객체를 생성하면 불필요한 Null을 채워야한다.
new TourPlan(&quot;전주 맛집 투어&quot;, &quot;전주&quot;, LocalDate.of(2021,12, 24), null, null, null,
    Collections.singletonList(new DetailPlan(1, &quot;한옥마을&quot;, &quot;한복 입기&quot;)));</code></pre>
<h3 id="2-자바-빈bean-패턴">2) 자바 빈(Bean) 패턴</h3>
<ul>
<li>이러한 단점을 보완하기 위해 setter 메소드를 사용한 자바 빈 패턴이 생겼다.</li>
</ul>
<pre><code class="language-java">TourPlan tourPlan = new TourPlan();
tourPlan.setTitle(&quot;칸쿤 여행&quot;);
tourPlan.setNights(2);
tourPlan.setDays(3);
tourPlan.setStartDate(LocalDate.of(2021, 12, 24));
tourPlan.setPlace(&quot;칸쿤&quot;);
tourPlan.addPlan(1, &quot;어쩌고 호텔&quot;, &quot;체크인 이후 짐풀기&quot;);
tourPlan.addPlan(1, &quot;어쩌고 호텔&quot;,&quot;저녁 식사&quot;);
tourPlan.addPlan(2, &quot;어쩌고 호텔&quot;, &quot;조식 먹기&quot;);
tourPlan.addPlan(2, &quot;저쩌고 비치&quot;, &quot;해변가 산책&quot;);
tourPlan.addPlan(2, &quot;이러쿵 음식점&quot;, &quot;점심은 수영장 근처 음식점에서 먹기&quot;);
...
tourPlan.addPlan(3, &quot;어쩌고 호텔&quot;, &quot;체크아웃&quot;);</code></pre>
<ul>
<li>가독성도 좋아지고 순서의 제약에서도 어느정도 벗어나기 때문에 에러 발생 가능성도 줄어든다.</li>
<li>하지만 과연 문제가 없을까 ?<ul>
<li>함수 호출이 인자만큼 이루어지고, 객체 호출을 한번에 할 수 없다.</li>
<li>불변(immutable) 객체를 생성할 수 없다. (setter로 값이 변경 가능하기 대문이다)<ul>
<li>쓰레드간 공유 가능한 객체 일관성(consistency)이 일시적으로 깨질 수 있다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="3-빌더-패턴">3) 빌더 패턴</h3>
<ul>
<li>생성자 패턴과 자바 빈 패턴의 장점을 결합하여 객체 생성과 관련된 문제를 해결한다.</li>
<li>필요한 객체를 직접 생성하지 않고, 먼저 필수 인자들을 생성자에 전부 전달해서 <strong>빌더 객체를 만든다.</strong></li>
<li>그리고 선택 인자는 가독성이 좋은 코드로 인자를 넘길 수 있다.</li>
<li>setter가 없으므로 객체 일관성을 유지하여 불변 객체로 생성 가능하다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/9940a949-cf1d-40c9-99f2-f3efa7d017a7/image.png" alt="백기선님 - 코딩으로 학습하는 GoF의 디자인 패턴 강의 자료"></p>
<ul>
<li>인터페이스인 TourPlanBuilder를 만들어준다.</li>
</ul>
<pre><code class="language-java">public interface TourPlanBuilder {

    TourPlanBuilder nightsAndDays(int nights, int days);

    TourPlanBuilder title(String title);

    TourPlanBuilder startDate(LocalDate localDate);

    TourPlanBuilder place(String place);

    TourPlanBuilder addPlan(int day, String place, String doing);

    TourPlan getPlan();

}</code></pre>
<ul>
<li>이를 구현하는 구현체 ConcreteBuilder를 만들어준다.</li>
</ul>
<pre><code class="language-java">public class DefaultTourBuilder implements TourPlanBuilder {

    private String title;

    private int nights;

    private int days;

    private LocalDate startDate;

    private String place;

    private List&lt;DeilyPlan&gt; plans;

    @Override
    public TourPlanBuilder nightsAndDays(int nights, int days) {
        this.nights = nights;
        this.days = days;
        return this;
    }

    @Override
    public TourPlanBuilder title(String title) {
        this.title = title;
        return this;
    }

    @Override
    public TourPlanBuilder startDate(LocalDate startDate) {
        this.startDate = startDate;
        return this;
    }

    @Override
    public TourPlanBuilder whereToStay(String place) {
        this.place = place;
        return this;
    }

    @Override
    public TourPlanBuilder addPlan(int day, String place, String doing) {
        if (this.plans == null) {
            this.plans = new ArrayList&lt;&gt;();
        }

        this.plans.add(new DailyPlan(day, place, doing));
        return this;
    }

    @Override
    public TourPlan getPlan() {
        return new TourPlan(title, startDate, days, nights, place, plans);
    }
}</code></pre>
<ul>
<li>이렇게 하면 다음과 같이 TourPlan 객체를 생성할 수 있다.</li>
</ul>
<pre><code class="language-java">return tourPlanBuilder.title(&quot;칸쿤 여행&quot;)
        .nightsAndDays(2, 3)
        .startDate(LocalDate.of(2020, 12, 9))
        .place(&quot;칸쿤&quot;)
        .addPlan(1, &quot;어쩌고 호텔&quot;, &quot;체크인하고 짐 풀기&quot;)
        .addPlan(1, &quot;어쩌고 호텔&quot;, &quot;저녁 식사&quot;)
        .getPlan();</code></pre>
<ul>
<li>아래 다이어그램처럼 Director를 적용하면 클라이언트 코드를 단축할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/4f815fd0-8d94-4875-ae20-b55464b52499/image.png" alt=""></p>
<pre><code class="language-java">public class TourDirector {

    private TourPlanBuilder tourPlanBuilder;

    public TourDirector(TourPlanBuilder tourPlanBuilder) {
        this.tourPlanBuilder = tourPlanBuilder;
    }

    public TourPlan cancunTrip() {
        return tourPlanBuilder.title(&quot;칸쿤 여행&quot;)
                .nightsAndDays(2, 3)
                        .startDate(LocalDate.of(2020, 12, 9))
                        .place(&quot;칸쿤&quot;)
                        .addPlan(1, &quot;어쩌고 호텔&quot;, &quot;체크인하고 짐 풀기&quot;)
                        .addPlan(1, &quot;어쩌고 호텔&quot;, &quot;저녁 식사&quot;)
                        .getPlan();
    }

    public TourPlan busanTrip() {
        return tourPlanBuilder.title(&quot;부산 당일 치기&quot;)
                .startDate(LocalDate.of(2021, 7, 15))
                .getPlan();
    }
}</code></pre>
<pre><code class="language-java">public static void main(String[] args) {
    TourDirector director = new TourDirector(new DefaultTourBuilder());
    TourPlan tourPlan = director.cancunTrip();</code></pre>
<br>

<h3 id="☑️-근데-이-빌더-패턴을-spring에-어떻게-접목한다는거-">☑️ 근데 이 빌더 패턴을 Spring에 어떻게 접목한다는거 ?</h3>
<ul>
<li>그래서 Java 개발자들의 보일러플레이트 코드를 기똥차게 줄여준 라이브러리인 Lombok을 사용하는 것이다 !</li>
</ul>
<pre><code class="language-java">        @AllArgsConstructor(access = AccessLevel.PRIVATE)
    @Builder(builderMethodName = &quot;**travelCheckListBuilder**&quot;)
    @ToString
    public class TravelCheckList {

        private Long id;
        private String passport;
        private String flightTicket;
        private String creditCard;
        private String internationalDriverLicense;
        private String travelerInsurance;

        public static TravelCheckListBuilder **builder**(Long id) {
            if(id == null) {
                throw new IllegalArgumentException(&quot;필수 파라미터 누락&quot;);
            }
            return **travelCheckListBuilder**().id(id);
        }
    }
</code></pre>
<ul>
<li><p><code>@AllArgsConstructor(access = AccessLevel.PRIVATE)</code></p>
<ul>
<li>@Builder 애노테이션을 선언하면 전체 인자를 갖는 생성자를 자동으로 만든다.</li>
<li>@AllArgsConstructor는 전체 인자를 갖는 생성자를 만드는데, 접근자를 private으로 만들어서 외부에서 접근할 수 없도록 만든다.</li>
</ul>
</li>
<li><p><code>@Builder</code></p>
<ul>
<li><p>앞서 설명한 Builder 패턴을 자동으로 생성해주는데, builderMethodName에 들어간 이름으로 빌더 메서드를 생성해준다.</p>
<pre><code class="language-java">public class MainClass {

      public static void main(String[] args) {
          // 빌더패턴을 통해 어떤 필드에 어떤 값을 넣어주는지 명확히 눈으로 확인할 수 있다!
          TravelCheckList travelCheckList = TravelCheckList.builder(145L)
                  .passport(&quot;M12345&quot;)
                  .flightTicket(&quot;Paris flight ticket&quot;)
                  .creditCard(&quot;Shinhan card&quot;)
                  .internationalDriverLicense(&quot;1235-5345&quot;)
                  .travelerInsurance(&quot;Samsung insurance&quot;)
                  .build();

          System.out.println(&quot;빌더 패턴 적용하기 : &quot; + travelCheckList.toString());

      }

     // 결과
     // 빌더 패턴 적용하기 : TravelCheckList(id=1, passport=M12345, flightTicket=Paris flight ticket, creditCard=Shinhan card, internationalDriverLicense=1235-5345, travelerInsurance=Samsung insurance)
  }</code></pre>
</li>
</ul>
</li>
<li><p><strong>클래스 내부 builder 메서드</strong> : 필수로 들어가야할 필드들을 검증하기 위해 만들었다. 꼭 id가 아니라도 해당 클래스를 객체로 생성할 때 필수적인 필드가 있다면 활용할 수 있다.</p>
</li>
<li><p>만약 여기서 @AllArgsConstructor 어노테이션을 쓰지 않고 직접 생성자를 만든다면 다음과 같이 생성자 위에 @Builder 어노테이션을 붙여주면 된다.</p>
</li>
<li><p>사실 @AllArgsConstructor 어노테이션이 편하다고 해서 무조건 좋은 것은 아니라 이 방법을 더 권장한다.</p>
</li>
</ul>
<pre><code class="language-java">import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Car {

    private String id;
    private String name;

    **@Builder**   // 생성자를 만든 후 그 위에 @Builder 애노테이션 적용
    public Car(String id, String name) {
        this.id = id;
        this.name = name;
    }
}</code></pre>
<ul>
<li><p>적용은 다음과 같다.</p>
<pre><code class="language-java">public class CarImpl {

  private String id = &quot;1&quot;;
  private String name = &quot;carTest&quot;;

  Car car3 = Car.builder()
          .id(id)
          .name(name)
          .build();
}</code></pre>
</li>
</ul>
<p>사실 이 빌더 패턴에 대해서는 개발환경이나 언어적 특성에 따라 긍정적인 반응과 부정적인 반응이 갈리기는 한다. </p>
<p>모든 그렇듯, 무작정 사용하지 말고 한번 자신이 개발하는 전체적인 구조나 환경의 관점에서 장단점을 판단해보고 사용하는 것이 중요한 것 같다 !</p>
]]></description>
        </item>
        <item>
            <link>https://velog.io/@ye_suri_106/n9h7cddk</link>
            <guid>https://velog.io/@ye_suri_106/n9h7cddk</guid>
            <pubDate>Mon, 08 May 2023 16:20:26 GMT</pubDate>
            <description><![CDATA[<p>위상 정렬에 대해 설명하기 전에, 먼저 <strong>DAG</strong>에 대해서 알아야한다.</p>
<blockquote>
<p>❓ <strong>DAG (Directed Acyclic Graph) ?</strong>
DAG는 순환을 가지지 않는 방향 그래프를 의미한다. 
일반적으로 우선순위를 가진 일련의 작업들은 DAG 구조를 가진다.
<em>ex) 대학 과정의 선수과목, 스타 크래프트에서 건물 짓는 순서…</em></p>
</blockquote>
<p><strong>위상 정렬</strong>은,</p>
<ul>
<li><p>DAG (비순환 방향 그래프)에서 그래프의 <strong>방향성을 거스르지 않고</strong> 정점을 나열하는 것</p>
</li>
<li><p>위상 정렬은 각 정점을 우선순위에 따라 배치한 것</p>
</li>
<li><p>위상 정렬의 결과는 유일하지 않다.</p>
<p>  ⇒ 시작 정점은 선택하기 나름이고, 방향성만 지키면 되기 때문이다</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/5662ee69-123d-4fb5-9247-8fe4e10a534c/image.png" alt=""></p>
<p>다음과 같은 그래프를 예시로 보자.</p>
<p>&lt;A, B, C, D, E, F&gt; 와 &lt;B, A, E, C, D, F&gt;와 같은 순서는 위상 정렬이 된다. </p>
<p>하지만 &lt;C, A, B, D, E, F&gt;는 방향성이 A 다음 C 이기 때문에 위상 정렬이 아니다.</p>
<hr>
<p>자, 이제 위상 정렬이 대충 어떤 것을 의미 하는 것인지 알았으니 <strong>위상 정렬 알고리즘</strong>을 알아보자 !</p>
<p>먼저, 진입 차수라는 개념을 알아두도록 하자.</p>
<p>앞으로 얘기하는 진입 차수란 그 정점으로 들어오는 정점의 개수를 의미한다. </p>
<p>즉, 선행되어야하는 정점의 개수를 뜻한다. 위에 그림에서 D 정점 같은 경우는 D 이전에 실행되어야하는 정점은 A, C, B 세 개 이므로 D의 진입 차수는 3이 된다.</p>
<p>진입 차수가 0이라는 것은 선행되어야하는 정점이 없는 정점이란 뜻이기 때문에, 시작 정점이 된다.</p>
<p>따라서,</p>
<p>진입 차수가 0인 정점을 선택하고 선택된 정점과 연결된 모든 간선을 삭제한다.</p>
<p>이때, 삭제되는 간선과 연결된 정점들의 진입 차수를 변경해주고 </p>
<p>이 과정을 반복해서 모든 정점을 출력하면 알고리즘이 끝난다.</p>
<p>말로는 잘 이해가 안될 수도 있으니, 한 스텝씩 그림과 함께 설명하겠다 !</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/f41ae8fd-ab71-41cb-9653-da2595fefbf1/image.png" alt=""></p>
<p>1) <strong>진입 차수가 0인 정점 1번</strong>이 시작 정점이 된다. 1번 정점과 연결된 정점은 2번, 3번, 4번이므로 이 정점들의 진입 차수를 바꿔주고, 정점 1번을 삭제한다.</p>
<h3 id="1번-→"><strong>1번 →</strong></h3>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/62047898-76b7-48e6-bc82-a26d1056c391/image.png" alt=""></p>
<p>2) <strong>진입 차수가 바뀐 것은 초록색</strong>으로 표시했다. 위와 마찬가지로 이제는 정점 3번의 진입 차수가 0이므로 그 다음은 <strong>정점 3번</strong>을 선택한다. 3번과 연결된 정점 4번의 진입 차수를 바꿔주고 정점 3번을 삭제한다.</p>
<h3 id="1번-→-3번"><strong>1번 → 3번</strong></h3>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/a5fab9ab-a03c-4e8b-bacd-029825f1ff6e/image.png" alt=""></p>
<p>3) 위와 마찬가지로 진행해준다. <strong>정점 4번</strong> 선택.</p>
<h3 id="1번-→-3번-→-4번"><strong>1번 → 3번 → 4번</strong></h3>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/8b6d1f25-aeab-49e3-88be-64a9736f4d09/image.png" alt=""></p>
<p>4) 위와 마찬가지로 진행해준다. <strong>정점 3번</strong> 선택.</p>
<h3 id="1번-→-3번-→-4번-→-2번"><strong>1번 → 3번 → 4번 → 2번</strong></h3>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/e78337d1-76b0-469b-a6fa-723469603f5d/image.png" alt=""></p>
<p>5) 마지막 정점까지 모두 삭제하면 알고리즘이 끝난다.</p>
<h3 id="1번-→-3번-→-4번-→-2번-→-5번"><strong>1번 → 3번 → 4번 → 2번 → 5번</strong></h3>
<p>정리하면,</p>
<blockquote>
<ol>
<li>각 노드마다 자신으로 향하는 노드의 개수를 저장 ( 진입 차수 ⇒ 1차원 배열 indegree )</li>
<li>탐색 노드에서 출발하여 도착하는 노드의 indegree를 하나씩 빼준다.</li>
<li>indegree가 0인 경우엔 선행되어야하는 정점이 없는 것이므로 queue에 저장하여 출발 노드로 이용한다.</li>
<li>queue가 빌 때까지 탐색을 진행한다.</li>
</ol>
</blockquote>
<p>순서로 코드를 작성하면 된다.</p>
<p>실제 코드를 어떻게 작성하여 문제를 해결하는 것인지 궁금하다면 </p>
<p>밑에 달아놓은 페이지를 참고하면 좋을 것 같다 !</p>
<hr>
<p>👇 <strong>위상 정렬과 관련된 백준 문제 풀이</strong></p>
<p><a href="https://yesuri-masuri.notion.site/1516-2b7331e0eadb4977af4c9831befd9edf">1516 게임 개발</a> </p>
<p><a href="https://yesuri-masuri.notion.site/2252-59f34ab6154f41369858d08114ca6f6a">2252 줄 세우기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring - Lombok을 사용한 생성자 전략]]></title>
            <link>https://velog.io/@ye_suri_106/Lombok%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%83%9D%EC%84%B1%EC%9E%90-%EC%A0%84%EB%9E%B5</link>
            <guid>https://velog.io/@ye_suri_106/Lombok%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%83%9D%EC%84%B1%EC%9E%90-%EC%A0%84%EB%9E%B5</guid>
            <pubDate>Fri, 05 May 2023 20:44:08 GMT</pubDate>
            <description><![CDATA[<p>Entity 개발을 하다가 문득 @NoArgsConstructor을 아무 생각없이 붙이고 있는 나 자신을 발견했다. 그러면서 @AllArgsConstructor과 @RequiredArgsConstructor을 잘 구분해서 알맞게 사용하고 있는 것인지 헷갈리기 시작했다...😶‍🌫️ 정리가 필요할 것 같아서 이렇게 글을 써본다 !</p>
<hr>
<h3 id="noargsconstructor"><code>@NoArgsConstructor</code></h3>
<ul>
<li><p>JPA에서는 프록시를 생성을 위해서 기본 생성자를 반드시 하나를 생성해야한다.</p>
</li>
<li><p>사실 기본 생성자는 <code>@Entity</code> 어노테이션만 붙여도 자동으로 생성해주지만, <code>@NoArgsConstructor</code> 를 붙여주는 이유는 AccessLevel 옵션값을 부여해서 접근 제한을 하도록 해 기본 생성자의 무분별한 생성을 막아서 의도하지 않은 엔티티를 생성하는 것을 막을 수 있기 때문이다.</p>
</li>
<li><p>근데 보통 <code>@NoArgsConstructor(access = AccessLevel.PROTECTED)</code> 와 같이 접근 제한을 PROTECTED로 해주는데 그건 왜 그런걸까 ? 안전하게 PRIVATE으로 하면 안돼 ?</p>
</li>
<li><p>JPA에서 연관 관계에 있는 엔티티를 조회할 때 보통 지연 로딩(LAZY)으로 값을 조회하여 리소스 낭비를 줄인다. 이때 지연 로딩은 프록시 객체를 생성해서 엔티티 값을 참조할 수 있게 하는데 접근 제한이 PRIVATE이라면 이 프록시 객체를 생성할 수 없기 때문이다.</p>
</li>
<li><p>사용 시 고려해야 할 점</p>
<ul>
<li>필드들이 final로 생성되어 있는 경우에는 필드를 초기화할 수 없기 때문에 생성자를 만들 수 없고 에러가 발생한다. → @NoArgsConstructor(force=true) 옵션을 이용해 final 필드를 강제 초기화 시켜 생성자를 만들 수 있다.<ul>
<li>@Nonnull 같이 필드에 제약조건이 설정되어 있는 경우, 추후 초기화를 진행하기 전까지 생성자 내 null-check 로직이 생성되지 않는다.<br>

</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="builder와-noargsconstructor의-사용"><code>@Builder</code>와 <code>@NoArgsConstructor</code>의 사용</h3>
<ul>
<li><p>Builder 개념이 궁금하다면 다음 포스팅을 참고하자 !</p>
<p>🖇 <a href="https://velog.io/@ye_suri_106/Builder-%ED%8C%A8%ED%84%B4%EC%9D%B4-%EB%AD%90%EC%95%BC">빌더 패턴이 뭐야 ?</a></p>
</li>
<li><p><code>@Builder</code> 의 경우, 해당 어노테이션이 붙은 클래스에 생성자가 없는 경우 모든 멤버 변수를 파라미터로 받는 기본 생성자를 생성하고, 생성자가 있다면 따로 생성자를 생성하지 않는다.</p>
</li>
<li><p>이 때 <code>@NoArgsConstructor</code> 어노테이션이 붙어 있다면 기본 생성자가 이미 생성이 되는 것이므로, 따로 생성자를 생성하지 않는데 이렇게 되면 매개변수를 일치하게 받는 생성자가 없어 에러가 발생한다. 따라서 모든 필드를 파라미터로 가지는 <code>@AllArgsConstructor</code> 를 붙여주어 해결한다.</p>
</li>
<li><p>또 다른 방법으로는 직접 클래스 내에 생성자를 만들고, 클래스에 <code>@Builder</code> 를 붙여서 선언하는 것이 아니라 생성자에 붙여서 선언하는 방법이 있다. 코드로 설명하면 다음과 같다.</p>
</li>
</ul>
<pre><code>```java
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String nickname;
    @Column(nullable = false)
    private String email;
    @Column(nullable = false)
    private String password;

    @Builder
    private User(String nickname, String email, String password) {
        this.nickname = nickname;
        this.email = email;
        this.password = password;
    }
}
```</code></pre><ul>
<li>사실 후자의 방법처럼 생성자를 직접 만들어서 생성자에 @Builder 어노테이션을 붙이는 것을 더 지향하는 편이다.  그 이유는 <code>@AllArgsConstructor</code>의 사용은 되도록 지양하고, 생성자를 직접 선언해주고 필요에 따라 Builder 패턴을 사용할 것을 권장하기 때문이다. 자세한 건 아래에서 설명하겠다 !<br>

</li>
</ul>
<h3 id="allargsconstructor">@AllArgsConstructor</h3>
<ul>
<li><p>클래스에 존재하는 모든 필드를 파라미터로 받는 생성자를 만들어주는 어노테이션이다.</p>
</li>
<li><p>굉장히 간단하게 생성자를 만들어주는 것 같아 오호 좋은데 ~? 할 수 있지만 지양 해야하는 어노테이션 중 하나이다.</p>
</li>
<li><p>가장 흔히 문제가 될 수 있는 케이스를 봐보자.</p>
<ul>
<li>두 개의 같은 타입 맴버 멤버를 선언한 상황에서 개발자가 선언된 멤버 변수의 순서를 바꾸면, 개발자도 인식하지 못하는 사이에 lombok이 생성자의 파라미터 순서를 필드 선언 순서에 따라 변경하게 된다.</li>
<li>이때, IDE가 제공해주는 리팩토링은 전혀 동작하지 않고, 두 필드가 동일 타입이기 때문에 기존 소스에서도 오류가 발생하지 않아 아무런 문제없이 동작하는 것으로 보이지만, 실제로 입력된 값이 바뀌어 들어가는 상황이 발생한다.</li>
</ul>
</li>
<li><p>다음 코드를 보면 문제점을 확실히 알 수 있다 !</p>
<pre><code class="language-java">@AllArgsConstructor
public static class Member {
private String firstName;
private String lastName;
}
// 성이 남, 이름이 주혁이라면
Member boy = new Person(&quot;남&quot;, &quot;주혁&quot;);</code></pre>
</li>
<li><p>여기서 만일 두 멤버 변수, firstName과 lastName의 선언 순서가 바뀐다면 ?</p>
</li>
<li><p><code>@AllArgsConstructor</code> 는 선언된 필드의 순서대로 생성자의 파라미터 순서를 정해 만들어주기 때문에 &quot;주혁남&quot;이 될 수 있다..</p>
</li>
<li><p>이런 문제를 미연에 방지하기 위해 다음과 같이 @builder 패턴으로 파라미터의 순서가 아닌 필드 명으로 값을 설정하도록 한다. 단순히 유연한 생성을 위해서만 builder를 사용하는 것이 아니라는 것을 알 수 있다 !</p>
<pre><code class="language-java">public static class Person {
 private String firstName;
 private String lastName;

 @Builder
 private Person(String firstName, String lastName){
     this.firstName = firstName;
     this.lastName = lastName;
 }
}
// 필드 순서를 변경해도 한국식 이름이 만들어진다.
Person me = Person.builder().lastName(&quot;현수&quot;).firstName(&quot;권&quot;).build();
System.out.println(me);</code></pre>
<ul>
<li>그렇기 때문에 위에서 설명한 <code>@AllArgsConstructor</code> 과 <code>@Builder</code>를 같이 쓰는 것보다, <code>@NoArgsConstuctor</code> 과 생성자를 직접 만들어 그 생성자에 <code>@Builder</code> 을 붙이는 전략을 권장하는 것이다 !<br>

</li>
</ul>
</li>
</ul>
<h3 id="requiredconstructor"><code>@RequiredConstructor</code></h3>
<ul>
<li><p>&#39;Required&#39; 즉, 꼭 필요한 객체의 변수를 인수로 받는 생성자를 구현해준다. 여기서 꼭 필요한 객체의 변수는 <strong>final 또는 @NotNull 어노테이션이 붙은 변수</strong>를 의미한다.</p>
</li>
<li><p>이 어노테이션과 함께 보면 좋을 개념은 바로 <code>@Autowired</code> 어노테이션이다.</p>
<ul>
<li><p><code>@Autowired</code> 을 사용해서 의존성을 주입해주는 것을 <strong>필드 주입</strong>이라고 한다.</p>
<pre><code class="language-java">@Service
public class MemberService {
   @Autowired private MemberRepository memberRepository;

               /* 이하 생략 */
   }</code></pre>
<ul>
<li>이런 필드 주입 방식은 코드가 간결하고 사용하기 편리하지만 단점이 있어서 필드 주입 방식은 지양하고, 생성자 주입 방식을 지향한다. 그 이유는 다음 아티클이 잘 설명해주는 것 같아서 링크를 첨부한다 !<ul>
<li>🖇 <a href="https://madplay.github.io/post/why-constructor-injection-is-better-than-field-injection">생성자 주입을 @Autowired를 사용하는 필드 주입보다 권장하는 하는 이유</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p>따라서 생성자 주입을 해주어야 하는데 이때 필드 객체에 final 키워드를 붙이면 컴파일 시점에 해당 필드를 주입하지 않을 시에 누락된 의존성 오류를 체크할 수 있다.</p>
</li>
<li><p>여기서 final 키워드와 지금 설명하고 있는 <code>@RequiredConstructor</code> 를 같이 쓰면 좋은 이유가 설명되는 것이다 !</p>
</li>
<li><p><code>@RequiredConstructor</code> 가 final 변수를 위한 필수 필드를 정의하는 생성자를 대신 생성해주기 때문에 다음과 같이 코드를 작성하는 것이 바람직한 방법이다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class OrderService {

   private final OrderRepository orderRepository;
   private final MemberRepository memberRepository;

   public Long order(Long memberId, Long itemId) {
       Member member = memberRepository.findById(memberId).get();
       ...
       }
}</code></pre>
</li>
<li><p>Spring 컨테이너는 생성자가 1개인 경우 @Autowired를 생략하면 자동으로 생성자 주입을 해준다. 위의 OrderService 클래스는 필수 필드를 가지는 <code>@RequiredArgsConstructor</code> 생성자가 1개 있으므로 <code>@Autowired</code> 를 생략하고 다음과 같은 코드로 생성자 주입을 해주는 것이 가능하다.</p>
</li>
<li><p>즉, 다른 클래스의 의존성이 주입되어야 하는 클래스 (예를 들면 Service나 Controller..) 는 위와 같이 <code>final</code> 키워드와 <code>@RequiredArgsConstructor</code> 을 함께 사용해주는 것이 좋다 !</p>
</li>
</ul>
<br>


<blockquote>
<p> 💡 다음과 같이 Lombok을 이용해서 생성자를 쉽게 만들 수 있는 어노테이션에 대해 알아보았다.
그냥 무작정 어노테이션부터 붙이고 개발을 하는 것이 아니라, 현재 <strong>내가 설계하고 있는 클래스의 특징과 상황을 고려하여 (Entity인지 Service인지..등등) 어떤 부분을 주의해야하는지 판단하고, 알맞고 유연한 설계를 하는 것</strong>이 중요한 것 같다 !</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Node.js - Express와 라우터]]></title>
            <link>https://velog.io/@ye_suri_106/Express%EC%99%80-%EB%9D%BC%EC%9A%B0%ED%84%B0</link>
            <guid>https://velog.io/@ye_suri_106/Express%EC%99%80-%EB%9D%BC%EC%9A%B0%ED%84%B0</guid>
            <pubDate>Fri, 14 Oct 2022 11:16:34 GMT</pubDate>
            <description><![CDATA[<h2 id="1-express">1. Express</h2>
<blockquote>
<p><strong>Express 란 ?</strong>
Express.js, 또는 간단히 익스프레스는 Node.js를 위한 웹 프레임워크의 하나로, MIT 허가서로 라이선스 되는 자유-오픈 소스 소프트웨어로 출시되었다. 웹 어플리케이션, API 개발을 위해 설계되었다. <em><strong>Node.js의 사실상의 표준 서버 프레임워크로 불리고 있다.</strong></em></p>
</blockquote>
<p>본격적으로 서버를 구축하기 위해서는 이 express 프레임워크를 사용해야한다.
express를 사용하기 위해서는 패키지 매니저를 먼저 설치해야된다.
패키지 매니저로는 <strong>yarn</strong>을 사용한다.</p>
<h3 id="1-1-yarn-first-before-express">1-1. Yarn first before Express</h3>
<blockquote>
<p><strong>Yarn 이란?</strong>
yarn은 Node.js 자바스크립트 런타임 환겨을 위해 페이스북이 2016년 개발한 소프트웨어 패키지 시스템이다. <em><strong>npm 패키지 관리자의 대안</strong></em> 으로서 Yarn은 페이스북, Exponent, 구글, Tilde의 협업으로 대형 코드의 일관성, 보안, 성능 문제를 해결하고자 개발되었다.</p>
</blockquote>
<p>기존에 사용하던 npm을 보완해서 새롭게 등장한 패키지 매니저이다.</p>
<h3 id="1-2-yarn-설치">1-2. yarn 설치</h3>
<p>아주 간단하다.</p>
<pre><code>//설치 진행 (오류 발생할 경우는 대부분 권한.. 명령문 앞에 sudo 붙여주면 해결)

&gt; npm i -g yarn

//설치 잘 되었는지 버전 확인
&gt; yarn --version</code></pre><p>버전 확인 오류 없이 잘 된다면 성공 !!</p>
<h3 id="1-3-express-개발-환경-세팅">1-3. Express 개발 환경 세팅</h3>
<p><strong>1) yarn 세팅</strong></p>
<p>작업 경로에 이제 express로 서버 작업을 할 폴더를 하나 만들어준다.</p>
<p>(나는 임의로 <code>express-start</code> 라는 이름의 폴더를 만들어서 그 곳에서 작업해보겠댱)</p>
<p>그리고 이 디렉토리로 이동해서 터미널에 명령어 입력 !</p>
<pre><code>&gt; yarn init</code></pre><p>그럼 이제 터미널에 서버에 대한 간단한 사전 설정을 하도록 다라라락 뜨게 된다.
<code>name</code>은 원하는대로 적어주면 되고 나머지는 엔터 눌러주면 된다.</p>
<pre><code>yarn init v1.22.18
question name (seminar2-express): express-example
question version (1.0.0): 
question description: 
question entry point (index.js): 
question repository url: 
question author: 
question license (MIT): 
question private: 
success Saved package.json
✨  Done in 12.78s.</code></pre><p>이 과정까지 해주면 디렉토리에 <code>package.json</code> 파일이 자동적으로 생성된다.</p>
<blockquote>
<p>💡 설치한 패키지의 버전을 관리하는 파일이 바로 <code>package.json</code> 이다. <br> 따라서 <strong>노드 프로젝트를 시작하기 전에는 폴더 내부에 무조건 package.jos 부터 만들고 시작해야 한다.</strong></p>
</blockquote>
<p><strong>2) Express 설치</strong></p>
<p>앞에서 사용했던 패키지 매니저 yarn을 이용해서 이제부터 사용할 express 패키지를 설치하면 된다.</p>
<pre><code>yarn add express &amp;&amp; yarn add -D @types/node @types/express nodemon</code></pre><ul>
<li><p><code>@types/node</code> 와 같이 <code>@types</code> 가 붙은 것은 typescript용 모듈을 설치하는 것이다.</p>
</li>
<li><p><code>nodemon</code> 은 서버 코드에서 수정 사항이 생겨도 개발자가 직접 재가동하지 않도록 자동으로 재시작 해주는 좋은 모듈이다. </p>
</li>
<li><p>nodemon은 개발용으로 사용하는 것을 권장한다. 배포 후에는 서버 코드가 빈번하게 변경될 일이 없으므로 nodemon을 사용하지 않아도 된다. <br></p>
</li>
</ul>
<p><strong>3) tsconfig.json 파일 생성</strong></p>
<p>Typescript 기반의 서버를 구축하기 위해서는 필수적인 파일이다.</p>
<p>TS 파일을 JS 파일로 컴파일해서 실행한다는 것은 1차 세미나에서 배웠다.</p>
<p>여기서 <code>tsc</code> 명령어와 <code>ts-node</code>의 차이까지 배웠었다.</p>
<blockquote>
<p><code>tsc</code> : Production의 컴파일 단계에서 주로 사용<br>
<code>ts-node</code>: 간단한 개발 단계에서의 컴파일에 주로 사용 (build 파일이 필요 없기 때문에)</p>
</blockquote>
<p><code>tsc</code> 의 설정 즉, TS를 JS로 컴파일하는 옵션을 설정할 수 있는 파일이 바로 <code>tsconfig.json</code> 파일이다.</p>
<p>이어서 다음 명령어를 <strong>현재 작업 폴더 내의 터미널</strong>에서 입력해준다.</p>
<pre><code>&gt; tsc --init</code></pre><p>이후에 <code>tsconfig.json</code> 파일이 생성되는데 이 파일을 아래의 내용으로 바꿔준다.</p>
<pre><code class="language-javascript">// tsconfig.json
{
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;es6&quot;, //? 어떤 버전으로 컴파일
    &quot;allowSyntheticDefaultImports&quot;: true, //? default export가 없는 모듈에서 default imports를 허용
    &quot;experimentalDecorators&quot;: true, //? decorator 실험적 허용
    &quot;emitDecoratorMetadata&quot;: true, //? 데코레이터가 있는 선언에 대해 특정 타입의 메타 데이터를 내보내는 실험적인 지원
    &quot;skipLibCheck&quot;: true, //? 정의 파일 타입 체크 여부
    &quot;moduleResolution&quot;: &quot;node&quot;, //? commonJS -&gt; node 에서 동작
    &quot;module&quot;: &quot;commonjs&quot;, //? import 문법
    &quot;strict&quot;: true, //? 타입 검사 엄격하게
    &quot;pretty&quot;: true, //? error 메시지 예쁘게
    &quot;sourceMap&quot;: true, //? 소스맵 파일 생성 -&gt; .ts가 .js 파일로 트랜스 시 .js.map 생성
    &quot;outDir&quot;: &quot;./dist&quot;, //? 트랜스 파일 (.js) 저장 경로
    &quot;allowJs&quot;: true, //? js 파일 ts에서 import 허용
    &quot;esModuleInterop&quot;: true, //? ES6 모듈 사양을 준수하여 CommonJS 모듈을 가져올 수 있게 허용
    &quot;typeRoots&quot;: [
      &quot;./src/types/express.d.ts&quot;, //? 타입(*.d.ts)파일을 가져올 디렉토리 설정
      &quot;./node_modules/@types&quot; //? 설정 안할시 기본적으로 ./node_modules/@types
    ]
  },
  &quot;include&quot;: [
    &quot;./src/**/*&quot; //? build 시 포함
  ],
  &quot;exclude&quot;: [
    &quot;node_modules&quot;, //? build 시 제외
    &quot;tests&quot;
  ]
}</code></pre>
<p>해당 설정값은 다 외우지 않아도 된다. 주석을 꼼꼼하게 정말 잘 달아주신 sopt 30th 서버 팟장님께 감사드린다...
최고인 것 같다..👍</p>
<h2 id="2-express로-서버-구축하기">2. express로 서버 구축하기</h2>
<h3 id="1-indexts-파일-만들기">1) index.ts 파일 만들기</h3>
<p>현재 디렉토리에 <code>src</code> 폴더를 만들고 그 내부에 <code>index.ts</code> 파일을 만들어주자. </p>
<p>이는 서버의 역할을 하는 ts 파일이다. 파일명으로는 app.ts 도 많이 쓰는 것 같다.</p>
<pre><code class="language-typescript">//index.ts는 user의 request가 가장 먼저 도달하는 ts이다.

import express, {NextFunction, Request, Response } from &quot;express&quot;;

const app = express(); // express 객체를 받아옴
const PORT = 3000; // 사용할 port를 3000번으로 설정
app.use(express.json()); // express 에서 request body를 json 으로 받아오겠다.

app.use(&quot;/api&quot;, require(&quot;./api&quot;)); // use -&gt; 모든 요청
//localhost:3000/api -&gt; 앞으로 만들어줄 api 폴더의 ts 파일들과 연결하는 url
//ex) localhost:3000/api/user -&gt; api/user.ts 파일과 연결

//* HTTP method - GET
app.get(&quot;/&quot;, (req: Request, res: Response, next: NextFunction) =&gt; {
    res.send(&quot;2차 세미나 안 듣고 과제 하려니까 죽을 맛이다...&quot;);
});

app.listen(PORT, () =&gt; {
    console.log(`
        ###############################################
            🛡️ Server listening on port: ${PORT} 🛡️
        ###############################################
    `);
}); // 3000 번 포트에서 서버를 실행하겠다!</code></pre>
<p> 짚고 넘어가야 할 부분은 <strong>app.use(&quot;/api&quot;, require(&quot;./api&quot;));</strong> 인데, 여기서 바로 <strong>미들웨어</strong> 라는 개념이 등장하기 때문이다 !</p>
<p> *<em>📌 여기서 잠깐 ! 미들웨어 ? *</em></p>
<blockquote>
<p><em><strong>익스프레스의 핵심은 미들웨어</strong></em> 이다. 요청과 응답의 중간에 위치하여 미들웨어라고 한다. 뒤에 나오는 라우터와 <del>(에러 핸드러)</del> 또한 미들웨어의 일종이다. 미들웨어는 요청과 응답을 조작하여 기능을 추가하기도 하고, 나쁜 요청은 거르기도 한다.</p>
</blockquote>
<p>이런 미들웨어는 app.use 와 함께 사용된다. </p>
<pre><code class="language-typescript">app.use((req: Request, res: Response, next: NextFunction) =&gt; {
    res.send(&quot;모든 요청에서 다 실행됩니다 ...&quot;);
});

app.get(&quot;/&quot;, (req: Request, res: Response, next: NextFunction) =&gt; {
      console.log(&quot;GET / 요청에서만 실행됩니다 ...&quot;);
      next();
}, (req: Request, res: Response) =&gt; {
  throw new Error(&quot;에러는 에러 처리 미들웨어로 갑니다 ...&quot;)
});

app.use((err: Error, req: Request, res: Response, next: NextFunction) =&gt; {
  console.error(err);
  res.status(500).send(err.message);
});
</code></pre>
<p>미들웨어는 위에서부터 아래로 순서대로 실행되면서 요청과 응답 사이에 특별한 기능을 추가할 수 있다.</p>
<p>여기서 next 라는 세번째 매개변수는 다음 미들웨어로 넘어가는 함수이다. next를 실행하지 않으면 다음 미들웨어가 실행되지 않는다.</p>
<p>주소를 첫번째 인수로 넣어주지 않는다면 미들웨어는 모든 요청에서 실행되고, 주소를 넣는다면 해당하는 요청에서만 실행된다고 보면 된다.</p>
<ul>
<li><code>app.use(미들웨어)</code> : 모든 요청에서 미들웨어 실행</li>
<li><code>app.use(&#39;/abc&#39;, 미들웨어)</code> : abc로 시작하는 요청에서 미들웨어 실행</li>
<li><code>app.post(&#39;/abc&#39;, 미들웨어)</code> : abc로 시작하는 POST 요청에서 미들웨어 실행</li>
</ul>
<p>app.use나 app.get 같은 라우터에 미들웨어를 여러 개 장착할 수도 있다. <del>(router의 개념은 밑에서 자세히 설명한다.)</del> 다만 이때도 next를 호출해야 다음 미들웨어로 넘어간다.</p>
<p>현재 <code>app.get(&#39;/&#39;)</code> 의 두번째 미들웨어에서 에러가 발생하고, 이 에러는 그 아래에 있는 에러 처리 미들웨어에 전달된다.</p>
<p><strong>에러 처리 미들웨어</strong> 는 매개변수가 <code>err, req, res, next</code> 로 반드시 4개를 써줘야 한다. 에러 처리 미들웨어를 직접 연결하지 않아도 기본적으로 익스프레스가 에러를 처리하지만 실무에서는 직접 연결해주는 것이 좋다.</p>
<h3 id="2-nodemonjson-파일과-packagejson-파일-수정하기">2. nodemon.json 파일과 package.json 파일 수정하기</h3>
<p><code>src</code> 폴더에서 나가서 작업 디렉토리 (<code>express-start</code> 폴더) 에서 <code>nodemon.json</code> 을 생성한다.</p>
<pre><code>  {
    &quot;watch&quot;: [&quot;src&quot;, &quot;.env&quot;],
    &quot;ext&quot;: &quot;js,ts,json&quot;,
    &quot;ignore&quot;: [&quot;src/**/*.spec.ts&quot;],
    &quot;exec&quot;: &quot;ts-node  --transpile-only ./src/index.ts&quot;
  }</code></pre><p>위와 같이 작성해준 뒤, 저장해준다. 위에서 정해준 설정값을 간단히 설명하자면 아래와 같다 !</p>
<ol>
<li><code>watch</code>
: 이 파일 들의 코드 변경이 감지되면 서버를 재시작하겠다.</li>
<li><code>exec</code>
: 감지되었을 때 수행할 명령</li>
<li><code>ext</code>
: extension, 파일확장자를 적어준다.</li>
<li><code>ignore</code>
: 이거는 감지하지 않음.</li>
</ol>
<p>그 다음으로는  <code>package.json</code> 파일을 살짝 수정해주어야 한다.
개발환경에서는 실제 실행을 도와줄 명령 스크립트를 프로젝트의 package.json 의 run script에 등록해놓는다.</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/e7a0a740-179b-4ef6-8ba0-e25830b150ef/image.png" alt=""></p>
<p>위의 사진에 드래그 해놓은 부분을 추가해주면 된다. 이렇게 <code>package.json</code> 파일까지 수정하고 나면 아래와 같은 명령어로 실행할 수 있다.</p>
<p>  <code>yarn run dev</code> : nodemon을 기반으로 서버 실행
  <code>yarn run build</code> : TS 프로젝트를 JS로 빌드</p>
<p>그렇다면 서버를 실행해보자 !! 터미널에 위의 명령어인 <code>yarn run dev</code> 를 치게 되면, <img src="https://velog.velcdn.com/images/ye_suri_106/post/bbbaf337-7be7-4a45-8462-aea37ca67cfe/image.png" alt="">위와 같이 <code>nodemon</code> 을 통해서 서버를 잘 구동시킬 수 있다.
그리고 이제 localhost:3000으로 접속해보면 <img src="https://velog.velcdn.com/images/ye_suri_106/post/1a652241-d0da-4b9f-b8b9-affd41df3915/image.png" alt="">서버가 아주 잘 돌아가고 있댱 ! 😎</p>
<p>nodemon의 장점이 무엇이었는가,, 서버 코드에서 수정 사항이 생겨도 개발자가 직접 재가동하지 않도록 자동 재시작을 해주는 아이다.. 그렇다면 코드를 바꿔볼까 ?? <img src="https://velog.velcdn.com/images/ye_suri_106/post/65409ac9-7679-47e0-a975-40c633bc3049/image.png" alt=""> 위와 같이 변화를 아주 잘 감지해서 서버를 자동으로 재시작해준다 ! 물론 localhost:3000도 새로 고침하면 변동 사항이 적용되어 보인다 !</p>
<p>이후 이 서버를 실제로 배포할 때는 TS 파일이 아닌 JS 서버 파일이 필요하다. 이 때 build 폴더를 생성해줄 <code>yarn run build</code> 를 사용하면 되는 것이다.
그럼 작업 디렉토리에 js 트랜스파일과 sourceMap 파일이 생성될 것이다 !</p>
<hr>
<h2 id="2-router">2. Router</h2>
<p>위에서 살짝 언급했었던 라우터,, 라우터가 대체 뭐야 ❓❓❓</p>
<blockquote>
<p><strong>클라이언트의 요청 경로(path)를 보고 이 요청을 처리할 수 있는 곳으로 기능을 전달해주는 역할</strong>을 한다. 이러한 역할을 라우팅이라고 하는데, 애플리케이션 엔드 포인트 (URI)의 정의, 그리고 URI가 클라이언트 요청에 응답하는 방식을 의미한다. </p>
</blockquote>
<p>예를 들어, 클라이언트가 <code>/users</code> 경로로 요청을 보낸다면 이에 대한 응답 처리를 하는 함수를 별도로 분리해서 만든 다음 <code>get()</code> 메소드를 호출하여 라우터로 등록할 수 있다.</p>
<p>말로 하면 이해가 더 어려운거 같다.. 직접 실습을 통해 라우터를 이해해보자 !
<br></p>
<hr>
<br>


<h2 id="3-express-에서-router-구현하기">3. Express 에서 router 구현하기</h2>
<p>하나의 서버를 직접 구축하는 예시로 설명을 하면 이해가 쉬울 것 같아서,
이번 SOPT 2차 세미나의 도전 과제로 express 에서 router를 구현하는 방법을 설명하고자 한다 !
<br><strong>👇 이번 2차 세미나의 도전 과제의 내용은 다음과 같았다</strong></p>
<blockquote>
<p>📌 Express 프로젝트를 만들고, 다음 5개의 라우팅을 구성해보기
/api/user
/api/blog
/api/comment
/api/movie
/api/members ( 1차 세미나때 작업했던 파트원들 정보를 Response )
(라우팅 형식은 자유입니다. 여러분의 입맛에 맞게 작성하시고 코드 리뷰팀과 이야기 나눠보세요!)</p>
</blockquote>
<p>이 과제를 통해 Express에서 라우팅 하는 방법에 대해 정리하고자 한다.</p>
<h3 id="1-디렉토리-구조">1. 디렉토리 구조</h3>
<p>Express를 사용하는 이유 중 하나가 Express에서는 라우터를 분리할 수 있는 방법을 제공하기 때문에 라우팅을 깔끔하게 관리할 수 있다는 점이다.</p>
<p>또한 라우터 분리 구조로 구현하면, 쉽게 위치를 찾고 유지 보수에 용이하다.</p>
<img width="153" alt="image" src="https://user-images.githubusercontent.com/68415644/195842988-f8eaa8e7-6c93-4a67-afbe-e88cfdd0c4b6.png">

<blockquote>
<ul>
<li><strong>api 폴더</strong> : 요청/응답에 대한 로직을 수행하는 api를 묶어놓은 폴더</li>
</ul>
</blockquote>
<ul>
<li><strong>router 폴더</strong> :  api 폴더에 있는 api들의 라우팅을 담당하는 폴더</li>
</ul>
<h3 id="2-express-객체에-각-라우터를-분기해줄-index-라우터-연결">2. express 객체에 각 라우터를 분기해줄 index 라우터 연결</h3>
<p>우리는 지금 5개의 라우팅을 하는 것이 목표이다.
그렇다면 src/index.ts 파일에서 이 5개의 라우터를 각각 연결해줘야 할까 ?</p>
<p>❗️** NO <strong>❗️ 
**<em>간단하게 하나의 /router/index.ts 파일을 만들어서 여기서 각 라우터들로 분기를 한번에 해주면 더 깔끔하게 구현할 수 있다.</em></strong></p>
<p><strong>☑️ 1. src / index.ts</strong> <img src="https://user-images.githubusercontent.com/68415644/195846770-c259690b-1603-4bcc-b9da-9e05a6748efe.png" alt="image">index.ts 파일에서  <code>.use</code> 함수를 이용해서 <code>/api</code> 로 시작하는 요청에서 <strong>indexRouter</strong> 라우터 객체를 연결하도록 구현했다. </p>
<p>여기서 <code>indexRouter</code> 객체는 <strong><code>./router/index</code></strong> 에서 export 하는 라우터 모듈이다. </p>
<p><strong>indexRouter</strong> 객체가 뭔데 ?! <code>./router/index.ts</code> 을 살펴보자.
<br></p>
<p><strong>☑️ 2. src / router / index.ts</strong></p>
<img width="402" alt="image" src="https://user-images.githubusercontent.com/68415644/195847128-434cf47e-48d3-4ff7-9553-fec38c5f3469.png">

<p>우리는 router 폴더 안에 총 5개의 라우터 파일을 만들 것이다. 이를 이 index.ts 파일에서 모두 import 한 뒤 라우터 객체를 하나 만들고, <code>.use</code> 함수를 사용하여 각 라우터가 연결될 요청 주소와 라우터 객체를 넘겨준다. </p>
<p>그렇게 되면
<code>/api/user</code> 로 시작하는 요청은 userRouter 객체를 연결하고 ... <code>/api/blog</code> 로 시작하는 요청은 blogRouter 객체를 연결하고 ... <del>(이하생략)</del> ...</p>
<p>💡 다시 말해, <code>/api</code> 를 요청하면 이 indexRouter가 연결되고, <strong>여기서 이제 각 5개의 라우터로 분기되는 것이다 !</strong></p>
<p>이렇게 5개로 분기시키는 라우터를 마지막에 export router로 모듈화 해서 내보내므로 <code>src/index.ts</code> 에서 <code>indexRouter</code> 로 받을 수 있는 것이다 !
<br>
<strong>☑️ 3. src / router / userRouter.ts</strong></p>
<p>이제 각 라우터 파일을 봐보자. 5개의 형식은 다 똑같아서 userRouter.ts 만 보면 이해가 될 것 같다 !
<img src="https://velog.velcdn.com/images/ye_suri_106/post/e0de9c32-5c06-4b4b-b4a7-75aeaf763e68/image.png" alt=""></p>
<p>여기서는 이제 이 라우터가 수행할 api와 연결을 해주어야 한다.</p>
<p>HTTP 메소드에 따라 수행하는 api가 다를 것이므로 여기서 여러 개의 HTTP 메소드 (.get(), .post(), .put(),,, 등등) 을 연결해주면 된다.</p>
<p>우리는 이런 api들도 따로 폴더를 만들어 분리해서 구현할 것이기 때문에 3번 line처럼 모듈을 import해서 사용한다.</p>
<p>이후 <em><strong>HTTP 메소드에 알맞는 라우팅 메소드 (여기서는 <code>.get()</code>)에 라우팅 경로와 수행할 api 모듈을 넘겨주여 연결해준다.</strong></em> <em><strong>-&gt; 7번 코드라인</strong></em></p>
<blockquote>
<p>📌  <strong>잠깐 ! 라우팅 경로 ?</strong><br>
라우팅 경로는 <em><strong>요청 메서드와의 조합으로 요청이 이루어질 수 있는 엔드포인트를 정의한다</strong></em>. 라우트 경로는 문자열, 문자열 패턴, 정규식이 될 수 있다.<br>
정리하자면 !
서버 개발에서는 통상적으로 엔드포인트를 정의하고, 해당 엔드포인트의 요청에 대해 어떻게 응답할지 정하는 것을 <strong>라우팅</strong> 이라고 말한다.
이 때 <strong>엔드포인트는 uri</strong>로, <strong>요청 방식은 http 메서드</strong> 가 되는 것이다.<br>
단순히 <strong>라우팅 경로</strong> 라고 표현한다면 <strong><em>엔드포인트만이 경로에 해당하는 정보</em></strong>이고, <strong>그 경로에서 들어오는 &quot;어떤 요청&quot;에 대한 응답인지가 http 메소드</strong>가 되는 것이다.</p>
</blockquote>
<p>마지막으로 이 라우터를 export 해서 indexRouter에서 이 라우터 모듈을 import 해서 분기할 수 있게 구현하면 되는 것이다.</p>
<h3 id="3-api-폴더-안에-모든-ts-파일들에서-함수-모듈화">3. api 폴더 안에 모든 ts 파일들에서 함수 모듈화</h3>
<p>결국 라우터는 각 요청 메서드와 요청 경로가 있을 때 어떤 로직을 수행하면 되는지 중간에서 연결해주는 것이라고 생각할 수 있겠다.</p>
<p>이런 로직을 api 라고 하는데 이를 구현하는 ts 파일들도 따로 api 폴더를 구분해서 구현하는 것이 좋다 !</p>
<p>이것도 함수 내용만 변경이 있을 뿐 전체적인 구조는 같기 때문에 <strong>api/movie.ts</strong> 만 설명하면 될 것 같다 !</p>
<p><strong>☑️ src / api / movie.ts</strong>
<img src="https://velog.velcdn.com/images/ye_suri_106/post/e8b61a00-2beb-431b-acf7-e74556c19e36/image.png" alt=""></p>
<p>코드를 보면 <code>getMovie</code> 라는 함수를 하나 만들게 된다. 이 때 Request 객체와 Response 객체를 파라미터로 받는다.</p>
<p>여기서 이 getMovie는 <em><strong>프론트엔드가 요청한 movie의 정보를 json 형태로 보내주는 api</strong></em> 이다. 따라서 Response 객체, 즉 res에 json 형태로 정보를 담아 return 해주도록 구현하는 것이다.</p>
<p>이후 이 함수를 모듈화 하여 export하고 이를 위에 설명했듯이, 각 라우터 파일에서 import 해서 라우팅 메소드에 넘겨주면 되는 것이다.</p>
<h3 id="4-각-요청-경로에-따른-실행-결과">4. 각 요청 경로에 따른 실행 결과</h3>
<p>다음과 같이 라우팅이 잘 된 것을 확인 할 수 있다 !!!!</p>
<ul>
<li><strong>localhost:3000/api/user</strong></li>
</ul>
<p><img src="https://user-images.githubusercontent.com/68415644/195851512-dc0dc3f5-f6ea-463b-ade8-5ace68abbdc3.png" alt="image"></p>
<ul>
<li><strong>localhost:3000/api/blog</strong></li>
</ul>
<p><img src="https://user-images.githubusercontent.com/68415644/195851755-6c48d80d-18d5-44d4-9ff9-a3c6c1aad1a1.png" alt="image"></p>
<ul>
<li><strong>localhost:3000/api/comment</strong></li>
</ul>
<p><img src="https://user-images.githubusercontent.com/68415644/195851864-da3108ee-dcc4-4f9f-a7ca-3c4b48b0e709.png" alt="image"></p>
<ul>
<li><strong>localhost:3000/api/movie</strong></li>
</ul>
<p><img src="https://user-images.githubusercontent.com/68415644/195851934-cb168ceb-a0e2-4d12-9786-f796f2692393.png" alt="image"></p>
<ul>
<li><strong>localhost:3000/api/members</strong></li>
</ul>
<p><img src="https://user-images.githubusercontent.com/68415644/195852024-b5a34dd5-c1d7-43f6-830c-7e041c6b3c92.png" alt="image"></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[REST API]]></title>
            <link>https://velog.io/@ye_suri_106/REST-API</link>
            <guid>https://velog.io/@ye_suri_106/REST-API</guid>
            <pubDate>Wed, 12 Oct 2022 14:08:09 GMT</pubDate>
            <description><![CDATA[<p><strong>REST</strong> 는 Representational State Transfer 의 줄임말이다.
즉, 서버의 <strong>리소스(자원)을 정의하고 리소스에 대해 주소를 지정하는 방법</strong>을 말한다.
이러한 리소스 지향 아키텍쳐를 <strong><em>모든 것을 가급적이면 리소스를 중심으로, 명사로 표현하는 것</em></strong>으로 이야기 할 수 있.</p>
<p>말이 어렵다.. 조금 더 쉬운 설명을 가져와봤다 !....</p>
<p>서버에 요청을 보낼 때는 주소를 통해 요청의 내용을 표현한다. 주소가 /index.html이면 서버의 index.html을 보내달라는 뜻이고, /about.html이면 about.html을 보내달라는 뜻이다.</p>
<p>항상 html을 요청할 필요는 없다. css나 js 또는 이미지 같은 파일을 요청할 수도 있고 특정 동작을 요청할 수도 있다. 요청의 내용이 주소를 통해 표현되므로 서버가 이해하기 쉬운 주소를 사용하는 것이 좋은데, 이때 등장하는 개념이 바로 <strong>REST</strong> 이다!</p>
<p>이런 REST 아키텍쳐를 준수하는 API가 바로 <strong>REST API</strong> 이다 !
(그리고 이런 REST를 따르는 서버를 <strong>RESTful하다</strong> 라고 표현한다. <strong><em>우리는 최대한 RESTful한 API를 만드는 것에 집중하는 것이 중요하다.</em></strong>)</p>
<blockquote>
<p><strong>아니 그럼 API는 또 뭔데?</strong>
<em>Application Programming Interface</em> 의 줄임말이다.
API는 응용 프로그램에서 사용할 수 있도록, 운영 체제나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스를 뜻한다.</p>
</blockquote>
<p>본격적으로 <strong>REST API</strong>에 대해서 알아보자 !</p>
<hr>

<h3 id="1-rest-api-의-핵심">1. REST API 의 핵심</h3>
<p>REST API 설계 시 가장 핵심은 다음 두 가지이다.</p>
<blockquote>
<p>💡 <strong>자원 (resource)</strong> - URI는 정보의 자원을 표현해야 한다.
 💡 <strong>행위 (verb)</strong> - 자원에 대한 행위를 HTTP Methods(GET, POST, PUT, DELETE)로 표현한다.</p>
</blockquote>
<h3 id="2-rest-api-핵심-규칙">2. REST API 핵심 규칙</h3>
<h4 id="1-uri는-정보의-자원을-표현해야-한다-리소스명은-동사보다는-명사를-사용한다">1) URI는 정보의 자원을 표현해야 한다. (리소스명은 동사보다는 명사를 사용한다.)</h4>
<pre><code>DELETE /members/update/1</code></pre><p>위와 같은 방식은 REST를 제대로 적용하지 않은 URI이다. URI는 자원을 표현하는데 중점을 두어야 한다. update와 같은 행위에 대한 표현이 들어가서는 안된다.</p>
<h4 id="2-자원에-대한-행위는-http-methodget-post-put-delete-등로-표현해야-한다">2) 자원에 대한 행위는 HTTP Method(GET, POST, PUT, DELETE 등)로 표현해야 한다.</h4>
<p>위의 잘못 된 URI를 HTTP Method를 통해 수정해 보면 아래와 같다.</p>
<pre><code>DELETE /members/1</code></pre><p>회원정보를 가져올 때는 GET, 회원 추가 시의 행위를 표현하고자 할 때는 POST METHOD를 사용하여 표현한다.</p>
<p><strong>- 회원정보를 가져오는 URI</strong></p>
<pre><code>GET /members/show/1     (x)
GET /members/1          (o)</code></pre><p><strong>- 회원을 추가할 때</strong></p>
<pre><code>GET /members/insert/2 (x)  - GET 메서드는 리소스 생성에 맞지 않습니다.
POST /members/2       (o)</code></pre><blockquote>
<p><strong>[참고]HTTP METHOD의 알맞은 역할</strong>
POST, GET, PUT, DELETE 이 4가지의 Method를 가지고 CRUD를 할 수 있다.<br></p>
</blockquote>
<ul>
<li><strong>POST</strong> : POST를 통해 해당 URI를 요청하면 리소스를 생성합니다.</li>
<li><strong>GET</strong> :    GET를 통해 해당 리소스를 조회합니다. 리소스를 조회하고 해당 도큐먼트에 대한 자세한 정보를 가져온다.</li>
<li><strong>PUT</strong> :    PUT를 통해 해당 리소스를 수정합니다.</li>
<li><strong>DELETE</strong> : DELETE를 통해 리소스를 삭제합니다.</li>
</ul>
<p>✔️ 이처럼 URI는 자원을 표현하는 데에 중점을 두고, 행위에 대한 정의는 HTTP METHOD를 통해 하는 것이 REST한 API를 설계하는 핵심 규칙이다</p>
<h3 id="3-uri와-url의-차이-">3. URI와 URL의 차이 ?</h3>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/82f41082-5a3a-4981-b39c-3fa6fe649a26/image.png" alt=""></p>
<h4 id="--uri">- URI</h4>
<p><strong>Uniform Resource Identifier</strong>
통합 자원 식별자 (자원을 나타내는 주소)
자원을 나타내는 유일한 주소</p>
<h4 id="--url">- URL</h4>
<p><strong>Uniform Resource Locator</strong>
통합 자원 지시자
특정 서버의 한 리소스에 대한 구체적인 위치 서술</p>
<p><strong><em>즉, URL ⊂ URI 라고 생각하면 된다 !</em></strong></p>
<p>자 그렇다면 여기서 Quiz !!
다음 세 가지는 <strong>URI</strong> 일까? <strong>URL</strong> 일까?
<a href="https://sopt.org/">https://sopt.org/</a>
<a href="https://github.com/IN-SOPT-SERVER">https://github.com/IN-SOPT-SERVER</a>
<a href="https://www.youtube.com/results?search_query=sopt">https://www.youtube.com/results?search_query=sopt</a></p>
<p>아놔 당근 URI가 범위가 더 크니까 셋 다 URI 겠지요 !!
그렇다면 이들 중 URL은 ??</p>
<p>정답은, 제일 하단에 있는 유튜브 링크를 제외하고 모두이다 !
제일 하단의 유튜브 예시는 리소스의 위치와는 상관이 없기 때문이다.</p>
<p><strong>URI는 식별, URL은 위치를 가르킨다</strong> 는 점만 잘 기억하면 된다.</p>
<h3 id="4-rest-api의-기준">4. REST API의 기준</h3>
<h4 id="4-1-기준">4-1) 기준</h4>
<ol>
<li>클라이언트, 서버 및 리소스로 구성되었으며 요청이 <strong>HTTP를 통해 관리되는 클라이언트-서버 아키텍처</strong></li>
<li><strong>스테이트리스(stateless) 클라이언트-서버 커뮤니케이션</strong><ul>
<li>요청 간에 클라이언트에 대한 데이터가 저장되지 않음</li>
<li>각 요청이 분리되어 있고 서로 연결되어 있지 않음</li>
</ul>
</li>
<li>클라이언트-서버 상호 작용을 간소화하는 <strong>캐시 가능 데이터</strong></li>
<li>요청된 정보를 검색하는 데 관련된 서버(보안, 로드 밸런싱 등을 담당)의 각 유형을 클라이언트가 볼 수 없는 계층 구조로 체계화하는 <strong>계층화된 시스템</strong></li>
<li>정보가 <strong>표준 형식으로 전송</strong>되도록 하기 위한 구성 요소 간 통합 인터페이스</li>
</ol>
<hr>
<h4 id="4-2-규칙">4-2) 규칙</h4>
<p>API의 라우팅을 직접해야하기 때문에 아래의 규칙을 꼭 기억해서 올바른 엔드포인트를 정의해야한다. </p>
<ol>
<li><code>/</code> 는 계층 관계를 표현합니다.</li>
<li><code>/</code> 는 URI 마지막에 포함하지 않습니다.<ul>
<li><a href="https://sopt.org">https://sopt.org</a> ( O )</li>
<li><a href="https://sopt.org/">https://sopt.org/</a>  ( X )</li>
</ul>
</li>
<li>가독성을 높이기 위해 불가피한 경우에는 <code>-</code> 를 사용합니다. <ul>
<li><a href="https://sopt.org/server-part">https://sopt.org/server-part</a> ( O )</li>
<li><a href="https://sopt.org/serverpart">https://sopt.org/serverpart</a> ( X )</li>
</ul>
</li>
<li>언더바(<code>_</code>)는 사용하지 않습니다.<ul>
<li><a href="https://sopt.org/server-part">https://sopt.org/server-part</a> ( O )</li>
<li><a href="https://sopt.org/server-part">https://sopt.org/server_part</a> ( X )</li>
</ul>
</li>
<li>소문자를 사용하는 것이 적합합니다.<ul>
<li><a href="https://sopt.org/server-part">https://sopt.org/server-part</a> ( O )</li>
<li><a href="https://sopt.org/server-part">https://sopt.org/serverPart</a> ( X )</li>
</ul>
</li>
<li>파일 확장자를 포함시키지 않습니다.<ul>
<li><a href="https://sopt.org/server.png">https://sopt.org/server.png</a> ( X )</li>
</ul>
</li>
<li>리소스 간의 연간 관계를 표현합니다.<ul>
<li><a href="https://sopt.org/members/">https://sopt.org/members/</a>{memberId}/device</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTP]]></title>
            <link>https://velog.io/@ye_suri_106/HTTP-hyn61jvv</link>
            <guid>https://velog.io/@ye_suri_106/HTTP-hyn61jvv</guid>
            <pubDate>Wed, 12 Oct 2022 13:34:22 GMT</pubDate>
            <description><![CDATA[<p>서버 개발자라면 기본으로 알고 있어야 할 개념 중 하나이다 !</p>
<p>HTTP를 알기 전에 서버의 개념을 먼저 간단하게 다시 보자.</p>
<h3 id="1-서버">1. 서버</h3>
<ul>
<li><strong>서버란?</strong> <blockquote>
<p>네트워크를 통해 클라이언트에 정보나 서비스를 제공하는 컴퓨터 또는 프로그램 </p>
</blockquote>
</li>
</ul>
<ul>
<li>웹이나 앱을 사용할 떼 데이터(아이디, 비밀번호, 이메일 등)와 서비스의 데이터가 생성되면, 이를 어딘가에 저장하고 클라이언트는 이 어딘가에서 데이터를 받아와야하는데 이곳이 바로 서버이다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/c6cf7a7b-acd5-499e-b243-e1f8256278c9/image.png" alt=""></li>
</ul>
<p>이런 서버의 동작을 하는 간단한 코드를 작성해보자.</p>
<h3 id="2-로컬에-서버-띄우기">2. 로컬에 서버 띄우기</h3>
<p>서버에는 요청을 받는 부분과 응답을 보내는 부분이 있어야 한다. <strong>요청과 응답은 이벤트 방식</strong> 이라고 생각하면 된다.
클라이언트로 부터 요청이 왔을 때 어떤 작업을 수행할지 이벤트 리스너를 미리 등록해두어야 한다.</p>
<pre><code class="language-javascript">const http = require(&quot;http&quot;); //기본으로 내장된 http 모듈

const port = 3000;

http
    .createServer((req, res) =&gt; { //인수로 요청에 대한 콜백 함수, 이 콜백 함수에 응답을 적으면 됨 !
        //req 객체: 요청(request)에 관한 정보, res 객체: 응답(response)에 관한 정보
        res.write(&quot;&lt;h1&gt;IN SOPT SERVER!&lt;/h1&gt;&quot;);
        res.end(&quot;&lt;p&gt;awesome&lt;/p&gt;&quot;);
    })
    .listen(port, () =&gt; { //서버 연결
        console.log(`${port} 번 포트에서 대기중 !`);
    });</code></pre>
<p>다음과 같은 코드를 작성 후 터미널에서 <code>node createServer.js</code> 명령어를 쳐보자.</p>
<pre><code>3000 번 포트에서 대기중 !</code></pre><p>라는 출력을 확인할 수 있다.
그렇다면 진짜 로컬에 서버를 띄우는 걸 성공한 것일까 ??</p>
<p>브라우저를 열고 <a href="http://localhost:3000">http://localhost:3000</a> (<a href="http://127.0.0.1:3000">http://127.0.0.1:3000</a>) 으로 접속해보자!
<img src="https://velog.velcdn.com/images/ye_suri_106/post/abf0a775-ace1-41db-8a6b-78ad6789522d/image.png" alt=""></p>
<p>짠. 성공한 것을 확인할 수 있다 !
명령어를 입력한 터미널에서 서버를 종료하고 싶다면 <code>Cmd + c</code> 를 누르면 된다.
(window의 경우는 <code>control + c</code>)</p>
<blockquote>
<p>📌 <em><strong>잠시만! localhost와 포트는 뭐야 근데 ?..</strong></em>
<br><strong>- localhost</strong> 란 현재 컴퓨터 내부 주소를 가리킨다. 외부에서는 접근할 수 없고 자신의 컴퓨터에서만 접근할 수 있으므로, 서버 개발 시 테스트용으로  많이 사용된다. localhost 대신 127.0.0.1을 주소로 사용해도 같다 ! <em>(이러한 숫자 주소를 IP라고 한다.)</em><br>
<strong>- 포트</strong>는 서버 내에서 프로세스를 구분하는 번호이다. 서버는 HTTP 요청을 대기하는 것 외에도 다양한 작업을 한다. 데이터베이스와 통신해야 하고, FTP 요청을 처리하기도 한다. 따라서 서버는 프로세스에 포트를 다르게 할당하여 들어오는 요청을 구분한다.<br>유명한 포트 번호는 21(FTP), 80(HTTP), 443(HTTPS), 3306(MYSQL)이 있다.<br>
그래서 <a href="https://github.com">https://github.com</a> 는 사실 포트 번호(443)가 생략되어 있는 것이다. <a href="https://github.com:443">https://github.com:443</a> 으로 요청해도 똑같이 깃허브 홈페이지에 접속한다.<br>
여기서 3000이란 포트 번호를 사용한 것은 충돌을 방지하기 위해서 ! 일반적으로 컴퓨터에서는 80번이나 443번 포트는 이미 다른 서비스가 사용하고 있을 확률이 크다. 따라서 예제를 실행할 때는 다른 포트 번호들을 사용하고, 실제로 배포할 때는 80번 또는 443번 포트를 사용한다.</p>
</blockquote>
<h3 id="3-http-개념">3. HTTP 개념</h3>
<blockquote>
<p><strong>HTTP란 ?</strong>
하이퍼 텍스트를 주고 받는 프로토콜(규칙)을 말한다.</p>
</blockquote>
<h4 id="1-request">1) Request</h4>
<p>서버에 어떠한 작업을 요청하는 날리는 것을 <strong>Request</strong> 를 한다고 표현한다.</p>
<p>Request를 할 때는 주소를 통해 요청의 내용을 표현한다. 여기서 발전하는 개념이 <strong>REST</strong> 인데 이 것은 다음 포스팅에서 자세히 설명하기로 한다.</p>
<p>여기서 주목해야할 점은 주소 이외에도 <strong>HTTP 요청 메서드</strong> 라는 것을 사용하여 요청하는 내용이 무엇인지를 명확히 전달한다는 것이다.</p>
<p>이 때 사용되는 HTTP의 9가지 메서드 중 주요 메서드는 5가지가 된다.</p>
<ul>
<li><strong>GET</strong> : 서버 자원을 가져오고자 할 때 사용 (조회)</li>
<li><strong>POST</strong> : 서버에 자원을 새로 등록하고자 할 때 사용 (생성)</li>
<li><strong>PUT</strong> : 서버의 자원을 요청에 들어 있는 자원으로 치환하고자 할 때 사용 (수정)</li>
<li><strong>PATCH</strong> : 서버 자원의 일부만 수정하고자 할 때 사용 (일부 수정)</li>
<li><strong>DELETE</strong> : 서버의 자원을 삭제하고자 할 때 사용 (삭제)</li>
</ul>
<h4 id="2-response">2) Response</h4>
<p>반대로 서버가 이런 요청을 받아 결과를 보내주는 것을 <strong>Response</strong> 한다고 표현한다.</p>
<p>여기서 알고 있으면 좋은 개념인 <code>Response Status</code> 를 살짝 보고 가자 ㅎ</p>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/a7fdc702-c30e-4cfd-8219-38e6e93a64b3/image.png" alt=""><del>저번 학기 웹 개발 수업을 들었을 때 진짜 팔짝 뛰는 화면..</del></p>
<p>이런 것들을 <strong>Status Code</strong> (상태 코드) 라고 한다.
나 또는 클라이언트에서 보낸 요청이 어떤 상태가 되었는지 코드로 알려주는 것이다.</p>
<p><strong>Status Code</strong>를 크게 나누면 다음과 같다.</p>
<ul>
<li><code>2xx</code> : 성공 상태</li>
<li><code>3xx</code> : 리다이렉션</li>
<li><code>4xx</code> : 요청 에러</li>
<li><code>5xx</code> : 서버 내부 에러</li>
</ul>
<p>자세한 코드는 다음과 같다. 
이는 클라이언트와 통신할 때 마주치기 쉬우니 잘 알아두는 것이 좋다 !</p>
<table>
<thead>
<tr>
<th>응답 코드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>200</td>
<td>성공 OK</td>
</tr>
<tr>
<td>201</td>
<td>성공, 리소스 생성 Created</td>
</tr>
<tr>
<td>204</td>
<td>성공, 응답 데이터 없음 No Content</td>
</tr>
<tr>
<td><br></td>
<td></td>
</tr>
</tbody></table>
<table>
<thead>
<tr>
<th>응답 코드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>400</td>
<td>요청을 이해할 수 없음 Bad Request</td>
</tr>
<tr>
<td>401</td>
<td>인증 필요 Unauthorized</td>
</tr>
<tr>
<td>403</td>
<td>요청 거부 Forbidden</td>
</tr>
<tr>
<td>404</td>
<td>리소스를 찾을 수 없음 Not Found</td>
</tr>
<tr>
<td>409</td>
<td>요청 충돌 Conflict</td>
</tr>
<tr>
<td><br></td>
<td></td>
</tr>
</tbody></table>
<table>
<thead>
<tr>
<th>응답 코드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>500</td>
<td>서버 내부 오류 Internal Server Error</td>
</tr>
<tr>
<td>503</td>
<td>일시적 서버 이용 불가능 Service unavailable</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[Node.js - 비동기/동기]]></title>
            <link>https://velog.io/@ye_suri_106/%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%8F%99%EA%B8%B0</link>
            <guid>https://velog.io/@ye_suri_106/%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%8F%99%EA%B8%B0</guid>
            <pubDate>Wed, 12 Oct 2022 12:20:40 GMT</pubDate>
            <description><![CDATA[<h2 id="1-비동기동기">1. 비동기/동기</h2>
<ul>
<li><strong>blocking (블로킹)</strong> : 이전 작업이 끝날 때까지 기다렸다가 다음 작업을 수행</li>
<li><strong>non-blocking (논블로킹)</strong> : 이전 작업이 완료될 때까지 대기 하지 않고 다른 작업을 동시에 수행할 수 있는 것</li>
</ul>
<p>노드에서는 <strong>동기와 블로킹</strong>이 유사하고 <strong>비동기와 논 블로킹</strong>이 유사하다.</p>
<h3 id="✔️-nodejs에서-비동기-처리">✔️ Node.js에서 비동기 처리</h3>
<p>Node.js에서는 대부분의 메서드들이 비동기 방식으로 처리한다.</p>
<p>하지만 Javascript가 기본적으로 단일 스레드 (Single thread)이기 때문에 한 번에 한 작업만 수행한다.</p>
<blockquote>
<p><strong>What is Single Thread ?</strong>
: 프로세스 내 하나의 쓰레드가 하나의 요청만을 수행하는 것을 말한다.
즉, 들어온 요청이 돌아가고 있을 때 다른 요청을 함께 수행할 수 없다 !
<br><em><strong>Node.js에서는 싱글 스레드 논블로킹 모델을 적용해서 사용.</strong></em></p>
</blockquote>
<p>따라서 하나의 Thread이지만 이 Thread를 이용해 비동기 처리를 하지 않고,
<strong>논블로킹 I/O 작업</strong>을 통해 동시에 들어온 많은 요청을 비동기적으로 처리할 수 있다.</p>
<p>논블로킹 I/O 는 <strong>Event-driven (이벤트 기반)</strong> 으로 동작 가능하다.</p>
<blockquote>
<p><strong>Event-driven는 또 뭐여 ?</strong>
: 이벤트가 발생할 때 미리 지정해둔 작업을 수행하는 방식을 의미한다.</p>
</blockquote>
<p>이를 도와주는 기능이 총 3가지가 있다 !</p>
<h3 id="1-callback-function">1) Callback Function</h3>
<p>어떤 이벤트 발생 시, 특정 시간이 지난 뒤 시스템에서 호출하는 함수이고 다른 함수의 인자로 사용한다.</p>
<pre><code class="language-typescript">//* Callback Function

console.log(&quot;Ready ...&quot;);

setTimeout((): void =&gt; { 
    console.log(&quot;Set ...&quot;); //3초 뒤에 출력
}, 3000);

console.log(&quot;Go !&quot;);

//* 출력
//Ready ...
//Go !
//Set ...</code></pre>
<p>이처럼 콜백 함수를 통해 비동기 처리를 할 수 있다.
하지만 ! 이런 callback function을 이용한 비동기 처리는, 콜백 지옥을 만들어낸당 ㅎㅎㅎ..</p>
<p>이를 해결하기 위해 ES2015부터는 <code>Promise</code> 를 사용한다.</p>
<h3 id="21-promise">2.1) Promise</h3>
<p>Promise에는 총 3단계의 상태가 존재한다.</p>
<ul>
<li><strong>Pending</strong> (대기) - 비동기 처리가 완료되지 않은 상태</li>
<li><strong>Fullfiled</strong> (이행) - 비동기 처리가 완료되어 Promise 결과를 반환</li>
<li><strong>Rejected</strong> (실패) - 비동기 처리 도중 실패했거나 오류가 발생함</li>
</ul>
<pre><code class="language-typescript">const condition : boolean = false; // true면 resolve, false면 reject

//* 최초 생성 시점
const promise = new Promise((resolve, reject) =&gt; {
    if (condition) {
        resolve(&quot;우와 Promise다 !&quot;);
    } else {
        reject(new Error(&quot;비동기 처리 도중 실패!&quot;));
    }
});

/*
다른 코드 들어갈 수 있다
!
!
!
*/

//* 비동기 처리 성공(then), 비동기 처리 실패(catch)
//resolve와 reject에 넣어준 인수는 각각 then과 catch의 매개변수에서 받을 수 있다.
promise
.then((resolveData): void =&gt; console.log(resolveData))
.catch((error): void =&gt; console.log(error)); //현재 condition 의 값이 false 이므로, error(&quot;비동기 처리 도중 실패!&quot;)를 출력
</code></pre>
<p>new Promise로 프로미스를 생성할 수 있으며, 그 내부에 resolve와 reject를 매개변수로 갖는 콜백 함수를 넣는다.</p>
<ul>
<li><p>프로미스 내부에서 resolve가 호출되면 -&gt; then이 실행 / reject가 호출되면 catch가 실행</p>
</li>
<li><p>finally 부분은 성공/실패 여부와 상관없이 실행</p>
</li>
</ul>
<p>resolve와 reject에 넣어준 인수는 각각 then과 catch의 매개변수에서 받을 수 있다.</p>
<ul>
<li><p>resolve(&#39;성공&#39;)이 호출되면 then의 message가 &#39;성공&#39;이 됨.</p>
</li>
<li><p>reject(&#39;실패&#39;)가 호출되면 catch의 error가 &#39;실패&#39;가 됨.</p>
</li>
</ul>
<p>위 코드에서 condition 변수를 true로 바꿔보면 catch에서 message가 로깅된다.</p>
<p><em><strong>즉, 프로미스를 쉽게 설명하자면, 실행은 바로 하되 결과값은 나중에 받는 객체이다.
결과값은 실행이 완료된 후 then이나 catch 메서드를 통해 받는다.</strong></em></p>
<h3 id="22-promise-chaining">2.2) Promise Chaining</h3>
<p>여러 개의 <code>promise</code> 를 연결해서 사용할 수 있다.</p>
<p>앞서 확인했던 <code>&lt;Promise&gt;.then()</code>과 <code>&lt;Promise&gt;.catch()</code>를 이용하면 된다.</p>
<p>아침에 일어나서 어렵게.. 어렵게.. 양치를 하는 나를 Promise Chaining을 이용해 만들어보자 </p>
<pre><code class="language-typescript">//* 아침에 어렵게,, 어렵게,, 일어나는 나를 표현한 함수
const me = (callback: () =&gt; void, time: number) =&gt; { 
    setTimeout(callback, time);
};

//* 기상
const wakeUp = (): Promise&lt;string&gt; =&gt; { //Promise 객체를 반환하는 함수
    return new Promise((resolve, reject) =&gt; {
        me(() =&gt; {
            console.log(&quot;[현재] 일어남&quot;);
            resolve(&quot;일어남&quot;); 
        }, 1000);
    });
};

//* 화장실 감
const goBathRoom = (now: string): Promise&lt;string&gt; =&gt; {
    return new Promise((resolve, reject) =&gt; {
        me(() =&gt; {
            console.log(&quot;[현재] 화장실로 이동함&quot;);
            resolve(`${now} -&gt; 화장실로 이동함`);
        }, 1000);
    });
};

//* 칫솔과 치약을 준비함
const ready = (now: string) : Promise&lt;string&gt; =&gt; {
    return new Promise((resolve, reject) =&gt; {
        me(() =&gt; {
            console.log(&quot;[현재] 칫솔과 치약을 준비함&quot;);
            resolve(`${now} -&gt; 칫솔과 치약을 준비함`)
        }, 1000);
    });
};

//* 양치함
const startChikaChika = (now: string) : Promise&lt;string&gt; =&gt; {
    return new Promise((resolve, reject) =&gt; {
        me(() =&gt; {
            console.log(&quot;[현재] 양치함&quot;);
            resolve(`${now} -&gt; 양치함`)
        }, 1000);
    });
};

//* 나 자신한테 칭찬함
const goodjob = (now: string) : Promise&lt;string&gt; =&gt; {
    return new Promise((resolve, reject) =&gt; {
        me(() =&gt; {
            console.log(&quot;[현재] 나 자신에게 칭찬중&quot;);
            resolve(`${now} -&gt; 칭찬중`)
        }, 1000);
    });
};

wakeUp() //resolve가 chaining 되어서 화살표가 이어져서 출력된다. (now 값에 문자열들이 추가되고, 추가되고,,,)
    .then((now) =&gt; goBathRoom(now))
    .then((now) =&gt; ready(now))
    .then((now) =&gt; startChikaChika(now))
    .then((now) =&gt; goodjob(now))
    .then((now) =&gt; console.log(`\n${now}`)); //출력값: 일어남 -&gt; 화장실로 이동함 -&gt; 칫솔과 치약을 준비함 -&gt; 양치함 -&gt; 칭찬중</code></pre>
<p><img src="https://velog.velcdn.com/images/ye_suri_106/post/87101a0f-1377-4582-b7e8-87b94c7d38ca/image.png" alt=""></p>
<p>여기서 주목할 값은 Promise의 resolve가 넘겨주는 인수 <code>now</code> 값이다.
이 now 값은 결국 Promise.then() 함수가 불릴 때 사용되는데, 여기서 chaining 이 이뤄지는 것을 볼 수 있다 !</p>
<p>이해를 쉽게 하기 위해 그림을 그려서 나타내보았다.
<img src="https://velog.velcdn.com/images/ye_suri_106/post/ad35663b-8aba-4512-bf3c-7f01b66feded/image.png" alt=""></p>
<p><code>resolve</code> 말고도 <code>reject</code>에도 체이닝이 그럼 가능할까 ?..
다음 코드를 돌려보자 !</p>
<pre><code class="language-typescript">Promise.resolve(true)
    .then((response) =&gt; {
        throw new Error(&quot;비동기 처리 중 에러 발생!&quot;);
    })
    .then((response) =&gt; {
        console.log(response);
        return Promise.resolve(true);
    })
    .catch((error) =&gt; {
        console.log(error.message); //출력값: &quot;비동기 처리 중 에러 발생!&quot;
    });</code></pre>
<p>여러개의 프로미스 체인 중 하나라도 reject되면 바로 마지막에 달린 catch()로 내려가서 에러를 처리한다. 불필요하게 나머지 프로미스까지 차례차례 확인하지 않는다.</p>
<h3 id="3-async---await">3) async - await</h3>
<p><code>async</code>는 ES2017부터 제공되고 알아두면 엄청나게 편리한 기능이다.</p>
<p><code>Promise</code>가 콜백 지옥을 해결했다고 하지만 then과 catch가 계속 반복되기 때문에 여전히 코드가 장황하다. 이를 async/await 문법으로 프로미스를 사용하여 깔끔하게 줄일 수 있다.</p>
<ul>
<li><strong>async</strong> : 암묵적으로 Promise를 반환한다.</li>
<li><strong>await</strong> : resolve, reject 같은 Promise 객체를 기다린다. 이 때, async가 정의된 내부에서만 사용 가능하다.</li>
</ul>
<p><code>async</code>와 <code>await</code>를 이용하여 함수를 선언하고 표현하는 방법은 다음과 같다</p>
<pre><code class="language-typescript">//* async - await

//함수 선언식
async function foo1() {

}

//함수 표현식
const foo2 = async () =&gt; {

}</code></pre>
<p>다음 코드로 왜 async/await를 알고 있어야 하는지 살펴보자 !</p>
<pre><code class="language-typescript">//* 이전에 치카치카 코드와 비슷한 Promise를 이용한 비동기 처리 코드
// 보기에 복잡해보이는 코드,,

let asyncFunc1 = (something: string): Promise&lt;string&gt; =&gt; {
    return new Promise((resolve, reject) =&gt; {
        setTimeout(() =&gt; {
            resolve(`resolved ${something} from func1 ...`);
        }, 1000);
    });
};

let asyncFunc2 = (something: string): Promise&lt;string&gt; =&gt; {
    return new Promise((resolve, reject) =&gt; {
        setTimeout(() =&gt; {
            resolve(`resolved ${something} from func2 ...`);
        }, 1500);
    });
};

const promiseMain = (): void =&gt; {
    asyncFunc1(&quot;test&quot;)
        .then((resolveData: string) =&gt; {
            console.log(resolveData);
            return asyncFunc2(&quot;testttt&quot;)
        })
        .then((resolveData: string) =&gt; {
            console.log(resolveData);
        });
};

promiseMain(); 
//resolved test from func1 ...
//resolved testttt from func2 ...</code></pre>
<p>promiseMain 함수를 보자. 정확히 어떤 데이터가 어떤 시점에서 출력되는지 이해하기 어렵다..</p>
<p>이 promiseMain 함수를 async/await로 바꾼다면 ?!?!??!</p>
<pre><code class="language-typescript">const main = async (): Promise&lt;void&gt; =&gt; {
    let result = await asyncFunc1(&quot;wow!&quot;);
    console.log(result);
    result = await asyncFunc2(&quot;holy moly&quot;);
    console.log(result);
};

main();
//resolved wow! from func1 ...
//resolved holy moly from func2 ...</code></pre>
<p>위와 같이 더 직관적이고 깔끔하게 쓸 수 있다.</p>
<p>아래 코드와 같이 for문과 async/await문을 같이 써서 프로미스를 순차적으로 실행할 수 있다. for문과 함께 쓰는 것은 노드 10 버전부터 지원하는 ES2018 문법이다.</p>
<pre><code class="language-typescript">const promise1 = Promise.resolve(&#39;성공1&#39;); 
const promise2 = Promise.resolve(&#39;성공2&#39;);
(async () =&gt; {
  for await (promise of [promise1, promise2]) {
    console.log(promise);
  }
})();</code></pre>
<p>for await of 문을 이용해서 프로미스 배열을 순회하는 코드이다. async 함수의 반환값은 항상 Promise로 감싸진다.</p>
<p>따라서, 실행 후 then을 붙이거나 또 다른 async 함수 안에서 await을 붙여 처리할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[나도 기록한다.]]></title>
            <link>https://velog.io/@ye_suri_106/%EB%82%98%EB%8F%84-%EA%B8%B0%EB%A1%9D%ED%95%9C%EB%8B%A4</link>
            <guid>https://velog.io/@ye_suri_106/%EB%82%98%EB%8F%84-%EA%B8%B0%EB%A1%9D%ED%95%9C%EB%8B%A4</guid>
            <pubDate>Wed, 12 Oct 2022 05:44:38 GMT</pubDate>
            <description><![CDATA[<h3 id="너-이제-공부하는-거-다-정리해-기술-블로그-빨리-시작해라-ㅠㅠㅠ">&quot; 너 이제 공부하는 거 다 정리해. 기술 블로그 빨리 시작해라 ㅠㅠㅠ &quot;</h3>
<p>나의 sherpa <del>(히말라야를 등반하는 등산객들의 길잡이...^^)</del> 언니가 해준 한 마디... 사실 그 전부터 배우고 경험한 것을 정리 해야겠다는 생각을 했다. 하지만 이 한 마디에 실천으로 이어졌다고 할까...🙃</p>
<p>나의 블로그 인생은 네이버에 조금 끄적여보다가 마감.. 그 마저도 꾸준히 하지 못했다.
사실 그 전에는 목적을 가지고 한다기 보다는 그냥 해야될 것 같아서 대충 몇 글자 적은게 전부였다.
하지만 지금은 조금 다르다.</p>
<p>배운 것을 내 것으로 만들기 위함.
나의 생각과 지식을 다른 이들에게 공유하기 위함.</p>
<p>이제 시작이다 !
<code>야금야금</code> 기록하자.</p>
]]></description>
        </item>
    </channel>
</rss>