<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>sarah.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 29 Sep 2025 05:09:17 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>sarah.log</title>
            <url>https://velog.velcdn.com/images/dayoung_sarah/profile/9b0d4d5a-72db-4225-ae63-b9b94c9e3ba1/image.JPG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. sarah.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dayoung_sarah" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[당탕탕우 msa 도입기] 피드 시스템 구현]]></title>
            <link>https://velog.io/@dayoung_sarah/%EB%8B%B9%ED%83%95%ED%83%95%EC%9A%B0-msa-%EB%8F%84%EC%9E%85%EA%B8%B0-%ED%94%BC%EB%93%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@dayoung_sarah/%EB%8B%B9%ED%83%95%ED%83%95%EC%9A%B0-msa-%EB%8F%84%EC%9E%85%EA%B8%B0-%ED%94%BC%EB%93%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Mon, 29 Sep 2025 05:09:17 GMT</pubDate>
            <description><![CDATA[<p>&#39;가상 면접 사례로 배우는 대규모 시스템 설계 기초 1 - 뉴스 피드 시스템 구현&#39;을 참고하여 &#39;아이바&#39; 프로젝트에 맞춰 설계해보았습니다.</p>
<p>아래 이미지는 책에서 나온 최종 시스템 설계도입니다.
<img src="https://velog.velcdn.com/images/dayoung_sarah/post/070c62f1-68c0-427f-88ac-41e4127ee576/image.png" alt=""></p>
<ul>
<li>피드발행[그림3-3]</li>
<li>뉴스 피드 가져오기[그림3-4]</li>
</ul>
<blockquote>
<p>위 설계도는 “<strong>팔로우 기반 개인 피드</strong>”이지만, 아이바는 “<strong>팔로우 없는 전역 최신 피드</strong>”를 기준으로 합니다. <br>그래서 포스팅 전송 서비스의 fan-out 파이프라인이 별도로 존재하지 않습니다.<br>또한 사용자 정보는 user-service에서 별도로 가져와야 해서 Redis 캐시 및 gRPC 기술을 사용하였습니다.</p>
</blockquote>
<h1 id="-aiva-community-service-요약">** [Aiva community-service 요약]**</h1>
<ul>
<li><strong>피드 목록</strong>은 Redis Sorted Set(ZSET), 게시글 본문은 Redis Hash, <strong>TTL</strong> 24시간</li>
<li><strong>작성자 정보</strong>(닉네임/아바타 등)도 헤더에서 받아 <strong>캐시</strong></li>
<li><strong>최신 피드 조회</strong>: ZSET에서 커서 기반 페이지네이션 → Post Hash 다건 조회 → 작성자 정보 캐시 조합</li>
<li><strong>교차 서비스 조회</strong>: user-service는 gRPC로 동기 통신</li>
<li><strong>알림</strong>: 댓글/대댓글/좋아요 이벤트를 Kafka 토픽에 퍼블리시 → notification-service가 컨슘 후 FCM 발송</li>
</ul>
<h2 id="1-피드-발행">1. 피드 발행</h2>
<h3 id="레디스-데이터키-설계-요약">레디스 데이터/키 설계 요약</h3>
<ul>
<li><strong>피드 인덱스(ZSET)</strong>: <code>feed:global</code>
score = createdAt(epochMillis), member = postId</li>
<li><strong>게시글 본문(HASH)</strong>: <code>post:{postId}</code>
title, content, authorId, createdAt, likeCount ...</li>
<li><strong>작성자 정보 캐시(HASH)</strong>: <code>user:{authorId}:profile</code>
nickname, avatarUrl, ...</li>
</ul>
<h3 id="흐름">흐름</h3>
<ol>
<li>Community에 글 작성 요청 수신</li>
<li>DB 트랜잭션: 게시글 영속화(MySQL)</li>
<li><strong>캐시 반영(TTL 24h)</strong></li>
</ol>
<ul>
<li>HSET <code>post:{id}</code> ... (본문 저장)</li>
<li>ZADD <code>feed:global score=now member=postId</code> (최신순 인덱스 갱신)</li>
<li>HSET <code>user:{authorId}:profile</code> (작성자 정보 캐시)</li>
</ul>
<h3 id="redis-키-설계-이유">Redis 키 설계 이유</h3>
<ul>
<li><p><strong>ZSET(<code>feed:global</code>)</strong></p>
<ul>
<li>점수(score)에 <code>createdAt(epochMillis)</code>를 쓰면 <strong>최신순 정렬이 자동</strong>이고, 커서 기반 페이지네이션에서 <code>O(logN + M)</code>로 안정적입니다.</li>
<li>목록에는 <strong>postId만</strong> 넣어 정렬/페이지네이션과 본문 캐시를 <strong>관심사 분리</strong> → 메모리 사용량 절감, 갱신 충돌 감소.</li>
<li>팔로우가 없는 전역 피드라 “사용자별 피드 상자”가 불필요해 <strong>키 수가 적고 단순</strong>합니다.</li>
<li><a href="https://velog.io/@dayoung_sarah/%EA%B0%80%EC%83%81-%EB%A9%B4%EC%A0%91-%EC%82%AC%EB%A1%80%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%88-2-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EA%B2%8C%EC%9E%84-%EC%88%9C%EC%9C%84%ED%91%9C">참고: Redis SortedSet</a></li>
</ul>
</li>
<li><p><strong>HASH(<code>post:{postId}</code>)</strong></p>
<ul>
<li>필드 단위 접근/갱신이 쉽고 명확합니다(예: <code>HINCRBY likeCount</code>).</li>
<li>본문/메타 일부만 읽어도 되어 <strong>네트워크/CPU 낭비 최소화</strong>.</li>
<li>포스트 스키마가 변해도 필드 추가로 <strong>유연하게 진화</strong> 가능.</li>
</ul>
</li>
<li><p><strong>작성자 프로필 HASH(<code>user:{authorId}:profile</code>)</strong></p>
<ul>
<li>헤더의 사용자 정보를 기본으로 캐시에 저장</li>
<li>사용자 정보 수정시 해당 캐시 내용 업데이트 필요</li>
</ul>
</li>
</ul>
<h3 id="ttl-시간24h">TTL 시간(24H)</h3>
<ul>
<li><strong>사용 패턴 가정</strong>: 전역 최신 피드는 ‘하루 단위’ 소비가 강함 → “전일 최신” 컨셉과 정합.</li>
<li><strong>신선도 vs 비용</strong>: 너무 짧으면 미스/DB 부하↑, 너무 길면 스테일·메모리↑. 24h는 합리적 시작값.</li>
<li><strong>운영 조정(히트율 보며 튜닝)</strong><ul>
<li>히트율↑·메모리 여유→ 늘림(예: 36h)</li>
<li>미스↑·DB 과열→ 늘림 (DB 보호)</li>
<li>메모리 압박·스테일 이슈→ 줄임(예: 12h)</li>
</ul>
</li>
</ul>
<h2 id="2-피드-목록-조회">2. 피드 목록 조회</h2>
<h3 id="커서-기반-페이지네이션-설계">커서 기반 페이지네이션 설계</h3>
<h4 id="페이지네이션-비교">페이지네이션 비교</h4>
<table>
<thead>
<tr>
<th>구분</th>
<th><strong>Offset 기반</strong></th>
<th><strong>Cursor 기반</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>기본 원리</strong></td>
<td>건너뛸 개수 지정 (OFFSET)</td>
<td>특정 지점 이후 조회 (WHERE)</td>
</tr>
<tr>
<td><strong>파라미터</strong></td>
<td><code>?page=3&amp;limit=20</code></td>
<td><code>?cursor=eyJpZCI6MTIzfQ&amp;limit=20</code></td>
</tr>
<tr>
<td><strong>SQL 예시</strong></td>
<td><code>LIMIT 20 OFFSET 40</code></td>
<td><code>WHERE id &lt; 123 LIMIT 20</code></td>
</tr>
<tr>
<td><strong>시간 복잡도</strong></td>
<td>O(N + M) - N은 offset 값</td>
<td>O(log N + M) - 인덱스 활용</td>
</tr>
<tr>
<td><strong>메모리 사용</strong></td>
<td>offset만큼 메모리 필요</td>
<td>일정한 메모리 사용</td>
</tr>
<tr>
<td><strong>무한 스크롤</strong></td>
<td>비효율적</td>
<td>최적화됨</td>
</tr>
</tbody></table>
<h4 id="성능-벤치마크-100만-레코드-기준">성능 벤치마크 (100만 레코드 기준)</h4>
<table>
<thead>
<tr>
<th>페이지</th>
<th><strong>Offset 응답시간</strong></th>
<th><strong>Cursor 응답시간</strong></th>
<th><strong>성능 차이</strong></th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>5ms</td>
<td>5ms</td>
<td>동일</td>
</tr>
<tr>
<td>10</td>
<td>8ms</td>
<td>5ms</td>
<td>1.6배</td>
</tr>
<tr>
<td>100</td>
<td>50ms</td>
<td>6ms</td>
<td>8.3배</td>
</tr>
<tr>
<td>1,000</td>
<td>380ms</td>
<td>6ms</td>
<td>63배</td>
</tr>
<tr>
<td>10,000</td>
<td>3,500ms</td>
<td>7ms</td>
<td>500배</td>
</tr>
<tr>
<td>50,000</td>
<td>17,000ms</td>
<td>7ms</td>
<td>2,428배</td>
</tr>
</tbody></table>
<p>-&gt; 소셜피드 같은 경우 커서 기반으로 페이지네이션을 구현한다.</p>
<hr>
<p>페이지네이션의 기준(정렬 키)은 “항상 동일”해야 합니다.
아이바에선 createdAt DESC, id DESC(= 최신순 + 동점 보조키)로 통일하고,</p>
<ul>
<li>24h 캐시 구간은 Redis ZSET 커서로,</li>
<li><strong>그 아래(오래된 구간)</strong>는 DB Keyset Pagination(= 커서 기반)으로 자연스럽게 넘겨 받으면 됩니다.</li>
</ul>
<h4 id="1-기준-정렬커서-통일">1. 기준 정렬(커서) 통일</h4>
<ul>
<li>커서 형태: <code>(lastCreatedAtEpochMs, lastPostId)</code></li>
<li>정렬: <code>ORDER BY created_at DESC, id DESC</code></li>
<li>조건: <code>(created_at &lt; :lastCreatedAt) OR (created_at = :lastCreatedAt AND id &lt; :lastId)</code></li>
</ul>
<blockquote>
<p>이렇게 하면 <strong>캐시 → DB</strong>로 넘어가도 <strong>커서의 의미가 동일</strong>해서 끊김 없이 다음 페이지를 이어감</p>
</blockquote>
<h4 id="2-캐시-→-db-컷오버cutover-규칙">2. 캐시 → DB “컷오버(cutover)” 규칙</h4>
<ul>
<li><p><code>cutoff = now - 24h</code></p>
</li>
<li><p>각 페이지를 만들 때:</p>
<ol>
<li><strong>ZSET</strong>에서 <code>score(=createdAt)</code>가 <code>&gt; cutoff</code>인 아이템만 커서 기준으로 가져옴(예: 최대 <code>limit</code>개).</li>
<li>가져온 개수가 <code>limit</code>보다 <strong>부족</strong>하면, <strong>남은 개수만큼 DB Keyset</strong>으로 추가로 채움.<ul>
<li>DB 조회의 <strong>시작 경계</strong>는:<ul>
<li>캐시에서 마지막으로 가져온 <code>(lastScore, lastId)</code>가 있으면 그걸 사용</li>
<li>없다면 <code>cutoff</code>를 시작점으로 사용</li>
</ul>
</li>
</ul>
</li>
</ol>
</li>
</ul>
<blockquote>
<p>즉, “페이지 하나”는 <strong>캐시 결과 + DB 결과</strong>가 합쳐질 수 있습니다. 커서는 항상 “마지막으로 반환한 항목의 <code>(score,id)</code>”로 갱신</p>
</blockquote>
<hr>
<h4 id="3-redis-쿼리-예시커서-기반">3. Redis 쿼리 예시(커서 기반)</h4>
<pre><code class="language-text"># 첫 페이지
ZREVRANGEBYSCORE feed:global +inf -inf LIMIT 0 :limit

# 다음 페이지
# (lastScore, lastId) 커서가 있을 때: score는 open range, 동점은 앱에서 postId로 필터
ZREVRANGEBYSCORE feed:global ( :lastScore  -inf  LIMIT 0 :limit</code></pre>
<p>※ 동점 처리: 동일 <code>score</code>가 많은 경우, 여유분을 조금 더 가져와서
<code>(score &lt; lastScore) OR (score = lastScore AND postId &lt; lastId)</code>로 애플리케이션에서 필터.</p>
<hr>
<h4 id="4-db-keyset-pagination-쿼리">4. DB Keyset Pagination 쿼리</h4>
<pre><code class="language-sql">-- 인덱스
CREATE INDEX idx_posts_created_id ON posts (created_at DESC, id DESC);

-- 다음 페이지 조회
SELECT id, title, content, author_id, created_at, like_count
FROM posts
WHERE
  (created_at &lt;  :lastCreatedAt)
  OR (created_at = :lastCreatedAt AND id &lt; :lastId)
ORDER BY created_at DESC, id DESC
LIMIT :limit;</code></pre>
<ul>
<li><strong>첫 DB 페이지</strong>라면 <code>:lastCreatedAt = :cutoff</code>, <code>:lastId = &#39;MAX&#39;</code> 같은 형태로 시작.</li>
<li>응답 마지막 레코드의 <code>(created_at, id)</code>를 <strong>다음 커서</strong>로 적용.</li>
</ul>
<h3 id="grpc를-통한-작성자-정보-조회">gRPC를 통한 작성자 정보 조회</h3>
<p>피드 발행 시 작성자 프로필을 <code>user:{authorId}:profile</code>에 <strong>캐시(기본 TTL 24h)</strong> 해둡니다.
하지만 TTL이 만료되었거나 캐시에 없는 경우, <strong>user-service</strong>에서 작성자 정보를 <strong>gRPC</strong>로 조회해 결합합니다.
핵심 흐름은 아래와 같습니다.</p>
<ol>
<li>피드 목록에서 수집한 <code>authorId</code>들을 <strong>중복 제거</strong></li>
<li>캐시에서 <strong>우선 조회</strong> → 존재하는 것은 즉시 사용</li>
<li><strong>캐시 미스 난 ID만 묶어서 gRPC 벌크 호출</strong>(지연/호출 수 최소화)</li>
</ol>
<blockquote>
<p>선택 이유: 작성자 정보는 <strong>재사용률이 높고</strong>(여러 게시글에 반복 등장), gRPC는 <strong>낮은 지연·작은 페이로드·타입 안정성</strong>으로 비용 대비 효용이 큽니다.</p>
</blockquote>
<h4 id="msa에서의-서비스-간-통신-선택지와-트레이드오프">MSA에서의 서비스 간 통신 선택지와 트레이드오프</h4>
<table>
<thead>
<tr>
<th>방식</th>
<th>장점</th>
<th>단점</th>
<th>아이바 적용 포인트</th>
</tr>
</thead>
<tbody><tr>
<td><strong>REST (HTTP/JSON)</strong></td>
<td>범용성 높고 디버깅 용이, 브라우저 친화</td>
<td>페이로드 큼, 스키마/버전 관리 느슨</td>
<td>외부/공개 API, 관리자 툴 등에 적합</td>
</tr>
<tr>
<td><strong>gRPC</strong></td>
<td><strong>고성능(HTTP/2, 바이너리), 타입 안정성(Proto), 스트리밍, 멀티플렉싱</strong></td>
<td>브라우저 직접 호출 어려움, 디버깅 난이도↑</td>
<td><strong>내부 마이크로서비스 간 조회</strong>(작성자 프로필, 벌크 조회)에 채택</td>
</tr>
<tr>
<td><strong>GraphQL</strong></td>
<td>단일 엔드포인트, <strong>정확한 필드만 조회</strong>(over/under-fetch 해결)</td>
<td>캐싱/권한/N+1 관리 복잡</td>
<td>BFF나 복합 조회에 고려 가능(현 단계 미도입)</td>
</tr>
<tr>
<td><strong>메시지 큐 (Kafka 등)</strong></td>
<td><strong>비동기</strong>, 느슨한 결합, 버퍼링/재시도</td>
<td>즉시성 부족, 운영 복잡도↑</td>
<td><strong>알림/이벤트 전송</strong>에 채택(댓글/좋아요 등)</td>
</tr>
</tbody></table>
<h2 id="3-알림-발송">3. 알림 발송</h2>
<blockquote>
<p>댓글/대댓글/좋아요 이벤트를 <strong>본요청 경로에서 분리</strong>하여, <strong>Kafka → notification-service → FCM</strong> 파이프라인으로 비동기 처리합니다.</p>
</blockquote>
<h3 id="전체-흐름">전체 흐름</h3>
<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/b34b0113-d252-439c-926c-76204b1df58d/image.png" alt=""></p>
<h3 id="이벤트-스키마--토픽">이벤트 스키마 &amp; 토픽</h3>
<ul>
<li><strong>토픽</strong>: <code>community.events</code> (파티션 N, 압축 on)</li>
<li><strong>파티셔닝 키</strong>: <code>targetUserId</code> (동일 사용자 알림 순서 보장/스로틀 유리)</li>
<li><strong>소비 모델</strong>: <em>at-least-once</em> → <strong>멱등 처리 필수</strong></li>
</ul>
<pre><code class="language-json">{
  &quot;eventId&quot;: &quot;uuid-...&quot;,            // 멱등키(필수)
  &quot;type&quot;: &quot;COMMENT|REPLY|LIKE&quot;,
  &quot;actorId&quot;: &quot;user-123&quot;,
  &quot;targetUserId&quot;: &quot;user-999&quot;,
  &quot;postId&quot;: &quot;post-456&quot;,
  &quot;commentId&quot;: &quot;c-789&quot;,             // (선택)
  &quot;createdAt&quot;: 1695972100123
}</code></pre>
<h3 id="notification-service-소비-로직">notification-service 소비 로직</h3>
<ol>
<li><strong>멱등 체크</strong>: <code>eventId</code>가 이미 처리되었으면 <strong>즉시 스킵</strong></li>
<li><strong>수신 가능 여부</strong>: user-service <strong>gRPC</strong>로 토큰/알림 설정 조회 → 거부/야간 차단이면 종료</li>
<li><strong>FCM 전송</strong>: 토큰 단위 전송(배치/병렬), 결과 수집</li>
<li><strong>결과 처리</strong>:<ul>
<li>재시도 대상(429/5xx/네트워크): <strong>지수 백오프 + 지터</strong>로 재시도</li>
<li>영구 실패(잘못된 토큰 등): <strong>토큰 폐기</strong> + 기록</li>
<li>N회 실패: <strong>DLQ</strong> 이동</li>
</ul>
</li>
<li><strong>멱등키 기록</strong>: 성공/영구 실패 모두 <code>eventId</code> 저장(중복 방지)</li>
</ol>
<pre><code class="language-kotlin">@KafkaListener(topics = [&quot;community.events&quot;], groupId = &quot;notification&quot;)
fun handle(e: Event) {
    if (idem.exists(e.eventId)) return
    val info = userGrpc.getPushInfo(e.targetUserId) // tokens, settings
    if (!info.settings.allow(e.type)) { idem.save(e.eventId); return }

    val payload = render(e, info.settings.lang)
    sendWithRetry(info.tokens, payload) // 429/5xx만 재시도

    idem.save(e.eventId) // 최종 기록(성공/영구실패)
}</code></pre>
<h3 id="코틀린-비동기-전송코루틴">코틀린 비동기 전송(코루틴)</h3>
<ul>
<li><strong>경량 동시성</strong>으로 <strong>수백~수천 전송</strong>을 스레드 과증식 없이 처리</li>
<li><code>withTimeout</code>/<code>retry(backoff+지터)</code>/<code>SupervisorJob</code>로 <strong>취소/재시도/격리</strong> 제어</li>
</ul>
<pre><code class="language-kotlin">suspend fun sendWithRetry(tokens: List&lt;String&gt;, payload: Payload) = coroutineScope {
    val sem = Semaphore(50) // 동시 전송 50
    tokens.chunked(500).map { batch -&gt;
        async {
            sem.withPermit { retry(3) { fcm.sendAll(batch, payload) } }
        }
    }.awaitAll()
}</code></pre>
<h3 id="신뢰성-전략-멱등-·-재시도-·-dlq">신뢰성 전략 (멱등 · 재시도 · DLQ)</h3>
<ul>
<li><strong>멱등성</strong>: <code>eventId</code>에 <strong>Unique</strong> 보장(테이블/캐시)</li>
<li><strong>재시도 정책</strong>: 429/5xx/일시 네트워크만 재시도(최대 N회, 지수 백오프)</li>
<li><strong>DLQ</strong>: 스키마 오류/참조 무효/최대 재시도 초과 이벤트 격리</li>
<li><strong>Exactly-once 착각 금지</strong>: 현실은 <em>at-least-once</em> → 멱등으로 해결</li>
</ul>
<h2 id="마무리">마무리</h2>
<p>이번 글에서는 ‘가상 면접 사례로 배우는 대규모 시스템 설계 – 뉴스 피드’의 아이디어를 참고해, 아이바(Aiva)의 현실 제약(팔로우 없음·전역 최신 피드)을 반영한 커뮤니티 서비스 설계를 정리했습니다.
핵심은 다음과 같습니다.</p>
<ul>
<li><strong>목록/정렬은 ZSET, 본문은 HASH로 관심사 분리</strong></li>
<li><strong>TTL 24h(히트율/비용 균형)과 커서 기반 페이지네이션(createdAt DESC, id DESC)</strong></li>
<li><strong>작성자 정보는 캐시 우선 → gRPC 폴백</strong></li>
<li><strong>알림은 Kafka 비동기 처리 + 멱등/재시도/DLQ로 유실·중복·폭주에 강하게</strong></li>
</ul>
<p>아직 분산환경을 위해 기술적으로 손볼 부분이 많습니다... 나머지 서비스들을 채워가며 계속 디벨롭할 예정입니다..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[당탕탕우 msa 도입기] msa 아키텍처 설계]]></title>
            <link>https://velog.io/@dayoung_sarah/%EB%8B%B9%ED%83%95%ED%83%95%EC%9A%B0-msa-%EB%8F%84%EC%9E%85%EA%B8%B0-msa-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@dayoung_sarah/%EB%8B%B9%ED%83%95%ED%83%95%EC%9A%B0-msa-%EB%8F%84%EC%9E%85%EA%B8%B0-msa-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Mon, 29 Sep 2025 03:05:23 GMT</pubDate>
            <description><![CDATA[<h1 id="소개">소개</h1>
<p>최근 해커톤에 참여했던 프로젝트를 계속 디벨롭하고 있고, 그 과정을 기록으로 남기고자 합니다.
프로젝트명은 <strong>아이바(Aiva)</strong> 로, <strong>생성형 AI 기반 육아 챗 서비스</strong>입니다. 팀 내 AI 개발자분께서 7세 미만 육아 도메인에 특화된 모델을 파인튜닝해주셨고, 회원가입 시 입력한 육아 정보를 바탕으로 <strong>사용자가 채팅으로 질문하면 맞춤형으로 답해줍니다.</strong></p>
<p>채팅뿐 아니라 <strong>커뮤니티 기능</strong>을 통해 양육자 간 정보 공유와 소통을 지원하고, <strong>알림 서비스</strong>로 개인별 육아 정보를 큐레이션합니다. 또한 <strong>구독제</strong>를 도입해 더 전문적인 모델 사용과 일일 채팅 횟수 상향을 제공하도록 설계했습니다.</p>
<h1 id="msa-도입배경">msa 도입배경</h1>
<p>아이바는 표면적으로는 ‘채팅 서비스’이지만, 실제로는 커뮤니티/알림/구독/결제 등 다양한 도메인이 얽혀 있습니다. 이 참에 MSA(Microservices Architecture) 를 직접 도입해보며 학습·검증하자는 목표를 세웠습니다.</p>
<p>해커톤 기간 내 전부 구현하는 건 무리라는 걸 알았기에, 해커톤 종료 후 개인적으로 프로젝트를 이어가며 적용했습니다. 솔직히 지금 단계에서는 오버스펙에 가깝지만, 개념으로만 알던 MSA를 실전에서 다뤄보며 배우는 것에 의의를 두었습니다.</p>
<p>그렇다면 실무에서는 왜 MSA를 택하고, 어떻게 도입할까요? 아주 짧게 개념을 정리하면:</p>
<ul>
<li>왜: 팀 단위 병렬 개발, 독립 배포, 장애 격리, 도메인 경계 명확화, 확장성(서비스별 스케일 아웃)</li>
<li>어떻게: 도메인 분해 → 데이터베이스 분리 → 서비스 간 통신 전략(REST/gRPC/이벤트) 수립 → 모니터링(로그/메트릭/트레이싱) → 배포/테스트 자동화(CI/CD 등)</li>
</ul>
<p><em>(각 항목의 구체 적용과 트레이드오프는 다음 글들에서 서비스별로 자세히 다룰 예정입니다.)</em></p>
<h1 id="아이바-msa-아키텍처-설계">아이바: msa 아키텍처 설계</h1>
<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/b1a46241-ef89-4f31-a6af-9f7a0aaf43d4/image.png" alt=""></p>
<h3 id="기본-설계">기본 설계</h3>
<ul>
<li><strong>도메인 분리</strong>: User / Chat / Community / Notification / Subscription (총 5개)</li>
<li><strong>데이터 격리</strong>: 각 서비스는 자체 MySQL(스키마/인스턴스 분리)</li>
<li><strong>API Gateway</strong>: Spring Cloud Gateway로 라우팅, 인증, 로드밸런싱 중앙화</li>
<li><strong>캐싱/세션</strong>: Redis로 세션 공유와 읽기 최적화 캐싱</li>
<li><strong>통신 방식</strong>: 서비스 내부는 동기(REST/gRPC) + 비동기(Kafka) 혼합</li>
<li><strong>모니터링</strong>: 로깅/메트릭/트레이싱을 서비스별로 수집·대시보드화(예정예정)</li>
</ul>
<h3 id="기술-스택">기술 스택</h3>
<ul>
<li><strong>Backend</strong>: Kotlin/Spring Boot</li>
<li><strong>API Gateway</strong>: Spring Cloud Gateway</li>
<li><strong>DB</strong>: MySQL (서비스별 독립 인스턴스/스키마)</li>
<li><strong>Cache/Session</strong>: Redis</li>
<li><strong>Messaging</strong>: Kafka (알림/이벤트)</li>
<li><strong>Push</strong>: FCM</li>
<li><strong>CI/CD &amp; Monitoring</strong>: (Not yet..)</li>
</ul>
<h1 id="마무리">마무리</h1>
<p>MSA 도입의 시작.. 대서막....!
해커톤 일정에 쫓겨 초반 아키텍처를 엉성하게 잡았고, 이후 『가상 면접 사례로 배우는 대규모 시스템 설계』 스터디와 추가 학습을 거치며 여러 번 갈아엎었습니다. 이제야 벨로그에 정리하며 기반을 다질 수 있을 만큼 방향이 잡힌 듯합니다.</p>
<p>물론 아직 부족한 부분이 많고, 앞으로도 계속 수정과 개선이 뒤따를 겁니다. 개발은 원래 그런 과정이니게..
점진적으로 나아가 보겠습니다. 아좌좌~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vertex AI Gemini API를 활용한 메모 기능 개선]]></title>
            <link>https://velog.io/@dayoung_sarah/Vertex-AI-Gemini-API%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A9%94%EB%AA%A8-%EA%B8%B0%EB%8A%A5-%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@dayoung_sarah/Vertex-AI-Gemini-API%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A9%94%EB%AA%A8-%EA%B8%B0%EB%8A%A5-%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Tue, 23 Sep 2025 08:02:49 GMT</pubDate>
            <description><![CDATA[<h2 id="1-배경">1. 배경</h2>
<p>필사 프로젝트 내에서 제공하고 있는 <strong>메모 기능</strong>은 전체 사용자 중 5% 미만만이 사용하는 상황이었다. 기능 활성화 방안을 고민하던 중, 단순히 메모 공간을 제공하는 것만으로는 참여율 제고가 어렵다는 결론에 도달하였다.</p>
<p>이에 따라 구글잼을 통해 학습하던 <strong><a href="https://www.cloudskillsboost.google/course_templates/959?catalog_rank=%7B%22rank%22%3A14%2C%22num_filters%22%3A0%2C%22has_search%22%3Atrue%7D&amp;search_id=53877727">Vertex AI Gemini API</a></strong>를 활용하여, 매일 제공되는 명언을 기반으로 사용자가 사유해볼 수 있는 질문을 생성하고 이를 메모 기능과 연동하는 접근을 시도하였다. 즉, 단순한 기록 공간에서 벗어나, <strong>AI가 제공하는 질문을 통해 자연스럽게 메모 참여를 유도</strong>하는 방식이다.</p>
<hr>
<blockquote>
<p>github: <a href="https://github.com/LEE-DAYOUNG-SARAH/fillsa-ai">https://github.com/LEE-DAYOUNG-SARAH/fillsa-ai</a></p>
</blockquote>
<h2 id="2-기술적-환경-설정">2. 기술적 환경 설정</h2>
<ol>
<li><p><strong>의존성 설치</strong></p>
<pre><code>pip install google-genai</code></pre></li>
</ol>
<ol start="2">
<li><p><strong>데이터 소스</strong></p>
<ul>
<li>명언 데이터는 로컬 DB에서 조회하도록 구현하였다.</li>
</ul>
</li>
<li><p><strong>모델 설정</strong>
아래와 같이 모델 파라미터를 설정하였다.</p>
<pre><code>GEMINI_MODEL=gemini-2.5-flash-lite
GEMINI_MAX_OUTPUT_TOKENS=248
GEMINI_TEMPERATURE=1.0
GEMINI_TOP_P=0.92
GEMINI_TOP_K=60</code></pre><ul>
<li><code>gemini-2.5-flash-lite</code>를 선택한 이유: 가볍고 빠른 응답 속도를 제공하며, 짧은 질문 생성에 적합.</li>
<li><code>Temperature=1.0</code>: 다양성과 창의성을 확보.</li>
<li><code>Top-P=0.92</code>, <code>Top-K=60</code>: 문장 다양성 확보를 위한 확률 샘플링 보정.</li>
</ul>
</li>
<li><p><strong>JSON 응답구조</strong></p>
<pre><code class="language-python">client.config = types.GenerateContentConfig(
 response_mime_type=&quot;application/json&quot;,
 response_schema={
      &quot;type&quot;: &quot;object&quot;,
      &quot;properties&quot;: {
         &quot;question_en&quot;: {&quot;type&quot;: &quot;string&quot;},
          &quot;question_ko&quot;: {&quot;type&quot;: &quot;string&quot;},
     },
     &quot;required&quot;: [&quot;question_en&quot;, &quot;question_ko&quot;],
 },
 ...
)</code></pre>
</li>
</ol>
<hr>
<h2 id="3-프롬프트-설계-및-실험-과정">3. 프롬프트 설계 및 실험 과정</h2>
<h3 id="1-1차-시도">(1) 1차 시도</h3>
<ul>
<li><p><strong>설정</strong>: <code>온도=0.5</code>, <code>Top-P=0.9</code></p>
</li>
<li><p><strong>프롬프트</strong>:
영어 명언을 입력으로 받아, 영어 질문과 한국어 직역 질문을 동시에 생성하도록 요구.</p>
<pre><code class="language-text">Generate one thought-provoking question for users based on this English quote.
First create the question in English, then translate it to Korean.
Both versions must be exactly the same question, just in different languages.

Quote: &quot;{eng_quote}&quot;

Requirements:
Question that helps with self-reflection and personal development 
Specific and practical question 
Question that explores the core meaning of the quote 
Korean question must be a direct translation of the English question</code></pre>
</li>
<li><p><strong>실패 원인</strong>:</p>
<ul>
<li>출력 문장이 과도하게 길고 딱딱함.</li>
<li>일상적 대화톤이 아닌 학술적 문체로 출력됨.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="2-2차-시도">(2) 2차 시도</h3>
<ul>
<li><strong>변경 사항</strong>: <code>온도=0.8</code>, <code>Top-P=0.9</code> </li>
<li><strong>프롬프트</strong>:<ul>
<li>“You are a warm journaling coach”로 톤을 부드럽게 지정.</li>
<li>10~18 단어 제한.</li>
<li>쉬운 단어 사용(academic term 금지).</li>
<li>실용적·구체적 질문 요구.</li>
</ul>
</li>
</ul>
<pre><code class="language-text">  You are a warm journaling coach.
  Write ONE short, conversational question inspired by the quote below.

  Quote: &quot;{eng_quote}&quot; 

  Constraints:
  - 10–18 words in English. Keep Korean the same meaning and similar length. 
  - Use plain, everyday words; avoid stiff/academic terms(e.g., “genuine”, “personal development”, “self-reflection”, “opportunity”). 
  - Must (a) invite self-reflection/growth, (b) be specific and practical, (c) probe the quote’s core meaning. 
  - Do NOT restate the quote, add explanations, or include code fences.

  [KO] 
  - 한국어는 자연스러운 구어체(또는 부드러운 존댓말)로, 영어 문장을 충실히 번역.</code></pre>
<ul>
<li><strong>실패 원인</strong>:<ul>
<li>여전히 문장이 길고 반복적 단어(<code>small</code>) 등장.</li>
<li>특정 시점(예: “오늘”)이 고정적으로 포함됨.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="3-3차-시도">(3) 3차 시도</h3>
<ul>
<li><p><strong>변경 사항</strong>: <code>온도=1.0</code>, <code>Top-P=0.9</code>,<code>TOP_K=60</code></p>
</li>
<li><p><strong>프롬프트</strong>:</p>
<ul>
<li>질문 길이: 8~14 단어.</li>
<li>랜덤 시간 앵커 정책 도입:<ul>
<li>시간x, 24시간, 이번 주, 특정 상황(비판·의심·좌절 시) 등 랜덤 선택.</li>
</ul>
</li>
<li>한국어는 부드러운 존댓말 번역 요구.<pre><code class="language-text">You are a warm journaling coach.
Write ONE short, conversational question inspired by the quote below.
</code></pre>
</li>
</ul>
<p>Quotes: {quotes_text}</p>
<p>Time anchor policy:</p>
<pre><code>- Randomly choose ONE for each question:
A) no time anchor</code></pre><p>  B) next 24 hours / this week
  C) next time <trigger>
  D) last time <trigger>
  E) today</p>
<ul>
<li>If C or D, pick <trigger> from:
[&quot;you get criticism&quot;,&quot;you feel stuck&quot;,&quot;you doubt yourself&quot;,&quot;you feel upset&quot;].</li>
</ul>
<ul>
<li>8–14 words in English; Korean same meaning and similar length.</li>
<li>Use plain words. Do Not use &quot;small&quot;</li>
<li>Be specific and practical; no restating the quote; no code fences; max one comma.</li>
</ul>
<p>[KO]</p>
<ul>
<li>한국어는 부드러운 존댓말(–요) 또는 편한 구어체.</li>
<li>영어와 의미·길이를 비슷하게 유지.
```</li>
</ul>
</li>
<li><p><strong>실패 원인</strong>:</p>
<ul>
<li>단일 문장을 보낼 경우 랜덤 정책이 정상 작동하지 않음.</li>
</ul>
</li>
<li><p><strong>해결</strong>:</p>
<ul>
<li>10개의 명언을 한 번에 보내어, 프롬프트 내 랜덤성이 효과적으로 적용됨.</li>
<li>결과적으로 자연스럽고 짧은 질문이 생성되었으며, 만족스러운 품질 확보.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="4-결론">4. 결론</h2>
<ol>
<li><p><strong>데이터 기반 문제 인식</strong></p>
<ul>
<li>기능 사용률을 수치로 확인하면서, 단순 제공만으로는 기능 활성화가 어렵다는 사실을 재확인.</li>
</ul>
</li>
<li><p><strong>AI 적용 효과</strong></p>
<ul>
<li>Gemini API를 활용해 사용자 경험을 보완하는 방식은 효과적이었다.</li>
<li>특히, <strong>짧고 실용적인 질문</strong>을 통해 사용자가 메모 기능에 자연스럽게 참여할 수 있는 계기를 제공.</li>
</ul>
</li>
<li><p><strong>학습과 성장</strong></p>
<ul>
<li>새로운 기술을 실제 문제 해결에 적용하며 성장 경험을 얻음.</li>
<li>프롬프트 설계와 파라미터 튜닝 과정을 통해 <strong>AI 모델의 출력 제어 기법</strong>을 체득.</li>
<li>지표 기반 문제 해결 접근이 서비스 운영에서 얼마나 중요한지 체감.</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[가상 면접 사례로 배우는 대규모 시스템 설계 기초 2] 실시간 게임 순위표]]></title>
            <link>https://velog.io/@dayoung_sarah/%EA%B0%80%EC%83%81-%EB%A9%B4%EC%A0%91-%EC%82%AC%EB%A1%80%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%88-2-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EA%B2%8C%EC%9E%84-%EC%88%9C%EC%9C%84%ED%91%9C</link>
            <guid>https://velog.io/@dayoung_sarah/%EA%B0%80%EC%83%81-%EB%A9%B4%EC%A0%91-%EC%82%AC%EB%A1%80%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%88-2-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EA%B2%8C%EC%9E%84-%EC%88%9C%EC%9C%84%ED%91%9C</guid>
            <pubDate>Sun, 14 Sep 2025 07:18:56 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Redis는 다양한 데이터 구조를 제공하지만, 그 중에서도 Sorted Set은 실시간 순위 시스템 구현에 있어 독보적인 성능을 자랑합니다. 해당 챕터를 읽으면서 Redis의 다양한 기능 중 Sorted Set에 대해 집중적으로 조사해보았습니다. <br>
이 글에서는 Redis Sorted Set의 내부 동작 원리부터 실무에서의 활용 패턴, 그리고 대규모 서비스에서의 확장 전략까지 실전 경험을 바탕으로 공유하겠습니다.
(우와 이 글을 작성하고 얼마 안되어서 사이드 프로젝트에 Redis Sorted Set을 사용해보게 되었습니다. 이에 대해선 글 마지막에 추가하였습니다.)</p>
</blockquote>
<h1 id="레디스-sorted-set">레디스 Sorted Set</h1>
<h2 id="redis-sorted-set의-정교한-아키텍처와-실무-활용">Redis Sorted Set의 정교한 아키텍처와 실무 활용</h2>
<p>Redis Sorted Set은 <strong>Skip List와 Hash Table의 이중 구조</strong>를 통해 O(log N) 성능을 보장하면서도 점수 기반 정렬과 범위 쿼리를 지원하는 고성능 데이터 구조입니다. 2024-2025년 현재, Redis 7.2+에서 <strong>30-100% 성능 향상</strong>을 달성했으며, 게임 리더보드부터 실시간 분석까지 다양한 분야에서 핵심 기술로 활용되고 있습니다. 특히 대용량 데이터셋에서도 <strong>초당 7만 건 이상의 삽입 성능</strong>과 <strong>마이크로초 단위 지연시간</strong>을 제공하여, 실시간 시스템 구축의 필수 요소로 자리잡았습니다.</p>
<h2 id="1-핵심-개념과-내부-구조">1. 핵심 개념과 내부 구조</h2>
<h3 id="redis-sorted-set의-혁신적-이중-데이터-구조">Redis Sorted Set의 혁신적 이중 데이터 구조</h3>
<p>Redis Sorted Set은 동일한 데이터를 두 개의 서로 다른 구조에 저장하는 독창적인 접근 방식을 사용합니다. <strong>Hash Table</strong>은 member → score 매핑을 위한 O(1) 조회를 제공하고, <strong>Skip List</strong>는 score 기반 정렬 순서를 유지합니다. 이 이중 구조는 빠른 검색과 효율적인 범위 쿼리를 동시에 가능하게 합니다.</p>
<p>Skip List는 확률적 다층 연결 리스트로, 각 레벨에서 <strong>span 변수</strong>를 통해 건너뛰는 노드 수를 저장합니다. 이를 통해 순위(rank) 계산을 O(log N)으로 최적화하며, 최대 32개 레벨(ZSKIPLIST_MAXLEVEL)까지 지원합니다. 레벨 결정은 <strong>0.25 확률의 기하 분포</strong>를 사용하여 평균적으로 균형 잡힌 구조를 유지합니다.</p>
<h3 id="메모리-최적화와-임계값-전환">메모리 최적화와 임계값 전환</h3>
<p>작은 데이터셋(기본값: 128개 요소 미만, 각 64바이트 미만)에서는 <strong>Ziplist 인코딩</strong>을 사용하여 메모리를 최대 10배 절약합니다. 임계값을 초과하면 자동으로 Skip List 구조로 전환되며, 이때 평균 <strong>37바이트의 오버헤드</strong>가 발생합니다. Redis 6.2+에서는 ZRANGESTORE 같은 대량 연산 시 예상 크기를 미리 계산하여 처음부터 적절한 구조를 선택하는 최적화가 추가되었습니다.</p>
<h3 id="시간-복잡도-분석">시간 복잡도 분석</h3>
<p>주요 연산의 시간 복잡도는 다음과 같습니다. <strong>ZADD와 ZREM</strong>은 O(log N), <strong>ZSCORE</strong>는 Hash Table 덕분에 O(1), <strong>ZRANGE</strong>는 O(log N + M) (M은 반환 요소 수)입니다. 실제 벤치마크에서는 <strong>ZADD가 초당 70,000-100,000회</strong>, ZRANGE가 초당 66,000-111,000회의 처리량을 보여줍니다. 이는 단일 스레드 기준이며, 네트워크 오버헤드가 실제 성능에 큰 영향을 미칩니다.</p>
<h2 id="2-대체-기술과의-심층-비교">2. 대체 기술과의 심층 비교</h2>
<h3 id="redis-vs-관계형-데이터베이스">Redis vs 관계형 데이터베이스</h3>
<p><strong>성능 벤치마크 결과</strong>에 따르면, 5,000개 레코드 기준으로 Redis는 쓰기 작업을 1초 미만에 완료하는 반면, MySQL은 400-500개 이후 급격한 성능 저하를 보입니다. PostgreSQL의 경우 업데이트에 10초 이상 소요됩니다. 실제 게임 리더보드를 PostgreSQL에서 Redis로 마이그레이션한 사례에서는 <strong>응답시간이 500ms에서 5ms로 100배 개선</strong>되었고, 동시 사용자 수용량이 10K에서 100K로 증가했습니다.</p>
<p>관계형 DB는 ACID 트랜잭션과 복잡한 조인 연산이 필요한 경우에 적합하지만, Redis Sorted Set은 실시간 랭킹과 빈번한 점수 업데이트가 필요한 시나리오에서 압도적인 성능 우위를 보입니다.</p>
<h3 id="redis-vs-mongodb">Redis vs MongoDB</h3>
<p>리더보드 성능 비교에서 MongoDB는 p99 레이턴시 350ms, p90 40ms, p50 15ms를 기록한 반면, Redis Sorted Set은 각각 <strong>9ms (39배 빠름), 2.5ms (16배 빠름), 1.5ms (10배 빠름)</strong>를 달성했습니다. 메모리 사용량 측면에서는 Redis가 MongoDB 대비 약 2배를 사용하지만, 이는 초고속 성능을 위한 트레이드오프입니다.</p>
<h3 id="redis-vs-elasticsearch">Redis vs Elasticsearch</h3>
<p>RediSearch와 Elasticsearch 벤치마크에서, 5.6M 문서 인덱싱 시 RediSearch가 221초로 Elasticsearch의 349초보다 58% 빠르며, 2단어 검색 쿼리에서 4배 빠른 성능을 보였습니다. 특히 벡터 검색에서는 OpenSearch 대비 <strong>최대 52배 높은 QPS와 106배 낮은 지연시간</strong>을 기록했습니다.</p>
<h3 id="redis-vs-kafka">Redis vs Kafka</h3>
<p>Kafka는 수백만 메시지/초의 처리량과 수평 확장이 가능하지만 일반적으로 100ms 미만의 지연시간을 보입니다. 반면 Redis는 제한적 처리량과 메모리 제약이 있지만 <strong>마이크로초 단위의 지연시간</strong>을 제공합니다. 이벤트 소싱과 대용량 스트림 처리에는 Kafka가, 실시간 상태 관리와 낮은 지연시간이 중요한 경우 Redis가 적합합니다.</p>
<h2 id="3-성능-특성과-2024-2025-최신-트렌드">3. 성능 특성과 2024-2025 최신 트렌드</h2>
<h3 id="메모리와-성능-최적화">메모리와 성능 최적화</h3>
<p>Redis Sorted Set은 멤버당 평균 <strong>103바이트의 오버헤드</strong> (16바이트 값 기준 6.4배)를 가지며, Skiplist 인코딩 시 엔트리당 37바이트의 추가 오버헤드가 발생합니다. 작은 데이터셋(≤128 elements)에서는 Listpack 인코딩으로 최대 10배 메모리를 절약할 수 있습니다.</p>
<p>대용량 데이터셋 처리 성능은 <strong>6백만 요소 기준 초당 70,000회 삽입</strong>, 동일 데이터셋에서 초당 40,000회 ZINCRBY, 10개 요소 조회 시 초당 132,718회 요청을 처리합니다. CPU 사용 패턴 분석 결과, 읽기 작업이 쓰기보다 더 많은 CPU를 소모하며, 80% CPU 사용률은 수천 개 키에서 초당 수천 회 읽기와 수백 회 쓰기 시 발생합니다.</p>
<h3 id="redis-72-혁신과-redis-8-전망">Redis 7.2+ 혁신과 Redis 8 전망</h3>
<p>Redis 7.2에서는 Sorted Set 성능이 <strong>30-100% 향상</strong>되었으며, 특히 게임 리더보드 시나리오에서 최적화가 이루어졌습니다. 복제 성능은 최대 18% 빨라졌고, Query Engine 최적화로 최대 16배 향상된 쿼리 처리 성능을 달성했습니다.</p>
<p>2024년 말 출시된 Redis 8 GA는 30개 이상의 성능 개선을 통해 <strong>최대 87% 빠른 명령어 실행과 2배의 처리량 증가</strong>를 실현했습니다. 특히 AI/ML 워크로드를 위한 Vector Set 데이터 타입이 추가되어, Vector Similarity Search와 Sorted Set의 점수 기반 정렬을 결합한 혁신적인 활용이 가능해졌습니다.</p>
<h3 id="클러스터링과-확장성-도전">클러스터링과 확장성 도전</h3>
<p>Redis Cluster에서 Sorted Set은 단일 키로만 존재하여 자동 샤딩이 불가능한 제약이 있습니다. 16,384개 해시 슬롯을 통한 분산과 해시태그를 활용한 수동 샤딩 전략이 필요하며, 여러 샤드에 걸친 집계 쿼리는 애플리케이션 레벨에서 처리해야 합니다. Read-heavy 워크로드에서는 최대 5개 replica를 활용한 읽기 분산이, Write-heavy 워크로드에서는 파이프라이닝과 Lua 스크립트를 통한 최적화가 권장됩니다.</p>
<h3 id="serverless-redis의-부상">Serverless Redis의 부상</h3>
<p>위와 같은 &#39;클러스터링 &amp; 샤딩&#39;과 같은 제약으로 인해, 이를 자동으로 처리해주는 ElasticCache를 활용해 볼 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/caeea6af-00f2-4862-bc5e-1276dbd6590c/image.png" alt="">
<strong>ElastiCache</strong>는 Redis OSS에서 직접 처리해야 하는 <strong>샤딩, 클러스터링, 페일오버, 백업</strong> 같은 복잡한 운영을 AWS가 대신 관리해 줍니다.</p>
<p>Serverless 모드를 사용하면 트래픽에 맞춰 <strong>자동으로 규모 확장</strong>이 가능해, 게임 순위표나 실시간 랭킹 같은 <strong>고트래픽 워크로드</strong>에서도 안정적으로 대응할 수 있습니다.</p>
<ul>
<li>참고: <a href="https://aws.amazon.com/ko/elasticache/redis/">ElastiCache 공식 문서</a> / <a href="https://aws.amazon.com/blogs/database/building-a-real-time-gaming-leaderboard-with-amazon-elasticache-for-redis/?utm_source=chatgpt.com">실시간 게임 순위표 구축 사례</a></li>
</ul>
<h2 id="4-실무-최적화-권장사항">4. 실무 최적화 권장사항</h2>
<h3 id="메모리-최적화-전략">메모리 최적화 전략</h3>
<p>설정 최적화를 통해 <code>zset-max-listpack-entries</code>를 128로, <code>zset-max-listpack-value</code>를 64로 설정하여 작은 집합의 메모리 효율을 극대화합니다. 데이터 설계에서는 긴 문자열 대신 ID 참조를 사용하고, ZREMRANGEBYRANK로 정기적으로 오래된 데이터를 제거하며, KEY 레벨 TTL을 활용하여 자동 만료를 구현합니다.</p>
<h3 id="파이프라이닝과-배치-처리">파이프라이닝과 배치 처리</h3>
<p>10,000개 명령어 단위의 배치 처리로 5배 성능 향상이 가능하며, 서버 측 응답 버퍼링 부담을 최소화하기 위해 적절한 배치 크기를 유지해야 합니다. Lua 스크립트를 활용한 원자적 연산으로 race condition을 방지하고, Redis Functions를 통해 네트워크 트래픽을 절약할 수 있습니다.</p>
<h3 id="모니터링-핵심-지표">모니터링 핵심 지표</h3>
<p>CPU 사용률 80% 초과 시 확장을 고려하고, 메모리 사용률이 물리 메모리 한계에 접근하면 알림을 설정합니다. P99 기준 응답 시간을 지속적으로 모니터링하며, SLOWLOG를 통해 임계값을 초과하는 명령어를 분석합니다. RedisInsight 2024의 메모리 분석기와 실시간 프로파일러를 활용하여 성능 병목을 식별하고 최적화합니다.</p>
<h2 id="5-결론">5. 결론</h2>
<p>Redis Sorted Set은 이중 데이터 구조의 정교한 설계를 통해 실시간 정렬 데이터 처리에서 탁월한 성능을 제공합니다. 2024-2025년 기준으로 Redis 7.2+의 성능 향상과 Redis 8의 혁신적 기능들, 그리고 클라우드 네이티브 Serverless 서비스의 발전으로 더욱 강력해졌습니다. </p>
<p>메모리 사용량과 클러스터 환경에서의 샤딩 제약 같은 한계가 존재하지만, 적절한 최적화 전략과 하이브리드 아키텍처를 통해 이를 극복할 수 있습니다. 특히 게임 리더보드, 실시간 분석, 우선순위 큐, Rate Limiting 등의 시나리오에서 Redis Sorted Set은 여전히 최고의 선택지이며, AI/ML 워크로드와의 통합을 통해 미래 애플리케이션 개발의 핵심 기술로 자리매김하고 있습니다.</p>
<ul>
<li>참고: <a href="https://systemdesign.one/leaderboard-system-design/#how-to-shard-the-leaderboard-cacheserver">리더보드 시스템 설계</a></li>
</ul>
<hr>
<h2 id="추가-사이드-프로젝트필사-명언-데이터-캐싱">추가) 사이드 프로젝트(필사): 명언 데이터 캐싱</h2>
<p>최근 진행한 사이드 프로젝트에서는 1년치의 명언 데이터를 DB에 저장하고 날짜별로 조회하는 기능을 제공하고 있습니다. 자주 변경되지 않는 데이터의 특성상, 매번 DB를 조회하는 대신 Redis 캐시를 사용해 응답 속도를 최적화하기로 결정했습니다.</p>
<p>이때, 저는 단순한 Key-Value 구조인 String이나 Hash 대신 Redis <strong>Sorted Set</strong>을 선택했습니다. 일반적인 캐싱 시나리오에서 사용되는 String이나 Hash는 정렬 기능이 없기 때문에, BETWEEN과 같은 범위 조회를 지원하지 않습니다. 이 구조들을 사용했다면 모든 데이터를 가져와 애플리케션 단에서 날짜별로 필터링해야 하는 비효율이 발생했을 것입니다.</p>
<p>하지만 Sorted Set은 멤버(member)와 함께 정렬 기준이 되는 스코어(score)를 저장하는 구조적 강점을 가지고 있습니다. score는 정수형이면서 시간의 흐름에 따라 증가하는 속성을 가지고 있어, <strong>시간 기반 정렬에 최적화된 스코어 역할</strong>을 했습니다.</p>
<p>이러한 구조 덕분에, 저는 ZSetOperations의 rangeByScore와 removeRangeByScore와 같은 Redis 명령어를 활용하여 &quot;오늘부터 일주일치&quot; 같은 <strong>특정 기간의 명언</strong>을 <strong>O(log N + M)의 효율적인 시간 복잡도</strong>로 가져올 수 있었습니다. 이는 DB에 의존했을 때 발생할 수 있는 부하를 줄여주면서, 동시에 매우 빠른 응답 속도를 보장했습니다. Sorted Set의 이러한 특성 덕분에, 데이터 정렬이 필요한 조회 시나리오에서 Redis를 효과적으로 활용할 수 있었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[가상 면접 사례로 배우는 대규모 시스템 설계 기초 2] 분산 이메일 서비스]]></title>
            <link>https://velog.io/@dayoung_sarah/%EA%B0%80%EC%83%81-%EB%A9%B4%EC%A0%91-%EC%82%AC%EB%A1%80%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%88-2-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/@dayoung_sarah/%EA%B0%80%EC%83%81-%EB%A9%B4%EC%A0%91-%EC%82%AC%EB%A1%80%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%88-2-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>Fri, 05 Sep 2025 07:42:52 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이번 장은 제가 스터디에서 발표를 맡았던 부분입니다. 기본적인 아키텍처 정리는 노션에 별도로 정리해 두었고, 이 글에서는 <strong>분산 이메일 서비스와 AI의 융합</strong>에 초점을 맞추려 합니다. <br />
이메일은 본질적으로 <strong>보안 위협에 취약</strong>하고, 동시에 <strong>사용자가 감당하기 어려울 만큼의 정보가 집약되는 채널</strong>이다. 그렇기에 단순한 메시징 도구를 넘어, 이제는 <strong>AI 없이는 성립하기 어려운 서비스</strong>로 발전해왔다. <br />
따라서 이번 글에서는 실제로 이메일이 어떤 방식으로 AI를 적극 도입해왔는지, 그리고 그 과정에서 발견할 수 있는 인사이트에 집중해 살펴보고자 합니다.</p>
</blockquote>
<br/>

<h2 id="1-보안-위협-대응-스팸·피싱에서-행위-기반-탐지로">1. 보안 위협 대응: 스팸·피싱에서 행위 기반 탐지로</h2>
<p>이메일은 사이버 공격의 주요 매개체다. Google의 Gmail은 하루 수천억 통의 메시지 중 99.9% 이상의 스팸을 차단하는데, 이는 머신러닝 기반 스팸 필터와 딥러닝 기반 피싱 탐지 모델 덕분이다. 최근에는 단순 키워드 차단을 넘어서, <strong>텍스트 내 변형(이모지·오타), 이미지화된 문자, QR코드 기반 피싱</strong>까지 탐지하는 다층적 모델이 적용된다.</p>
<p>Microsoft의 Advanced Threat Protection(ATP)은 한 걸음 더 나아가, <strong>사용자 행동 시퀀스</strong>를 학습한다. 로그인 위치, 첨부 다운로드, 링크 클릭과 같은 일련의 행동이 비정상적일 경우, 개별 이벤트를 넘어 전체 맥락에서 위협을 탐지한다. 이는 기존 룰 기반 보안 체계로는 포착 불가능한 영역이다.</p>
<p>&lt;참고문헌&gt;
<a href="https://blog.google/technology/safety-security/how-were-using-ai-to-combat-the-latest-scams/">Google Tech Blog – How we’re using AI to combat the latest scams</a>
<a href="https://www.microsoft.com/en-us/security/blog/2020/07/23/seeing-the-big-picture-deep-learning-based-fusion-of-behavior-signals-for-threat-detection/">Microsoft Security Blog – Deep learning-based fusion of behavior signals for threat detection</a></p>
<h2 id="2-정보-과부하-완화-자동-분류-검색-요약">2. 정보 과부하 완화: 자동 분류, 검색, 요약</h2>
<p>사용자는 매일 수십~수백 통의 메일을 관리해야 한다. Gmail의 <strong>Priority Inbox</strong>는 사용자의 읽기·삭제 패턴을 바탕으로 메일을 Primary, Social, Promotion으로 자동 분류한다. 이는 정보 과부하를 줄이고, 중요한 메일을 놓치지 않도록 하는 핵심 장치다.</p>
<p>최근에는 <strong>시맨틱 검색</strong>과 <strong>자동 요약</strong>이 도입되었다. NLP 기반 검색은 “지난주 계약 관련 메일”과 같은 질의를 문맥 수준에서 해석하고, LLM 기반 요약 기능은 긴 스레드를 압축하여 제공한다. 결과적으로 이메일은 단순한 저장소에서 <strong>능동적 정보 관리 시스템</strong>으로 변모하고 있다.</p>
<p>&lt;참고문헌&gt;
<a href="https://cloud.google.com/discover/what-is-semantic-search?hl=ko">Google Cloud - 시멘틱 검색이란 무엇인가요?</a></p>
<h2 id="3-gmail과-gemini-이메일의-llm-통합">3. Gmail과 Gemini: 이메일의 LLM 통합</h2>
<p>특히 Gmail은 자사 AI 모델인 <strong>Gemini</strong>를 본격적으로 통합하면서, 이메일을 <strong>사용자 친화적 AI 플랫폼</strong>으로 진화시키고 있다. Google Workspace Labs의 “Help me write” 기능은 Gemini를 기반으로 메일 초안을 자동 작성하거나, 기존 메일을 정중·간결·공식 등 다양한 톤으로 변환해 준다.</p>
<p>또한, Gemini는 Gmail 내에서 <strong>LLM 기반 문맥 이해</strong>를 수행한다. 예를 들어,</p>
<ul>
<li>여러 메일 스레드의 맥락을 분석해 요약 제공</li>
<li>메일에서 일정·액션 아이템을 추출해 Google Calendar와 연동</li>
<li>특정 키워드 검색이 아닌 <strong>의미 기반 검색</strong> 지원</li>
</ul>
<p>즉, Gmail은 더 이상 단순 메일 클라이언트가 아니라, <strong>Gemini를 중심으로 한 개인 비서형 플랫폼</strong>으로 진화하고 있다. 이는 이메일 서비스가 LLM의 상용화·대중화를 가장 빠르게 보여주는 사례이기도 하다.</p>
<p>&lt;참고문헌&gt;
<a href="https://blog.google/products/gmail/gmail-ai-features/">Gmail Blog – 6 Gmail AI features to help save you time</a></p>
<h2 id="4-운영-효율화-분산-시스템과-ai-결합">4. 운영 효율화: 분산 시스템과 AI 결합</h2>
<p>AI는 사용자 기능에 국한되지 않는다. 글로벌 이메일 서비스는 본질적으로 대규모 분산 메시징 인프라다. Gmail, Outlook과 같은 서비스는 매일 수십억 건의 메시지를 송수신하며, 이 과정에서 <strong>큐 적체, 서버 지연, 네트워크 병목</strong>이 빈번히 발생한다. Kafka 기반 메시징 시스템에 머신러닝을 접목하여 <strong>실시간 트래픽 예측과 자원 최적화</strong>를 수행한다.</p>
<p>&lt;참고문헌&gt;
<a href="https://www.automq.com/blog/how-kafka-ai-agents-leverage-real-time-data-for-smart-decision-making">How Kafka AI Agents Leverage Real-Time Data for Smart Decision Making</a></p>
<h2 id="결론">결론</h2>
<p>이메일 서비스는 단순한 메시징 도구를 넘어, 이미 <strong>AI 없이는 운영되기 어려운 서비스</strong>로 자리 잡아가고 있다. 보안 위협 대응, 정보 과부하 완화, 분산 시스템 운영 효율화 등 거의 모든 영역에서 AI가 핵심적인 역할을 하고 있으며, Gmail의 Gemini 통합은 이러한 변화를 상징적으로 보여준다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[가상 면접 사례로 배우는 대규모 시스템 설계 기초 2] 지표 모니터링 및 경보 시스템]]></title>
            <link>https://velog.io/@dayoung_sarah/%EA%B0%80%EC%83%81-%EB%A9%B4%EC%A0%91-%EC%82%AC%EB%A1%80%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%88-2-5%EC%9E%A5.-%EC%A7%80%ED%91%9C-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EB%B0%8F-%EA%B2%BD%EB%B3%B4-%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
            <guid>https://velog.io/@dayoung_sarah/%EA%B0%80%EC%83%81-%EB%A9%B4%EC%A0%91-%EC%82%AC%EB%A1%80%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%88-2-5%EC%9E%A5.-%EC%A7%80%ED%91%9C-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EB%B0%8F-%EA%B2%BD%EB%B3%B4-%EC%8B%9C%EC%8A%A4%ED%85%9C</guid>
            <pubDate>Sun, 31 Aug 2025 12:12:03 GMT</pubDate>
            <description><![CDATA[<p>최근 <em>「가상 면접 사례로 배우는 대규모 시스템 설계 기초 2」</em>의 <strong>5장. 지표 모니터링 및 경보 시스템</strong>을 읽었다.
책에서는 풀/푸시 모델, 시계열 DB, 다운샘플링, 경보 시스템 등 다양한 설계안을 다루지만, 실제 업계에서는 이미 <strong>Prometheus + Grafana 조합</strong>이 사실상 표준처럼 자리잡아 있다.</p>
<p>그렇다면 왜 이 조합이 표준이 되었을까? 내가 이전 회사에서 경험했던 <strong>Datadog</strong>과 비교해 보며, 지금 사이드 프로젝트에서는 어떻게 적용할 수 있을지 정리해본다.</p>
<hr>
<h2 id="업계-표준-prometheus--grafana">업계 표준: Prometheus + Grafana</h2>
<p>컨테이너, 마이크로서비스, 멀티 리전 클라우드 환경에서는 단순 “업타임 체크”만으로는 부족하다.
서비스와 리소스가 초 단위로 생겼다 사라지고, 자동 확장/축소되며, 수많은 구성 요소가 분산되어 있기 때문이다.</p>
<p>이런 복잡한 환경에 필요한 것은 <strong>실시간 가시성, 유연한 질의, 사전 경보, 확장성</strong>이다.
그리고 이를 해결하는 오픈소스 표준 스택이 바로 <strong>Prometheus + Grafana</strong>다.</p>
<hr>
<h3 id="prometheus--지표-수집의-핵심">Prometheus — 지표 수집의 핵심</h3>
<p align="center">
  <img src="https://velog.velcdn.com/images/dayoung_sarah/post/a5435a8c-42e7-4686-a4fd-a9f4a99088ae/image.png" width="600"/>
</p>

<ul>
<li><strong>Pull 기반 아키텍처</strong> : 서비스 엔드포인트에서 지표를 안정적으로 긁어옴</li>
<li><strong>레이블 기반 메트릭</strong> : 다차원 데이터 구조로 세분화 및 집계 용이</li>
<li><strong>PromQL</strong> : 시계열 데이터 분석에 최적화된 강력한 쿼리 언어</li>
<li><strong>Alertmanager</strong> : 내장된 조건 기반 알림 기능</li>
<li><strong>쿠버네티스 친화적</strong> : 동적 서비스 디스커버리 기본 지원</li>
</ul>
<p>→ 단순 수집이 아니라 “맥락 있는 모니터링”을 제공한다.</p>
<h3 id="grafana--데이터를-인사이트로">Grafana — 데이터를 인사이트로</h3>
<p align="center">
  <img src="https://velog.velcdn.com/images/dayoung_sarah/post/53086ace-002a-41e4-a635-6b526f8ee95c/image.png" width="600"/>
</p>

<ul>
<li><strong>실시간 대시보드</strong> : KPI를 직관적으로 추적</li>
<li><strong>알람 연동</strong> : Slack, PagerDuty, 이메일 등과 통합</li>
<li><strong>멀티 데이터 소스 지원</strong> : Prometheus뿐 아니라 InfluxDB, Elasticsearch, CloudWatch 등도 연결</li>
<li><strong>로그/트레이스 상관분석</strong> : Loki, Tempo 같은 툴과 함께 활용 가능</li>
</ul>
<p>→ 수집된 데이터를 시각화하여 <strong>운영 의사결정</strong>에 바로 활용할 수 있게 한다.</p>
<hr>
<h3 id="opentelemetry--데이터-수집의-표준-레이어">OpenTelemetry — 데이터 수집의 표준 레이어</h3>
<p>최근 몇 년 사이 <strong>OpenTelemetry(OTel)</strong> 가 CNCF를 중심으로 사실상 업계 표준으로 자리잡고 있다.
Prometheus와 Grafana가 각각 “지표 수집/저장”과 “시각화/알림”에 특화되어 있다면, OTel은 <strong>Metrics, Logs, Traces를 모두 아우르는 데이터 수집 표준</strong>이다.</p>
<p align="center">
  <img src="https://opentelemetry.io/img/otel-diagram.svg" width="600"/>
</p>

<ul>
<li><p><strong>3 Signals 통합</strong> : 메트릭(Prometheus), 로그(Loki/Elastic), 트레이스(Jaeger/Zipkin)로 흩어져 있던 관측 신호를 하나의 SDK/API로 통합.</p>
</li>
<li><p><strong>벤더 독립성</strong> : Datadog, New Relic 같은 SaaS를 쓰더라도, OTel로 수집하면 백엔드 전환이 자유로움 → <strong>No Vendor Lock-in</strong>.</p>
</li>
<li><p><strong>Collector 아키텍처</strong> : OTel Collector가 데이터를 받아서 <strong>Prometheus Remote Write, Kafka, Jaeger, Elastic 등</strong> 다양한 백엔드로 팬아웃 가능.</p>
</li>
<li><p><strong>Prometheus/Grafana와의 궁합</strong> :</p>
<ul>
<li>Prometheus가 OTel Collector에서 노출하는 Exporter 엔드포인트를 스크랩</li>
<li>Grafana는 Collector를 직접 붙여서 Metrics/Logs/Traces를 한 화면에 시각화</li>
</ul>
</li>
</ul>
<p>즉, OpenTelemetry는 <strong>데이터 수집의 표준화 레이어</strong>, Prometheus는 <strong>저장·질의 엔진</strong>, Grafana는 <strong>시각화/분석 도구</strong>로 보완적인 역할을 한다.</p>
<p>👉 앞으로는 “OTel → Prometheus → Grafana”가 현대 관측성 스택의 정석이 될 가능성이 크다.
특히 서비스가 성장하면서 “단순 지표”에서 “로그/트레이스까지 연결된 풀옵저버빌리티”를 원할 때, OpenTelemetry는 필수다.</p>
<hr>
<h3 id="세-가지의-시너지">세 가지의 시너지</h3>
<ul>
<li><strong>OpenTelemetry</strong>: 벤더에 독립적인 데이터 수집</li>
<li><strong>Prometheus</strong>: 안정적인 메트릭 저장 및 분석</li>
<li><strong>Grafana</strong>: 직관적이고 유연한 시각화</li>
</ul>
<p>이 세 가지를 결합하면, 클라우드 네이티브 환경에서 <strong>현대적 관측성(Observability) 스택</strong>을 완성할 수 있습니다.</p>
<hr>
<h3 id="실제-사례-airbnb의-결제-서비스-모니터링">실제 사례: Airbnb의 결제 서비스 모니터링</h3>
<p>Airbnb 같은 대규모 플랫폼은 <strong>결제 서비스</strong> 장애 시 즉각적인 매출 손실이 발생합니다.</p>
<ul>
<li>Prometheus가 결제 서비스 노드의 응답시간, 에러율, 메모리 사용량 등을 실시간으로 수집·저장.</li>
<li>Grafana가 이를 시각화해 운영팀이 한눈에 상태를 확인.</li>
<li>Prometheus 알람 규칙: 에러율 증가, 지연 시간 급등 시 즉각 Slack/PagerDuty 알림.</li>
<li>OpenTelemetry는 서비스 전반에서 공통적으로 Metrics/Logs/Traces를 수집, 다른 시스템과 연계 가능.</li>
</ul>
<p>결과적으로 <strong>문제 발생 전에 선제 대응</strong>이 가능하고, 비즈니스 연속성을 보장합니다.</p>
<hr>
<h2 id="확장-설계-왜-thanosmimir-같은-lts와-kafka가-필요할까">(확장 설계) 왜 Thanos/Mimir 같은 LTS와 Kafka가 필요할까?</h2>
<p>Prometheus 단독으로도 훌륭하지만, <strong>장기 보관·멀티 클러스터 통합·대규모 트래픽 흡수</strong> 같은 요구가 생기면 확장 컴포넌트가 필요하다.</p>
<h3 id="thanosmimir--장기보관--글로벌-뷰">Thanos/Mimir — 장기보관 &amp; 글로벌 뷰</h3>
<ul>
<li><strong>장기 보관(수개월~수년)</strong>: Prometheus 로컬 블록을 <strong>오브젝트 스토리지(S3/GCS)</strong>에 올려 보관(저렴).</li>
<li><strong>글로벌 쿼리/중복 제거</strong>: 여러 클러스터·리전에 분산된 Prometheus를 하나의 <strong>글로벌 쿼리</strong>로 조회, 이중화된 시계열은 <strong>dedup</strong>.</li>
<li><strong>다운샘플링/컴팩션</strong>: 오래된 데이터는 5m/1h 등으로 해상도를 낮춰 <strong>비용·성능</strong>을 균형화.</li>
<li><strong>구성 요소(개념)</strong>: Sidecar(업로드/프록시), Store Gateway(콜드 데이터 제공), Query/Frontend(글로벌 쿼리/캐시), Compactor(다운샘플링), Ruler(장기 규칙/알림).</li>
</ul>
<p><strong>효과</strong>: Grafana 한 화면에서 <strong>단기(로컬) + 장기(S3)</strong>를 끊김 없이 조회하고, 멀티 클러스터를 <strong>한 번에</strong> 본다.</p>
<h3 id="kafka--버퍼링--팬아웃">Kafka — 버퍼링 &amp; 팬아웃</h3>
<p>Prometheus의 표준은 Pull이지만, <strong>폭주 트래픽·다양한 소스·멀티 소비자</strong> 요구가 있으면 <strong>Kafka</strong>를 중간 버스에 둔다.</p>
<ul>
<li><strong>디커플링/버퍼링</strong>: 수집 ↔ 저장/소비를 분리해 <strong>스파이크</strong>와 <strong>일시 장애</strong>를 흡수.</li>
<li><strong>팬아웃</strong>: 한 번 들어온 메트릭 스트림을 <strong>알림·이상탐지·ML·장기 저장</strong> 등 여러 소비자가 공유.</li>
<li><strong>적용 패턴(개념)</strong>:<ul>
<li>가장 단순: <strong>remote_write → Thanos/Mimir</strong> (Kafka 없음)</li>
<li>확장형: <strong>OTel Collector → Kafka →(여러 Consumer)→ remote_write(LTS)</strong></li>
</ul>
</li>
<li><strong>설계 팁</strong>: 파티션 키는 <code>metric_name + 안정 라벨(app/namespace)</code> 중심, <strong>고카디널리티 라벨</strong>(user_id/URL full path)은 금지/정규화.</li>
</ul>
<hr>
<h2 id="사용했던-모니터링-도구">사용했던 모니터링 도구</h2>
<h3 id="1-datadog">1. Datadog</h3>
<p>직전 회사에서는 지표 모니터링 도구로 <strong>Datadog</strong>을 사용했다. Datadog은 모니터링, 로깅, APM(Application Performance Monitoring)을 아우르는 SaaS 기반 플랫폼이다.</p>
<ul>
<li><p><strong>장점</strong></p>
<ul>
<li><strong>올인원(All-in-one) 플랫폼</strong> : 인프라 모니터링, 애플리케이션 성능 추적, 로그 분석, 사용자 경험 모니터링까지 하나의 콘솔에서 제공한다.</li>
<li><strong>빠른 도입</strong> : 에이전트 설치와 간단한 설정만으로 대부분의 클라우드 리소스와 애플리케이션이 자동 계측된다.</li>
<li><strong>운영 부담 최소화</strong> : 시계열 DB, 저장소, Alertmanager 등 개별 컴포넌트를 직접 관리할 필요가 없고, SaaS이므로 장애 대응이나 확장성 확보도 Datadog이 맡아준다.</li>
<li><strong>통합 생태계</strong> : AWS, GCP, Kubernetes, DB, 메시지 큐 등 수백 개의 서비스와 원클릭으로 연동 가능하다.</li>
</ul>
</li>
<li><p><strong>단점</strong></p>
<ul>
<li><strong>비용 구조</strong> : 모니터링 지표(metric)의 수와 카디널리티가 늘어날수록 과금이 기하급수적으로 증가한다.</li>
<li><strong>벤더 종속성</strong> : 모든 모니터링이 Datadog SaaS 안에서 이루어지므로, 서비스 성장 시 비용 최적화를 위한 자유도가 떨어진다.</li>
</ul>
</li>
<li><p><strong>왜 사용했는가?</strong></p>
<ul>
<li>빠르게 모니터링 체계를 구축할 수 있다.</li>
<li><strong>즉시 효과</strong> : 대시보드, 알림, 트레이싱까지 바로 구축할 수 있다.</li>
</ul>
</li>
</ul>
<p>👉 그러나 서비스가 성장하면서 모니터링 지표 수와 사용자 트래픽이 늘어나자, <strong>비용 부담이 현실적인 한계</strong>로 다가왔다.
결국 대규모 서비스를 운영하는 기업들은 SaaS보다는 <strong>Prometheus + Grafana 같은 오픈소스 조합</strong>을 선택할 수밖에 없다는 걸 체감할 수 있었다.</p>
<hr>
<h3 id="2-cloudwatch">2. CloudWatch</h3>
<p>현재 사이드 프로젝트 <em>‘필사(Fillsa)’</em>에서는 AWS CloudWatch를 활용해 기본적인 모니터링을 하고 있다.</p>
<ul>
<li><p><strong>장점</strong></p>
<ul>
<li><strong>AWS 리소스와 네이티브 통합</strong> : EC2, RDS, Lambda, ALB 등 AWS에서 운영되는 모든 서비스가 CloudWatch 메트릭을 기본적으로 노출한다. 별도의 에이전트 설치나 엔드포인트 구성 없이 바로 수집 가능하다.</li>
<li><strong>운영 부담 최소화</strong> : 시계열 DB 운영, 저장소 증설, 인덱스 최적화 등을 신경쓸 필요가 없다. AWS가 백그라운드에서 관리해주므로 개발자는 모니터링 로직이 아닌 서비스 개발에 집중할 수 있다.</li>
<li><strong>비용 효율성</strong> : 기본 메트릭은 대부분 무료 제공되며, 알람 설정 및 단순 대시보드 사용은 추가 비용이 거의 없다. 사이드 프로젝트 같은 소규모 서비스에는 가장 경제적인 옵션이다.</li>
<li><strong>확장성</strong> : 서비스가 확장되더라도 AWS 인프라 위에서라면 CloudWatch 메트릭 수집 범위는 자동으로 늘어난다.</li>
</ul>
</li>
<li><p><strong>단점</strong></p>
<ul>
<li><strong>지표 표현의 제약</strong> : 시스템 리소스 수준(CPU, Memory, Network, Disk 등) 지표에는 강하지만, 비즈니스 레벨 지표나 애플리케이션 내부 메트릭(API 응답 속도, 에러율 등) 추적에는 한계가 있다.</li>
<li><strong>데이터 보존 및 분석 한계</strong> : 세밀한 시계열 분석(예: PromQL 같은 질의)은 불가능하며, 장기간 데이터는 집계 단위가 거칠어져 정밀한 분석이 어렵다.</li>
</ul>
</li>
<li><p><strong>왜 CloudWatch를 쓰는가?</strong></p>
<ul>
<li><strong>초기 단계의 요구사항에 부합</strong> : 현재 필사 프로젝트는 트래픽이 크지 않고, 운영 인프라도 단순하다. 따라서 “서버가 다운되기 직전 CPU가 급등하면 알림 받는다” 수준이면 충분하다.</li>
<li><strong>운영 복잡성을 최소화</strong> : Prometheus 같은 시계열 DB를 직접 운영하려면 Exporter 설치, 스토리지 관리, 업그레이드, Alertmanager 설정 등 관리 비용이 커진다. 사이드 프로젝트 단계에서 이런 인프라 운영 리소스를 쓰는 것은 과도하다.</li>
<li><strong>TCO(Total Cost of Ownership) 관점의 합리성</strong> : 인건비까지 고려했을 때, CloudWatch는 &quot;Zero Ops&quot;에 가깝다. 즉, 별도의 운영 담당자 없이도 기본적인 모니터링 체계를 유지할 수 있다.</li>
</ul>
</li>
</ul>
<p>👉 결론적으로, <strong>CloudWatch는 작은 규모 서비스에서는 운영 비용을 최소화하면서 안정성을 확보할 수 있는 가장 현실적인 선택</strong>이다. 하지만 장기적으로 서비스가 성장하면 <strong>애플리케이션 지표, 사용자 지표, 커스텀 메트릭</strong>을 추적해야 하고, 이때는 CloudWatch만으로는 부족하다. 그 시점부터는 Prometheus + Grafana 같은 <strong>확장 가능한 오픈소스 모니터링 스택</strong>으로의 전환이 필요하다.</p>
<hr>
<h2 id="왜-prometheusgrafana가-표준이-되었을까">왜 Prometheus+Grafana가 표준이 되었을까?</h2>
<p>다른 도구와 비교해 보면 이유가 명확하다.</p>
<ul>
<li><strong>Datadog</strong> : 쉽고 강력하지만 비용이 급격히 증가</li>
<li><strong>New Relic</strong> : APM에 강점 있으나 가격·벤더 종속 문제</li>
<li><strong>Zabbix</strong> : 전통적 환경에는 적합하나 클라우드 네이티브 확장성 부족</li>
<li><strong>Elastic Stack</strong> : 로그 분석엔 탁월하지만 운영 난이도 높음</li>
</ul>
<p>반면 Prometheus+Grafana는:</p>
<ul>
<li>오픈소스 → <strong>비용 효율</strong></li>
<li>완전한 데이터 소유권, 벤더 락인 없음</li>
<li>쿠버네티스 친화적, 확장성 탁월</li>
<li>활발한 커뮤니티</li>
</ul>
<p>즉, “비용 + 확장성 + 커스터마이즈” 3가지를 동시에 충족한다.
그래서 GitLab, Red Hat 같은 대규모 기업들도 이 조합을 표준으로 사용한다.</p>
<hr>
<h2 id="앞으로-cloudwatch에서-prometheusgrafana로">앞으로: CloudWatch에서 Prometheus+Grafana로</h2>
<p>현재 필사(Fillsa) 사이드 프로젝트에서는 AWS CloudWatch만으로 모니터링을 하고 있다.
→ CPU 알림 수준의 단순 모니터링이라, <strong>애플리케이션 성능이나 비즈니스 지표</strong>를 파악하기에는 부족하다.</p>
<p>앞으로는 Aiva 프로젝트에 <strong>Prometheus + Grafana를 적용</strong>해보고자 한다.</p>
<ul>
<li>초기: CloudWatch로 최소 리소스만 확인</li>
<li>성장: Prometheus로 API 레이턴시/에러율/DB 상태 수집</li>
<li>운영: Grafana 대시보드로 운영팀/개발팀 실시간 확인</li>
<li>확장: Kafka 파이프라인 + Cold Storage 정책으로 규모 확장</li>
</ul>
<p>👉 <strong>CloudWatch에서 출발해 Prometheus+Grafana로 발전시키는 것</strong>이 내 다음 목표다.</p>
<hr>
<h2 id="결론">결론</h2>
<ul>
<li><strong>Datadog</strong> : 빠른 도입과 운영 편의성, 하지만 비용이 발목</li>
<li><strong>CloudWatch</strong> : 단순 리소스 모니터링은 충분, 애플리케이션 지표는 한계</li>
<li><strong>Prometheus+Grafana</strong> : 현대 모니터링의 표준, 비용 효율·확장성·가시성 모두 충족</li>
</ul>
<p>책에서 배운 설계안과 업계 사례를 내 경험에 비추어 보니,
내 사이드 프로젝트도 CloudWatch를 넘어서 <strong>Prometheus+Grafana로 디벨롭</strong>해야겠다는 다짐이 섰다.</p>
<p>참고문서
<a href="https://medium.com/%40lamjed.gaidi070/why-prometheus-and-grafana-are-the-gold-standard-for-monitoring-modern-systems-153a40fb4fae">Medium - Why Prometheus and Grafana Are the Gold Standard for Monitoring Modern Systems</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[가상 면접 사례로 배우는 대규모 시스템 설계 기초 2] 주변 친구]]></title>
            <link>https://velog.io/@dayoung_sarah/%EA%B0%80%EC%83%81-%EB%A9%B4%EC%A0%91-%EC%82%AC%EB%A1%80%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%88-2-2%EC%9E%A5.-%EC%A3%BC%EB%B3%80-%EC%B9%9C%EA%B5%AC</link>
            <guid>https://velog.io/@dayoung_sarah/%EA%B0%80%EC%83%81-%EB%A9%B4%EC%A0%91-%EC%82%AC%EB%A1%80%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%88-2-2%EC%9E%A5.-%EC%A3%BC%EB%B3%80-%EC%B9%9C%EA%B5%AC</guid>
            <pubDate>Sat, 09 Aug 2025 17:11:53 GMT</pubDate>
            <description><![CDATA[<p align="center">
  <img src="https://velog.velcdn.com/images/dayoung_sarah/post/689a21e9-781a-4195-a794-8eb46746159a/image.png" width="300"/>
</p>


<blockquote>
<p>이번 챕터에서 양방향 통신을 위해 웹소켓 서버가 나오는데,
마침 채팅 서비스를 최근에 구현해야 해서 이부분에 대해 학습했는데 타이밍이 ㄷㄷ
1장보단 한큐에 이해가 가진 않았다. 집중력이 떨어진건지,, 한 4번 속독한듯.
레디스 펍/섭 부분은 개념적으로는 이해가 가는데 실제로 어떻게 구현된다는 건지 이해가 부족한것 같아 이부분은 더 찾아봐야 할듯</p>
</blockquote>
<h2 id="1단계">1단계</h2>
<h3 id="기능적-요구사항">기능적 요구사항</h3>
<ul>
<li>주변 친구 목록 표시<ul>
<li>거리이내</li>
</ul>
</li>
<li>주변 친구 목록 갱신<ul>
<li>10분 이상 비활성화 사라짐</li>
</ul>
</li>
</ul>
<h3 id="비기능-요구사항">비기능 요구사항</h3>
<ul>
<li>낮은 지연 시간</li>
</ul>
<h3 id="추정">추정</h3>
<ul>
<li>5마일 검색 반경</li>
<li>위치 갱신 주기: 30초</li>
<li>위치 정보 갱신 QPS: 334k/초</li>
</ul>
<h2 id="2단계">2단계</h2>
<h3 id="계획적-설계안">계획적 설계안</h3>
<ul>
<li>RESTful API 서버<ul>
<li>일반전인 친구 추가/삭제/수정</li>
</ul>
</li>
<li>웹소켓 서버<ul>
<li>위치 / 주변 친구 활성화</li>
</ul>
</li>
<li>레디스 위치 정보 캐시<ul>
<li>10분간 비활성화 시 데이터 삭제</li>
</ul>
</li>
<li>위치 이동 이력 데이터베이스<ul>
<li>주변 친구 표시와 직접 관계된 기능 x</li>
</ul>
</li>
<li>레디스 펍/섭 서버<ul>
<li>위치 정보 갱신 시 펍/섭 채널에 이벤트 발행</li>
</ul>
</li>
</ul>
<h3 id="주기적-위치-갱신">주기적 위치 갱신</h3>
<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/39dda759-65a3-41c2-834e-c6fdc17176b2/image.png" alt=""></p>
<ol>
<li>모바일 클라이언트가 위치 변경된 사실을 로드밸런서에 전송</li>
<li>로드밸런서는 위치 변경 내역을 해당 클라이언트와 웹소켓 서버 사이에 설정된 연결을 통해 웹소켓 서버로 보낸다.</li>
<li>웹소켓 서버는 해당 이벤트를 위치 이동 이력 데이터 베이스에 저장</li>
<li>웹소켓 서버는 새 위치를 위치 정보 캐시에 보관. TTL 갱신.</li>
<li>웹소켓 서버는 레디스 펍/섭 서버의 해당 사용자 채널에 새 위치 발행 (3 ~ 5 병렬로 수행)</li>
<li>레디스 펍/섭 채널에 발행된 새로운 위치 변경 이벤트는 모든 구독자에게 브로드캐스트. 이때 구독자는 위치 변경 이벤트를 보낸 사용자의 온라인 상태 친구들. 구독자의 웹소켓 연결 핸들러는 친구의 위치 변경 이벤트를 수신.</li>
<li>메시지를 받은 웹소켓 서버, 즉 연결된 웹소켓 서버는 새 위치를 보낸 사용자와 메시지를 받은 사용자 사이의 거리 새로 계산.</li>
<li>7에서 계산한 거리가 검색 반경을 넘지 않는다면, 해당 구독자의 클라이언트 앱으로 위치 정보 변경 내역 전송.</li>
</ol>
<h3 id="api-설계">API 설계</h3>
<ol>
<li>[서버 API] 주기적인 위치 정보 갱신</li>
<li>[클라이언트 API] 클라이언트가 갱신된 친구 위치를 수신하는 데 사용할 API</li>
<li>[서버 API] 웹소켓 초기화 API</li>
<li>[클라이언트 API] 새 친구 구독 API</li>
<li>[클라이언트 API] 구독 해지 API</li>
</ol>
<h3 id="데이터-모델">데이터 모델</h3>
<ul>
<li>위치 정보 캐시<ul>
<li>‘주변 친구’ 기능은 사용자의 현재 위치만 이용 → 영구저장 x → 캐시 처리</li>
</ul>
</li>
<li>위치 이동 이력 데이터베이스</li>
</ul>
<h2 id="3단계">3단계</h2>
<h3 id="각-컴포넌트의-규모-확장-전략">각 컴포넌트의 규모 확장 전략</h3>
<ul>
<li>API 서버</li>
<li>웹소켓 서버 클러스터<ul>
<li>웹소켓 서버는 유상태 → 기존 연결 종료시 주의</li>
</ul>
</li>
<li>사용자 정보 데이터베이스<ul>
<li>사용자 ID를 기준으로 샤딩</li>
</ul>
</li>
<li>위치 정보 캐시<ul>
<li>사용자의 위치 정보 → 서로 독립적 → 사용자 ID를 기준으로 샤딩</li>
</ul>
</li>
<li>레디스 펍/섭 서버 클러스터</li>
<li>레디스 펍/섭 외의 대안<ul>
<li>얼랭</li>
</ul>
</li>
</ul>
<h3 id="친구-추가삭제">친구 추가/삭제</h3>
<h3 id="친구가-많은-사용자">친구가 많은 사용자</h3>
<h3 id="주변-임의-사용자">주변 임의 사용자</h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[[가상 면접 사례로 배우는 대규모 시스템 설계 기초 2] 근접성 서비스]]></title>
            <link>https://velog.io/@dayoung_sarah/%EA%B0%80%EC%83%81-%EB%A9%B4%EC%A0%91-%EC%82%AC%EB%A1%80%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%88-2-1%EC%9E%A5.-%EA%B7%BC%EC%A0%91%EC%84%B1-%EC%84%9C%EB%B9%84%EC%8A%A4</link>
            <guid>https://velog.io/@dayoung_sarah/%EA%B0%80%EC%83%81-%EB%A9%B4%EC%A0%91-%EC%82%AC%EB%A1%80%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%B4%88-2-1%EC%9E%A5.-%EA%B7%BC%EC%A0%91%EC%84%B1-%EC%84%9C%EB%B9%84%EC%8A%A4</guid>
            <pubDate>Sat, 09 Aug 2025 17:07:05 GMT</pubDate>
            <description><![CDATA[<p align="center">
  <img src="https://velog.velcdn.com/images/dayoung_sarah/post/4a023be5-e046-4c66-bb2d-a16d179832f4/image.png" width="400"/>
</p>



<p>서울시여성가족재단에서 운영하는 스터디에 지원했다.구글 클라우드와 협력해 생성형 AI 분야를 실습까지 해볼 수 있는 좋은 기회였기 때문이다 ! ! !
마침 대규모 시스템에 관심이 많아 도서관에서 가상 면접 사례로 배우는 대규모 시스템 설계 기초 1권을 읽고 있었는데, 이번 시즌에 2권 스터디를 진행하는 팀이 있는 것을 발견했다. (운명인가ㄷㄷ)</p>
<p>합격 발표날 연락이 없어 탈락했다고 생각했는데, 저녁 늦게 결과가 나왔다. 합격이었다 ~~ (오예)
OT에서 팀장님과 팀원분들과 간단히 인사와 자기소개를 나누었는데, 30명이 넘게 지원했다고 한다. 약 3:1 경쟁률을 뚫었다니 더욱 열심히 임해야겠다고 다짐했다 ^___^</p>
<hr>
<blockquote>
<p><strong>[학습흐름]</strong></p>
</blockquote>
<ol>
<li>2회 속독</li>
<li>도식화 요약본 확인</li>
<li>기억나는 내용 작성</li>
<li>부족한 부분 재독</li>
<li>보완</li>
</ol>
<h2 id="1단계">1단계</h2>
<h3 id="기능적-요구사항">기능적 요구사항</h3>
<ul>
<li>주변 사업장 검색<ul>
<li>사용자가 검색 변경 지정 가능한지 (YES)<ul>
<li>최대 허용 반경(20km)</li>
<li>검색변경 반경 목록(0.5km, 1km, 2km, 5km, 20km)</li>
</ul>
</li>
<li>주어진 반경 내 정보가 충분하지 여부 → 더 넓혀야 하는지 (NO)</li>
<li>사용자 위치 → 상시 갱신 해야 하는지 (NO, 이동속도가 그렇게 빠르지 않을거라)</li>
</ul>
</li>
<li>사업장 정부 추가/삭제/갱신<ul>
<li>즉시 반영되어야 하는가 (NO, 다음날 반영가능 계약서상)</li>
</ul>
</li>
<li>사업장 정보 조회</li>
</ul>
<h3 id="비기능-요구사항">비기능 요구사항</h3>
<ul>
<li>낮은 응답 지연 → 사용자는 주변 사업장을 신속히 검색 가능해야 한다.</li>
<li>데이터 보호 → 사용자 위치는 민감한 정보</li>
<li>5k 검색 QPS → 고가용성 및 규모 확장성(인구 밀집 지역 + 트래픽 집중 시간 감당 가능해야 한다)</li>
</ul>
<h2 id="2단계">2단계</h2>
<h3 id="api-설계">API 설계</h3>
<ul>
<li>검색 API</li>
<li>사업장 정보 관리 API</li>
<li>페이지 분할</li>
</ul>
<h3 id="데이터-모델">데이터 모델</h3>
<ul>
<li>읽기/쓰기 연산 비율<ul>
<li>사업장 정보는 읽기 &gt; 쓰기</li>
<li>읽기 연산이 압도적이므로 관계형 데이터베이스가 적합</li>
</ul>
</li>
<li>데이터 스키마<ul>
<li>business 테이블</li>
<li>지리적 위치 색인 테이블</li>
</ul>
</li>
</ul>
<h3 id="개략적인-설계-도면">개략적인 설계 도면</h3>
<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/6a1a7e93-0eb3-4e11-9b18-7ab71ce690e0/image.png" alt=""></p>
<ul>
<li>로드밸런서<ul>
<li>읽기/쓰기 연산 분리</li>
</ul>
</li>
<li>LBS(위치 기반 서비스)<ul>
<li>읽기 요청 서비스</li>
<li>QPS 가 높다.</li>
<li>무상태 서비스 → 수평적 규모 확장이 쉽다</li>
</ul>
</li>
<li>사업장 서비스<ul>
<li>쓰기 요청 서비스</li>
<li>QPS가 높지 않다.</li>
<li>고객이 사업장 조회할때 QPS 높아진다.</li>
</ul>
</li>
<li>데이터베이스 클러스터<ul>
<li>주-부(primary-secondary) 데이터베이스 형태</li>
<li>주 데이터베이스 → 쓰기 요청 처리</li>
<li>부 테이터베이스 → 읽기 요청 처리</li>
<li>복제에 걸리는 시간 지연이 있을수 있다. → 사업장 정보는 실시간 반영 아니라 괜찮</li>
</ul>
</li>
</ul>
<h3 id="알고리즘">알고리즘</h3>
<ul>
<li>해시 기반 방안: 균등 격자, 지오해시, 카르테시안 계층</li>
<li>트리 기반 방안: 쿼드트리, 구글 S2, R 트리</li>
</ul>
<p>→ 지도를 작은 영역으로 분할하고 고속 검색이 가능하도록 색인을 만든다.</p>
<p>가장 널리: 지오해시, 쿼드트리, 구글 S2</p>
<ul>
<li>2차원 검색</li>
<li>균등 분할 격자</li>
<li>지오해시<ul>
<li>격자를 나누어 비트를 하나씩 늘려 재귀적으로 분할해 나간다.</li>
<li>이슈: 격자 가장자리 이슈</li>
<li>구현/사용이 쉽다. 색인 갱신이 쉽다.</li>
</ul>
</li>
<li>쿼드트리<ul>
<li>특정 기준을 만족할 때까지 2차원 공간을 재귀적으로 사분면 분할(내부/말단 노드)</li>
<li>이슈: 메모리에 쿼드트리 인덱스를 미리 구축해야하고, 이에 시간이 소요된다. → 청/녹배포</li>
<li>인구 밀도에 따라 격자 크기를 동적으로 조정할 수 있다.</li>
<li>지오해시보다 색인 갱신은 까다롭다. (그 부분 다시 구축해야 하니깐)</li>
</ul>
</li>
<li>구글 S2<ul>
<li>구글맵, 틴더</li>
</ul>
</li>
<li>지오해시 vs 쿼드트리</li>
</ul>
<h2 id="3단계">3단계</h2>
<h3 id="데이터베이스-규모-확장">데이터베이스 규모 확장</h3>
<ul>
<li>사업장 정보 테이블<ul>
<li>한 서버에 담을 수 없을 수 있다 → 샤딩 적용하기 좋은 후보 (사업장 ID)</li>
</ul>
</li>
<li>지리 정보 색인 테이블<ul>
<li>지오해시 - 사업장id</li>
</ul>
</li>
</ul>
<h3 id="캐시">캐시</h3>
<ul>
<li>캐시 계층 도입 전 정말 필요한가? → 읽기 연산이 하나로 부족하면 도입 ㄱ</li>
<li>캐시에서 없으면 디비 읽고 캐시에 추가(정보 업뎃되면 무효화 ㄱ)</li>
<li>캐시 키</li>
<li>데이터 유형</li>
</ul>
<h3 id="지역-및-가용성-구역">지역 및 가용성 구역</h3>
<ul>
<li>위치 기반 서비스는 여러 지역과 가용성 구역에 설치한다.<ul>
<li>사용자와 시스템 사이의 물리적 거리 최소한(미국인 → 미국 센터)</li>
<li>트래픽을 인구에 따라 고르게 분산하는 유연성 확보(일본/한국은 인구 밀도 높다. 별도로 빼기)</li>
</ul>
</li>
</ul>
<h3 id="결과-필터링">결과 필터링</h3>
<p>필터링할 데이터도 추가해 두면 됨.</p>
<h3 id="최종-설계-도면">최종 설계 도면</h3>
<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/85f02bdb-73eb-4438-b1e1-8fe01896184f/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Figma Make로 UI와 코드까지 한번에 자동화해보기]]></title>
            <link>https://velog.io/@dayoung_sarah/Figma-Make%EB%A1%9C-Flutter-UI%EC%99%80-%EC%BD%94%EB%93%9C%EA%B9%8C%EC%A7%80-%EC%9E%90%EB%8F%99%ED%99%94%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@dayoung_sarah/Figma-Make%EB%A1%9C-Flutter-UI%EC%99%80-%EC%BD%94%EB%93%9C%EA%B9%8C%EC%A7%80-%EC%9E%90%EB%8F%99%ED%99%94%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 26 Jul 2025 17:13:18 GMT</pubDate>
            <description><![CDATA[<h3 id="1-들어가며--기획서에-화면이-필요할-때">1. 들어가며 – 기획서에 화면이 필요할 때</h3>
<p>최근 해커톤에 참가하면서, 기획서에 <strong>앱 화면 구성(프로토타입)</strong>을 함께 제출해야 하는 상황이 생겼다.
아직 디자인이 픽스된 상태는 아니고, 개발 일정도 빠듯한 상황에서 <strong>디자인 + 개발 + 프로토타이핑을 동시에 빠르게</strong> 해결할 방법이 필요했다.</p>
<p>바로 그때 눈에 들어온 게 <strong>Figma Make</strong>.
기획서 내용과 스토리보드만으로, <strong>디자인부터 코드까지 한 번에 만들어주는 AI 기능</strong>이다.
이걸 제대로 써보니, 상상 이상으로 유용했다.</p>
<hr>
<h3 id="2-figma-make란">2. Figma Make란?</h3>
<p>Figma에서 최근 공개한 <strong>AI 생성 기능</strong> 중 하나로,</p>
<ul>
<li>기획 설명</li>
<li>스토리보드 이미지</li>
<li>간단한 텍스트 프롬프트
만 있으면, 실제 앱 화면을 자동으로 디자인해준다.</li>
</ul>
<p>심지어 React, HTML 등 <strong>실제 코드로 변환</strong>까지 가능해, <strong>디자인 → 개발 전환의 간극</strong>을 크게 줄여주는 도구다.</p>
<hr>
<h3 id="3-내가-활용한-방식">3. 내가 활용한 방식</h3>
<p>기획서 작성 중, 다음과 같은 방식으로 활용했다:</p>
<h4 id="①-프롬프트-1-서비스-설명--스토리보드">① 프롬프트 1: 서비스 설명 + 스토리보드</h4>
<ul>
<li>어떤 기능을 제공하는지 간략히 설명</li>
<li>각 화면(총 4개): 어떤 목적이고, 어떤 요소가 들어가는지 요약
→ 이 내용을 기반으로 <strong>기초적인 화면 구성과 Flutter 코드</strong>를 생성해줌</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/ef854d97-97ba-4d07-bf67-fc09b132f852/image.png" alt=""></p>
<h4 id="②-프롬프트-2-앱-이름-기반-디자인-시스템-적용">② 프롬프트 2: 앱 이름 기반 디자인 시스템 적용</h4>
<ul>
<li>앱 이름과 성격을 다시 알려주고(앱 명칭 변경이슈로 인해 다시 알려준;;)</li>
<li>“이 앱에 어울리는 디자인 시스템 몇 개 보여줘”라고 요청
→ 디자인 테마를 제안받고, 디자인별 설명과 함께 즉시 화면에 적용해보며 선택 가능
→ <strong>무채색 와이어프레임 → 실제 디자인 느낌</strong>으로 전환</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/0220dbb0-2a6a-4611-a5f4-43ab61b025b8/image.png" alt=""></p>
<hr>
<h3 id="4-사용-후기">4. 사용 후기</h3>
<ul>
<li><p><strong>단 3번의 프롬프트</strong>만으로</p>
</li>
<li><p><em>UI 구성 → 디자인 시안 → React 코드 생성*</em>까지 전 과정을 자동으로 처리할 수 있었다.</p>
</li>
<li><p>물론 완성본이 아닌 <strong>프로토타입용 퀄리티</strong>지만,
화면 구성력과 코드 구조가 너무 괜찮아서 <strong>설득력 있는 기획서 작성</strong>에 큰 도움이 됐다.</p>
</li>
<li><p>결과적으로 이 기획서를 통해 <strong>해커톤 예선 통과</strong>까지 성공!
이후에는 기획자분이 본격적인 디자인을 진행하고, 나는 그걸 기반으로 <strong>Figma Make + MCP + Cursor AI</strong>를 연동해 <strong>협업형 바이브 코딩</strong>까지 진행할 예정이다.</p>
</li>
</ul>
<hr>
<h3 id="5-요약--이-기능-언제-쓰면-좋을까">5. 요약 – 이 기능, 언제 쓰면 좋을까?</h3>
<ul>
<li>아이디어가 있는데 디자인 인력이 없는 경우</li>
<li>빠르게 앱 화면 흐름을 보여줘야 할 때</li>
<li>기획자/개발자가 <strong>기획-디자인-개발</strong>을 혼자 해야 하는 사이드 프로젝트</li>
<li>해커톤 등 <strong>프로토타입 중심의 데모 제작</strong>에 매우 적합</li>
</ul>
<hr>
<h3 id="다음-글-예고-✍">다음 글 예고 ✍</h3>
<p>Figma Make가 만들어준 UI 코드를 <strong>실제 프로젝트에 어떻게 적용하는지</strong>에 대해 다뤄보겠습니다.
<strong>MCP + Cursor AI 연동 바이브코딩</strong>까지 실제 예시와 함께 풀어볼게요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[API 트래픽 제어의 핵심, Rate Limiting vs Throttling]]></title>
            <link>https://velog.io/@dayoung_sarah/API-%ED%8A%B8%EB%9E%98%ED%94%BD-%EC%A0%9C%EC%96%B4%EC%9D%98-%ED%95%B5%EC%8B%AC-Rate-Limiting-vs-Throttling</link>
            <guid>https://velog.io/@dayoung_sarah/API-%ED%8A%B8%EB%9E%98%ED%94%BD-%EC%A0%9C%EC%96%B4%EC%9D%98-%ED%95%B5%EC%8B%AC-Rate-Limiting-vs-Throttling</guid>
            <pubDate>Wed, 16 Jul 2025 06:44:19 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>API를 운영하다 보면 트래픽이 갑자기 몰려 서버에 부하가 걸리는 상황을 종종 겪게 된다.
이럴 때 보통은 코드 레벨에서 캐시를 추가하거나, 비동기 처리로 대응하려고 하지만, 이런 방식만으로는 한계가 있다. <br>
그래서 이번엔 <strong>코드가 아니라, 그보다 한 단계 더 높은 레벨(예: 인프라나 API 게이트웨이)</strong>에서
어떻게 트래픽을 제어하고 방어할 수 있을지 고민하던 중, <a href="https://api7.ai/ko/learning-center/api-101/rate-limiting-and-throttling">API7.ai</a>에서 잘 정리된 아티클을 발견하게 되었다.<br>
Rate Limiting과 Throttling이라는 개념을 중심으로, API를 어떻게 안정적으로 보호하고 남용을 막을 수 있는지를 다루고 있어서, 그 내용을 번역하고 정리해봤다.</p>
</blockquote>
<hr>
<h2 id="✅-소개">✅ 소개</h2>
<p>API는 디지털 고속도로와 같으며, <strong>트래픽 규칙 없이 운용되면 서버 과부하, 보안 취약점, 사용자 불만</strong>이 발생합니다.
Alibaba Cloud의 2023년 설문조사에 따르면 <strong>개발자 78%가 API 남용을 주요 보안 문제</strong>로 꼽았습니다.
이를 방지하기 위해 사용하는 기술이 바로 <strong>Rate Limiting</strong>과 <strong>Throttling</strong>입니다.</p>
<hr>
<h2 id="✅-문제-통제되지-않는-api-트래픽">✅ 문제: 통제되지 않는 API 트래픽</h2>
<table>
<thead>
<tr>
<th>문제점</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>서비스 중단 위험</td>
<td>한 사용자가 수백만 요청을 보내면 서버가 마비될 수 있음</td>
</tr>
<tr>
<td>보안 위협</td>
<td>무차별 대입 공격(Brute-force), DDoS 공격</td>
</tr>
<tr>
<td>사용자 경험 저하</td>
<td>정당한 사용자도 지연이나 오류를 겪게 됨</td>
</tr>
</tbody></table>
<hr>
<h2 id="✅-용어-정리">✅ 용어 정리</h2>
<h3 id="📌-rate-limiting-요청-수-제한">📌 Rate Limiting (요청 수 제한)</h3>
<ul>
<li><strong>정해진 시간 안에 보낼 수 있는 요청 수 제한</strong></li>
<li>예: 분당 100건 요청 허용</li>
<li>주요 용도:<ul>
<li>DDoS 방어 (예: 트위터는 앱당 분당 150만 건 제한)</li>
<li>프리미엄/프리 유저 차등 적용</li>
</ul>
</li>
</ul>
<h3 id="📌-throttling-요청-속도-제한">📌 Throttling (요청 속도 제한)</h3>
<ul>
<li><strong>초과 요청을 차단하지 않고 느리게 처리</strong></li>
<li>예: 블랙프라이데이처럼 요청이 몰릴 때 점진적으로 응답</li>
<li>주요 용도:<ul>
<li>트래픽 급증 완화</li>
<li>우선순위 요청(예: 결제 API) 처리</li>
</ul>
</li>
</ul>
<hr>
<h2 id="✅-주요-차이점">✅ 주요 차이점</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>Rate Limiting</th>
<th>Throttling</th>
</tr>
</thead>
<tbody><tr>
<td>접근 방식</td>
<td>제한 초과 시 <strong>차단</strong></td>
<td>제한 초과 시 <strong>지연</strong></td>
</tr>
<tr>
<td>용도</td>
<td><strong>남용 방지</strong></td>
<td><strong>일시적 부하 조절</strong></td>
</tr>
<tr>
<td>사용자 경험</td>
<td>429 오류 반환</td>
<td>대기 후 처리됨</td>
</tr>
</tbody></table>
<hr>
<h2 id="✅-왜-중요한가">✅ 왜 중요한가?</h2>
<h3 id="✔-남용-및-공격-방지">✔ 남용 및 공격 방지</h3>
<ul>
<li>LinkedIn: 로그인 시도 시간당 5회 제한 → 무차별 대입 차단</li>
<li>Cloudflare: 2023년 3분기에 1,280만 DDoS 차단</li>
</ul>
<h3 id="✔-공정한-사용-보장">✔ 공정한 사용 보장</h3>
<ul>
<li>Zoom: 일반 100만 건/월, 기업 1천만 건/월 API 요청 허용</li>
<li>AWS Lambda: 요청당 과금 → 무제한 사용 시 요금 폭탄 가능</li>
</ul>
<h3 id="✔-sla-및-규정-준수">✔ SLA 및 규정 준수</h3>
<ul>
<li>Shopify: 분당 100건으로 SLA 99.99% 보장</li>
</ul>
<hr>
<h2 id="✅-rate-limiting-전략">✅ Rate Limiting 전략</h2>
<table>
<thead>
<tr>
<th>종류</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Key 기반</strong></td>
<td>API Key로 제한</td>
<td>Stripe: 초당 100건</td>
</tr>
<tr>
<td><strong>IP 기반</strong></td>
<td>IP당 요청 제한</td>
<td>GitHub: 인증 없는 IP는 시간당 60건 제한</td>
</tr>
<tr>
<td><strong>사용자 기반</strong></td>
<td>유저 역할별 차등</td>
<td>HubSpot: 무료 100건/시, 유료 10,000건/시</td>
</tr>
<tr>
<td><strong>동시 연결 제한</strong></td>
<td>연결 수 제한</td>
<td>AWS RDS: 동시 4만 커넥션 제한</td>
</tr>
</tbody></table>
<hr>
<h2 id="✅-알고리즘-예시">✅ 알고리즘 예시</h2>
<h3 id="1-token-bucket">1. Token Bucket</h3>
<ul>
<li><strong>일정 수의 토큰을 소비하며 요청 처리</strong></li>
<li>토큰은 일정 속도로 리필됨</li>
<li>예: Cloudflare → 트래픽 급증에도 일정 처리</li>
</ul>
<h3 id="2-leaky-bucket">2. Leaky Bucket</h3>
<ul>
<li><strong>고정 속도로 요청 처리</strong>, 넘치면 폐기</li>
<li>예: RabbitMQ → 큐 폭주 방지</li>
</ul>
<hr>
<h2 id="✅-구현-시-모범-사례">✅ 구현 시 모범 사례</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>권장 사항</th>
</tr>
</thead>
<tbody><tr>
<td><strong>현실적인 제한 설정</strong></td>
<td>Netflix: 트래픽 시뮬레이션으로 최적값 설정</td>
</tr>
<tr>
<td><strong>명확한 커뮤니케이션</strong></td>
<td><code>X-RateLimit-Limit</code>, <code>Retry-After</code> 헤더 반환</td>
</tr>
<tr>
<td><strong>지속적인 모니터링</strong></td>
<td>Prometheus, Datadog 등으로 트래픽 추적</td>
</tr>
<tr>
<td><strong>우아한 오류 메시지</strong></td>
<td>❌ 단순 에러 → ✅ <code>60초 후 재시도하세요</code> 등 안내 제공</td>
</tr>
</tbody></table>
<hr>
<h2 id="✅-실제-사례">✅ 실제 사례</h2>
<table>
<thead>
<tr>
<th>서비스</th>
<th>제한 방식</th>
</tr>
</thead>
<tbody><tr>
<td>Google Maps</td>
<td>프로젝트당 하루 10만 건 지오코딩 제한</td>
</tr>
<tr>
<td>GitHub</td>
<td>인증 없음: 시간당 60건, 인증됨: 5천 건</td>
</tr>
<tr>
<td>Outline.com</td>
<td>PDF 변환 API: 분당 5건 제한 (GPU 부담 때문)</td>
</tr>
<tr>
<td>Walmart</td>
<td>초당 2건 제한 → 가격 스크래핑 방지 목적</td>
</tr>
</tbody></table>
<hr>
<h2 id="✅-사용-가능한-도구">✅ 사용 가능한 도구</h2>
<h3 id="api-gateway">API Gateway</h3>
<table>
<thead>
<tr>
<th>도구</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>AWS API Gateway</td>
<td>토큰 버킷 방식, Lambda 인증자 연동</td>
</tr>
<tr>
<td>Azure API Management</td>
<td>정책 기반 제한</td>
</tr>
<tr>
<td>Kong</td>
<td>플러그인으로 IP 기반 제어</td>
</tr>
</tbody></table>
<h3 id="오픈소스--클라우드">오픈소스 &amp; 클라우드</h3>
<table>
<thead>
<tr>
<th>종류</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>API7 Cloud</td>
<td>모든 클라우드 통합 관리</td>
</tr>
<tr>
<td>Ambassador</td>
<td>쿠버네이티브, JWT 기반 제어</td>
</tr>
<tr>
<td>Apache APISIX</td>
<td>Lua 플러그인 기반 제어 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="✅-미래-동향">✅ 미래 동향</h2>
<ul>
<li><strong>AI 기반 Throttling</strong>: Azure는 ML로 이상 트래픽 탐지</li>
<li><strong>예측 기반 자동 조절</strong>: Google Cloud AutoML로 트래픽 예측</li>
<li><strong>표준화</strong>: OpenAPI 문서에 <code>x-rate-limit</code> 같은 확장 필드 사용 증가</li>
<li><strong>서버리스 연동</strong>: AWS Lambda는 동시 실행 제한을 통해 자동 Throttling 가능</li>
</ul>
<hr>
<h2 id="✅-결론">✅ 결론</h2>
<p>Rate Limiting과 Throttling은 <strong>API 안정성과 보안을 위한 핵심 전략</strong>입니다.
올바른 알고리즘 선택, 도구 활용, 정책 설계로 <strong>서비스 신뢰성 확보 및 비용 절감</strong>이 가능합니다.
<strong>API 중심 아키텍처가 증가하는 시대에선 필수적인 요소</strong>입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[앱(Fillsa) 출시를 회고하며..]]></title>
            <link>https://velog.io/@dayoung_sarah/%EC%95%B1Fillsa-%EC%B6%9C%EC%8B%9C%EB%A5%BC-%ED%9A%8C%EA%B3%A0%ED%95%98%EB%A9%B0</link>
            <guid>https://velog.io/@dayoung_sarah/%EC%95%B1Fillsa-%EC%B6%9C%EC%8B%9C%EB%A5%BC-%ED%9A%8C%EA%B3%A0%ED%95%98%EB%A9%B0</guid>
            <pubDate>Wed, 02 Jul 2025 08:24:12 GMT</pubDate>
            <description><![CDATA[<h1 id="드디어-앱-출시">드디어 앱 출시!</h1>
<p>프로젝트를 시작한 건 4월.
그리고 드디어! 두달만에 앱을 출시하게 되었습니다!!!!!
중간에 우여곡절도 많았지만, 하나씩 해결해가며 막판 스퍼트를 올린 덕분에 이렇게 출시까지 완주할 수 있었습니다.
<img src="https://velog.velcdn.com/images/dayoung_sarah/post/1ad0d5df-2dd7-4b33-9d0d-cac1cf425d3d/image.JPG" width="300px"></p>
<hr>
<h2 id="시작은-민음사-일력에서">시작은 &#39;민음사 일력&#39;에서</h2>
<p>처음 앱을 기획하게 된 계기는 <strong>&#39;민음사 일력&#39;</strong>이었습니다.
작년 말, 민음사 책을 구매하면서 ‘2025 민음사 일력’을 사은품으로 받았고, 매일 좋은 문장을 보며 개인적으로 필사를 하기 시작했습니다.</p>
<p>그러던 중, 민음사에서 <strong>&#39;2025 세계문학 일력&#39;</strong>을 앱으로 출시한 사실을 알게 되었고, 거기서 제공하는 <strong>타이핑 기반의 필사 기능</strong>을 보며 이런 생각이 들었죠!</p>
<blockquote>
<p>&quot;아, 이렇게 소비자의 니즈를 앱으로 풀어낼 수 있구나.&quot;</p>
</blockquote>
<p>그게 시작이었습니다. 뚜둥-
<img src="https://velog.velcdn.com/images/dayoung_sarah/post/5f01567d-f443-411f-a096-88ab71fad49e/image.png" alt="2025 세계문학 일력"></p>
<hr>
<h2 id="시장조사와-차별화-아이디어">시장조사와 차별화 아이디어</h2>
<p>&#39;나도 앱을 만들어볼까?&#39;란 생각이 들었고, 같은 주제인 <strong>‘필사’</strong>로 시장 조사를 시작했습니다.
이미 많은 필사 앱들이 존재하고 있었지만, 대부분 <strong>‘타이핑’</strong> 방식이었습니다.
그런데 MZ가 아닌? 제게 ‘필사’란 <strong>직접 손으로 쓰는 것</strong>이란 생각이였죠.</p>
<p>그래서 이런 아이디어가 떠올랐습니다..!</p>
<blockquote>
<p>“타이핑 기능 플러스 추가적으로 손으로 직접 필사한 내용을 <strong>사진으로 업로드</strong>하는 기능을 넣으면, 더 다양한 사용자층에게 어필할 수 있지 않을까?”</p>
</blockquote>
<p>이 아이디어를 중심으로 빠르게 목업 화면과 기획서를 만들고, 팀 구성을 시작했습니다.</p>
<hr>
<h2 id="앱-이름-그-고민의-끝">앱 이름, 그 고민의 끝</h2>
<p>앱 이름을 정하는 것도 쉽지 않았다죠..
한참을 고민하다가 결국 단순하게 <strong>‘필사’</strong>로 결정했습니다.
검색하면 무조건 걸리도록! (개발자스러운 발상 😅)</p>
<p>그래도 감성을 담고 싶어서 <strong>부제로 ‘Fillsa’</strong>를 붙였습니다.
느낌적인 느낌은... 낼 수 있었던 것 같습니다....</p>
<p>정확한 앱 명칭은 &#39;<strong>필사 - 하루 한 문장, 나만의 기록</strong>&#39; 입니다!
<img src="https://velog.velcdn.com/images/dayoung_sarah/post/3890bc24-a7a2-426c-979d-091453ef7c66/image.png" width="600px"></p>
<hr>
<h2 id="서버-aws-처음부터-끝까지-직접-구성">서버: AWS 처음부터 끝까지 직접 구성</h2>
<p>이번 기회에 <strong>AWS 프리티어를 이용해 처음부터 끝까지 직접 서버를 구성</strong>해보았습니다.
책에서만 보던 AWS의 다양한 기능을 실전에 적용해보며 많은 걸 배웠죠.</p>
<p>무엇보다 운영을 고려하다 보니 <strong>비용</strong>을 가장 신경 썼고,
현재는 고정 IP 비용 정도만 나오며 <strong>월 약 $10 수준</strong>으로 유지되고 있습니다.
(프리티어가 끝나면 비용을 고려해 다른 인프라로 이전할 계획도 있음)
<img src="https://velog.velcdn.com/images/dayoung_sarah/post/94192e37-dfca-4945-8127-12ee07624b28/image.png" alt=""></p>
<hr>
<h2 id="백엔드">백엔드</h2>
<p>빠른 출시를 목표로 했기에, 백엔드는 기존에 사용하던 스택을 그대로 사용했습니다.</p>
<ul>
<li>Kotlin</li>
<li>Spring Boot 3</li>
<li>MySQL</li>
<li>JPA
등등</li>
</ul>
<p>PM 역할을 병행하다 보니 백엔드에 집중을 많이 하진 못한것 같습니다..
다만!! 이후 버전부터 업데이트를 통해 하나씩 디벨롭해나갈 예정입니다.
재밌겠다<del>~</del>(눈물 스윽)</p>
<blockquote>
<p>조심스레.. <a href="https://github.com/LEE-DAYOUNG-SARAH/fillsa_api">GitHub 저장소 링크도 남깁니다</a></p>
</blockquote>
<hr>
<h2 id="pm-업무를-하며-느낀-점">PM 업무를 하며 느낀 점</h2>
<p>처음엔 목업을 기반으로 디자이너님과 UI 흐름을 맞췄고,
기획서를 나름 꼼꼼히 썼다고 생각했지만…
막상 진행하다 보니 빠진 부분이 계속 생겼습니다.</p>
<p>그래서 <strong>플로우차트도 그리고, 앱 흐름을 다시 공유하면서 정리</strong>해갔습니다.
<img src="https://velog.velcdn.com/images/dayoung_sarah/post/0c78c8be-7f70-456e-82bb-cea2e5ad284e/image.png" alt=""></p>
<p>이 과정에서 <strong>기획자 분들에 대한 리스펙</strong>이 생겼고,
&quot;협업을 잘하는 개발자란 어떤 사람인가?&quot;란 질문에 대한 답을 나름대로 정의내렸습니다..</p>
<blockquote>
<p><strong>협업을 잘하는 개발자란?</strong></p>
<ul>
<li>NO보다 긍정적 검토로 결과를 답할 수 있는 개발자</li>
<li>새로운 기술과 기능에 대한 거부감이 없는 개발자</li>
<li>사용자의 입장과 사업적인 요소를 고려하여 기획적인 부분을 제안할 수 있는 개발자.</li>
<li><strong>무엇보다 알잘딱깔쎈(업무공유, 일정공유 항시 하는. 말하지 않아도 결과물을 내오는..)인 개발자</strong></li>
</ul>
</blockquote>
<hr>
<h2 id="출시를-가로막은-뜻밖의-장벽-비공개-테스트">출시를 가로막은 뜻밖의 장벽: 비공개 테스트</h2>
<p>앱 출시를 위해선 <strong>비공개 테스트</strong>를 반드시 거쳐야 합니다.
그런데 조건이 생각보다 까다로웠습니다.</p>
<blockquote>
<p><strong>“2주 동안 12명 이상의 테스터가 매일 앱을 사용해야 한다.”</strong></p>
</blockquote>
<p>처음엔 <a href="https://cafe.naver.com/devsharing">네이버 카페</a>를 통해 자발적 참여자를 모집해보려 했지만,
매일 사용해줄 수 있을지 불확실해서 <strong><a href="https://kmong.com/gig/638898">크몽</a>을 활용</strong>해보았습니다.</p>
<p>크몽만 믿고 시간을 보내던 어느날 뚜둥!!!
아래와 같은 구글 개발자 콘솔의 오류가 발견되었습니다.</p>
<ul>
<li>시계열 데이터가 이상하게 찍힘</li>
<li>사용자 수가 왔다 갔다 함 (1명 → 10명 → 0명)
<img src="https://velog.velcdn.com/images/dayoung_sarah/post/980c0b98-c03c-4de5-bf59-ce79a81d6914/image.png" alt=""></li>
</ul>
<p>크몽 작업자분이 <strong>최근에도 이런 이슈를 겪은 사례와 무사 통과한 예시</strong>를 공유해주셔서 조금은 안심되었습니다..
(하지만 계속 불안에 떨긴함..)</p>
<p>그리고 드디어!
<strong>프로덕션 심사를 넣고 12시간도 안 돼서 심사 통과 메일 도착!</strong> 😂</p>
<hr>
<h2 id="이제-다시-시작해보자">이제 다시 시작해보자~~</h2>
<blockquote>
<p>&quot;출시는 끝이 아니라 시작이다.&quot; - 개발자 리다영 -</p>
</blockquote>
<p>바로 <strong>version 2 개발에 착수</strong>할 예정이고, 지속적인 QA를 통해 앱 품질을 향상시켜 나가려 합니다!</p>
<p>무엇보다,
<strong>저를 믿고 함께 해준 팀원들에게 무한한 감사를 전합니다.</strong>
계속 함께 달려나괍시돠<del>~</del>
<img src="https://velog.velcdn.com/images/dayoung_sarah/post/6662c686-c1f3-4545-8955-a019908be1b6/image.JPG" width="200px" ></p>
<hr>
<h2 id="마지막으로">마지막으로…</h2>
<p>이 글을 보고 있는 당신..
한 번만 사용해주실 수 있을까요…? (네, 구걸 맞습니다…)</p>
<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/1b8a6d64-1018-4515-8871-71d9bf1593b3/image.png" alt=""></p>
<p><a href="https://play.google.com/store/apps/details?id=com.arakene.fillsa">구글 플레이 스토어</a>
<a href="https://home.fillsa.store">앱 소개 홈페이지</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS로 웹 탈퇴 페이지 빠르게 만들기 (S3 + CloudFront + Route53 활용기)]]></title>
            <link>https://velog.io/@dayoung_sarah/AWS%EB%A1%9C-%EC%9B%B9-%ED%83%88%ED%87%B4-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0-S3-CloudFront-Route53-%ED%99%9C%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@dayoung_sarah/AWS%EB%A1%9C-%EC%9B%B9-%ED%83%88%ED%87%B4-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0-S3-CloudFront-Route53-%ED%99%9C%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Sun, 01 Jun 2025 06:30:52 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>&quot;서버 없이도 빠르게 웹페이지 배포하는 방법, 그리고 구글/카카오 간편 로그인 탈퇴 구현 후기까지!&quot;</p>
</blockquote>
<hr>
<p>앱 출시 심사 준비 중, 갑자기 “웹 탈퇴 페이지가 별도로 필요하다”는 게 떠오름.
 예전에 회사에서도 비슷한 이슈로 프론트엔드 팀이 급하게 처리하던 기억이 있었는데, 이번엔 그 역할을 내가 하게 됨..</p>
<p>프론트 프로젝트야 React + Vite로 빠르게 만들 수 있지만, 문제는 배포. 서버 띄워야 하나? SSL 인증서는 또 어떻게 하지...? 라는 고민이 시작됨.</p>
<p>그래서 찾아보니 아래의 방법으로 하면 해결 완.</p>
<blockquote>
<p>✅ 정적 웹사이트는 <strong>S3</strong>
✅ SSL 인증은 <strong>CloudFront + ACM</strong>
✅ 도메인 연결은 <strong>Route 53</strong></p>
</blockquote>
<p>덕분에 별도 서버 없이도, 클라이언트만 있는 웹사이트를 바로 띄울 수 있었음.
AWS에 한 걸음 더 가까워졌달까? 😅</p>
<hr>
<h2 id="🔧-구성도-요약">🔧 구성도 요약</h2>
<table>
<thead>
<tr>
<th>역할</th>
<th>사용 기술</th>
</tr>
</thead>
<tbody><tr>
<td>프론트엔드 앱</td>
<td>React + TypeScript + Vite</td>
</tr>
<tr>
<td>정적 파일 호스팅</td>
<td>S3 Static Website Hosting</td>
</tr>
<tr>
<td>HTTPS + CDN</td>
<td>CloudFront + ACM (SSL 인증서)</td>
</tr>
<tr>
<td>커스텀 도메인 연결</td>
<td>Route 53</td>
</tr>
</tbody></table>
<hr>
<h2 id="1️⃣-웹-탈퇴-페이지-프론트-개발">1️⃣ 웹 탈퇴 페이지 프론트 개발</h2>
<p>구현한 기능:</p>
<ol>
<li><strong>간편 로그인 연동 (카카오 / 구글)</strong></li>
<li><strong>백엔드에 탈퇴 API 호출</strong>
 a. access token 발급
 b. user info 조회 -&gt; oauth id 확인
 c. 필사 탈퇴처리 로직</li>
</ol>
<h3 id="✔-카카오-unlink-처리">✔ 카카오 unlink 처리</h3>
<p>카카오는 JavaScript SDK에서 <code>Kakao.API.request({ url: &#39;/v1/user/unlink&#39; })</code>를 호출하면 쉽게 연결을 끊을 수 있음. 그리고 해당 oauth ID로 탈퇴 요청 보내면 끝.</p>
<h3 id="✔-구글은-다소-복잡">✔ 구글은 다소 복잡...</h3>
<p>반면 구글은 <code>https://oauth2.googleapis.com/revoke</code> API를 호출해서 발급된 토큰을 무효화할 수 있지만, <strong>계정 연결 해제는 사용자가 직접 해야 함.</strong> 😅</p>
<p>그래서 앱 약관에 소셜계정 연결해제는 직접 해야한다는 문구만 남기고, 실제 연결 끊는 기능은 제외함. 대부분의 앱도 그렇게 하고 있었음.</p>
<hr>
<h2 id="2️⃣-aws-인프라-구성-및-배포">2️⃣ AWS 인프라 구성 및 배포</h2>
<h3 id="📁-s3-정적-웹사이트-설정">📁 S3 정적 웹사이트 설정</h3>
<ol>
<li>S3 버킷을 생성하고</li>
<li>&quot;정적 웹사이트 호스팅&quot; 옵션 활성화</li>
<li>기본 문서를 <code>index.html</code>로 지정</li>
</ol>
<h3 id="🛠-프론트-빌드-및-업로드">🛠 프론트 빌드 및 업로드</h3>
<pre><code class="language-bash">npm run build  # vite 기준 빌드
aws s3 sync dist/ s3://${탈퇴 도메인}</code></pre>
<p align="center">
  <img src="https://velog.velcdn.com/images/dayoung_sarah/post/6f219851-f2d0-424d-af1c-0e544de5dff1/image.png" width="300" />
  (디자인 나오기 전 기능 테스트용 화면...개발자의 미감이란..)
</p>

<h3 id="🌍-cloudfront-연결">🌍 CloudFront 연결</h3>
<ol>
<li>원본에 S3 버킷 연결</li>
<li>Viewer Protocol Policy는 <code>Redirect HTTP to HTTPS</code></li>
<li>ACM에서 인증서 발급받고 연결</li>
<li>Route 53에서 도메인에 A 레코드 추가 (CloudFront 배포 도메인 alias)</li>
</ol>
<hr>
<h2 id="3️⃣-앱-소개-페이지는-slash-page로">3️⃣ 앱 소개 페이지는 Slash Page로</h2>
<p>약관/소개 페이지는 Notion으로 할까 고민하다가 <strong><a href="https://slashpage.com/slashpage-kr">Slash Page</a></strong>라는 서비스를 알게 됨. 완전 신세계.</p>
<ul>
<li>커스텀 도메인 연결 가능 (베타)</li>
<li>에디터가 노션처럼 편리함</li>
<li>디자인도 깔끔하게 뽑힘</li>
</ul>
<p>그 외 아래와 같은 다양한 베타테스터 기능들이 있다.
잘 활용하면 별도의 사이트 구축없이 해당 서비스를 이용하면 될듯?</p>
<p align="center">
  <img src="https://velog.velcdn.com/images/dayoung_sarah/post/d9846202-78a7-423c-adb3-320e83b66903/image.png" width="500"/>
 </p>

<hr>
<h2 id="🪧-필사-앱도-곧-출시합니다">🪧 필사 앱도 곧 출시합니다!</h2>
<p align="center">
<img src="https://velog.velcdn.com/images/dayoung_sarah/post/80f071e3-1fbf-49ea-a911-6b9b94316772/image.png" />
 </p>

<p>현재 준비 중인 앱 소개는 <a href="https://home.fillsa.store/">home.fillsa.store</a>에서 보실 수 있어요.
이 페이지는 Slash Page를 활용해 만들었고, 아직 구축 중이지만 곧 완성될 예정입니다.</p>
<blockquote>
<p><strong>6월 중순 출시 예정이니 많관부 🙌</strong></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] OAuth2 간편 로그인·탈퇴 추상화]]></title>
            <link>https://velog.io/@dayoung_sarah/Spring-Boot-OAuth2-%EA%B0%84%ED%8E%B8-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%83%88%ED%87%B4-%EC%B6%94%EC%83%81%ED%99%94</link>
            <guid>https://velog.io/@dayoung_sarah/Spring-Boot-OAuth2-%EA%B0%84%ED%8E%B8-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%83%88%ED%87%B4-%EC%B6%94%EC%83%81%ED%99%94</guid>
            <pubDate>Mon, 28 Apr 2025 04:12:52 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>간편 로그인(OAuth2)을 카카오·구글로 연동하면서, 중복되는 코드를 줄이고 프로바이더별 로직을 분리함
특히 <strong>탈퇴(연동 해제)</strong> 과정이 카카오와 구글이 달라서 이를 어떻게 추상화할지 고민함</p>
</blockquote>
<hr>
<h2 id="1-간편-로그인-흐름">1. 간편 로그인 흐름</h2>
<p>(자세한 내용은 <a href="https://velog.io/@dayoung_sarah/Spring-Boot-OAuth2-JWT-Redis-%EA%B8%B0%EB%B0%98-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84">여기</a> 참고)</p>
<ol>
<li><strong>사용자</strong>가 앱에서 “카카오/구글 로그인” 버튼 클릭  </li>
<li>OAuth 서버에서 인증 후 <code>code</code> 발급  </li>
<li>백엔드 <code>GET /oauth/{provider}/callback?code=…</code>로 <code>code</code> 수신  </li>
<li>백엔드에서  <ul>
<li><code>code</code> → oauth 액세스 토큰 발급  </li>
<li>oauth 사용자 정보 가져오기  </li>
<li>oauth token DB 에 저장 </li>
</ul>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/46d25647-135d-4df5-ba19-02f61986edb5/image.png" alt="OAuth2 Flow"></p>
<hr>
<h2 id="2-로그인-연동-strategy-패턴으로-공통-로직-묶기">2. 로그인 연동: Strategy 패턴으로 공통 로직 묶기</h2>
<h3 id="oauthloginclient-인터페이스">OAuthLoginClient 인터페이스</h3>
<pre><code class="language-kotlin">interface OAuthLoginClient {
  fun getOAuthProvider(): Member.OAuthProvider
  fun getAccessToken(code: String): OAuthTokenInfo
  fun getUserInfo(accessToken: String): OAuthUserInfo
}</code></pre>
<ul>
<li>카카오·구글용 WebClient 구현체가 이 인터페이스를 따릅니다.  </li>
<li>새 프로바이더를 추가할 때는 <strong>이 인터페이스를 구현한 클래스 추가</strong></li>
</ul>
<pre><code class="language-kotlin">@Service class GoogleOAuthLoginWebClient : OAuthLoginClient { … }
@Service class KakaoOAuthLoginWebClient  : OAuthLoginClient { … }</code></pre>
<h3 id="oauthcallbackservice-분기-없이-똑같은-처리">OAuthCallbackService: 분기 없이 똑같은 처리</h3>
<ul>
<li>로그인 플로우는 카카오/구글 동일함</li>
<li>oauth token을 DB 에 저장하는 이유 -&gt; google 간편로그인 해제 시 이때 발급받은 refresh token이 필요함</li>
</ul>
<pre><code class="language-kotlin">@Service
class OAuthCallbackService(
    private val memberUseCase: MemberUseCase,
    private val tokenUseCase: OAuthTokenUseCase,
    private val clients: List&lt;OAuthLoginClient&gt;
): OAuthCallbackUseCase {

    override fun processOAuthCallback(provider: Member.OAuthProvider, code: String): Long {
        // 1) 클라이언트 조회
        val client = clients
            .firstOrNull { it.getOAuthProvider() == provider }
            ?: throw InvalidRequestException(&quot;지원하지 않는 provider: $provider&quot;)

        // 2) 코드로 토큰 발급
        val oauthTokenInfo = client.getAccessToken(code)

        // 3) 토큰으로 사용자 정보 조회
        val userInfo = client.getUserInfo(oauthTokenInfo.accessToken)

        // 4) DB에 회원 생성 또는 조회
        val member = memberUseCase.processOauthLogin(userInfo)

        // 5) 발급받은 토큰 DB에 저장
        tokenUseCase.createOAuthToken(member, oauthTokenInfo)

        return member.memberSeq
    }
}</code></pre>
<hr>
<h2 id="3-콜백-컨트롤러">3. 콜백 컨트롤러</h2>
<pre><code class="language-kotlin">@RestController
@RequestMapping(&quot;/oauth&quot;)
class OAuthCallbackController(
    private val callbackService: OAuthCallbackService,
    ...
) {
    @GetMapping(&quot;/{provider}/callback&quot;)
    fun callback(
        @PathVariable provider: String,
        @RequestParam code: String,
        response: HttpServletResponse
    ) {
        val memberSeq = oAuthCallbackService.processOAuthCallback(
            provider = Member.OAuthProvider.fromPath(provider),
            code = code
        )
            ...
    }
}</code></pre>
<hr>
<h2 id="4-탈퇴연동-해제-처리-프로바이더별로-다른-로직">4. 탈퇴(연동 해제) 처리: 프로바이더별로 다른 로직</h2>
<p>“탈퇴”라고 해도, 내부 회원 삭제와는 별개로 <strong>외부 OAuth 제공자에 연동 해제</strong>를 먼저 해야 함.
카카오와 구글이 API 방식이 달라서 이를 중점적으로 서술해봄</p>
<h3 id="41-공통-인터페이스">4.1 공통 인터페이스</h3>
<pre><code class="language-kotlin">interface OAuthWithdrawalUseCase {
  fun getOAuthProvider(): Member.OAuthProvider
  fun withdraw(member: Member)
}</code></pre>
<h3 id="42-서비스">4.2 서비스</h3>
<pre><code class="language-kotlin">@Service
class OAuthWithdrawalService(
    private val cases: List&lt;OAuthWithdrawalUseCase&gt;
) {
    fun withdraw(member: Member) {
        val useCase = cases
            .firstOrNull { it.getOAuthProvider() == member.oauthProvider }
            ?: error(&quot;지원하지 않는 provider: ${member.oauthProvider}&quot;)
        useCase.withdraw(member)
    }
}</code></pre>
<h3 id="43-카카오-vs-구글-플로우">4.3 카카오 vs. 구글 플로우</h3>
<table>
<thead>
<tr>
<th>제공자</th>
<th>처리 방식</th>
</tr>
</thead>
<tbody><tr>
<td><strong>카카오</strong></td>
<td><code>POST /v1/user/unlink</code> → Admin Key만 있으면 OK</td>
</tr>
<tr>
<td><strong>구글</strong></td>
<td>1) DB에서 refreshToken 조회<br>2) refreshToken 유효 확인<br>3) oauth 토큰 재발급 → accessToken 얻기<br>4) <code>POST /revoke?token=…</code> 호출</td>
</tr>
</tbody></table>
<pre><code class="language-kotlin">@Service
class KakaoOAuthWithdrawalService(
    private val kakaoClient: KakaoOAuthWithdrawalClient
): OAuthWithdrawalUseCase {
    override fun getOAuthProvider() = Member.OAuthProvider.KAKAO
    override fun withdraw(member: Member) {
        // oauth 탈퇴
        kakaoClient.withdraw(member.oauthId)
    }
}

@Service
class GoogleOAuthWithdrawalService(
    private val googleClient: GoogleOAuthWithdrawalClient,
    private val repo: OAuthTokenRepository
): OAuthWithdrawalUseCase {
    override fun getOAuthProvider() = Member.OAuthProvider.GOOGLE

    override fun withdraw(member: Member) {
        // DB의 oauth 토큰 조회
        val token = repo.findLatest(member)
            ?: throw OAuthWithdrawalException(&quot;OAuth 정보 없음&quot;)

        // refresh token 유효한지 확인    
        if (token.refreshTokenExpiresAt &lt;= LocalDateTime.now()) {
            throw OAuthWithdrawalException(&quot;만료된 refresh token, 재로그인 필요&quot;)
        }

        // oauth access Token 재발급
        val accessToken = googleClient.getAccessToken(token.refreshToken)

        // oauth 탈퇴
        googleClient.withdraw(accessToken)
    }
}</code></pre>
<h3 id="44-컨트롤러·서비스-연결">4.4 컨트롤러·서비스 연결</h3>
<pre><code class="language-kotlin">@RestController
@RequestMapping(&quot;/auth&quot;)
class AuthController(private val authUseCase: AuthUseCase) {
    @DeleteMapping(&quot;/withdraw&quot;)
    fun withdraw(
        @AuthenticationPrincipal member: Member,
        @RequestBody request: WithdrawalRequest
    ) {
        authUseCase.withdraw(member, request)
    }
}

@Service
class AuthService(
    private val oauthWithdrawalService: OAuthWithdrawalService,
    private val memberUseCase: MemberUseCase,
    private val redisTokenUseCase: RedisTokenUseCase
): AuthUseCase {
    override fun withdraw(member: Member, request: WithdrawalRequest) {
        // 1) 외부 연동 해제
        oauthWithdrawalService.withdraw(member)

        // 2) 내부 회원 탈퇴
        memberUseCase.withdraw(member)

        // 3) Redis에 저장된 refresh token 삭제
        redisTokenUseCase.deleteRefreshToken(member.memberSeq, request.deviceId)
    }
}</code></pre>
<hr>
<h2 id="5-마무리">5. 마무리</h2>
<ul>
<li>공통 부분을 추상화해서 로직을 하나로 합치고, 유지보수가 용이하도록 구현</li>
<li><strong>추상 인터페이스 + 구현체 리스트</strong>로 분기처리 하도록 함</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Boot] OAuth2 + JWT + Redis 기반 로그인 구현]]></title>
            <link>https://velog.io/@dayoung_sarah/Spring-Boot-OAuth2-JWT-Redis-%EA%B8%B0%EB%B0%98-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@dayoung_sarah/Spring-Boot-OAuth2-JWT-Redis-%EA%B8%B0%EB%B0%98-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Mon, 28 Apr 2025 01:55:08 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>OAuth 인증 후 모바일 앱에서 안전하게 사용자 식별 정보를 받아 JWT로 최종 로그인 처리</strong></p>
<ul>
<li>OAuth 인증이 끝나면 백엔드가 임시 토큰(UUID)을 발급해 Redis에 저장</li>
<li>앱은 딥링크를 통해 임시 토큰만 전달받아, 민감한 인증 코드를 직접 다루지 않음</li>
<li>임시 토큰을 검증한 뒤에야 실제 JWT Access/Refresh 토큰을 발급</li>
</ul>
</blockquote>
<h2 id="로그인-플로우">로그인 플로우</h2>
<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/285045e9-5bb4-4f03-b35e-d8ba2ae90b66/image.png" alt=""></p>
<h3 id="1-앱-→-브라우저-oauth-인증-요청">1. 앱 → 브라우저 (OAuth 인증 요청)</h3>
<ul>
<li>앱에서 구글/카카오 간편 로그인을 제공  </li>
<li>로그인 버튼 클릭 시 웹뷰로 동의 화면 이동  </li>
</ul>
<h3 id="2-브라우저-→-oauth-인증서버">2. 브라우저 → OAuth 인증서버</h3>
<ul>
<li>사용자가 아이디/비밀번호 입력 및 권한 동의  </li>
<li>완료되면 OAuth 서버가 <code>redirect_uri</code>(백엔드)로 인가 코드(<code>code</code>) 전달  </li>
</ul>
<h3 id="3-oauth-서버-→-백엔드-callback">3. OAuth 서버 → 백엔드 (Callback)</h3>
<ul>
<li>백엔드에서 <code>/oauth/{provider}/callback?code={authorization_code}</code> 호출  </li>
<li>프로바이더별 로그인 로직은 <code>OAuthCallbackService</code>에서 추상화  </li>
</ul>
<h3 id="4-백엔드-임시-토큰-생성·저장">4. 백엔드: 임시 토큰 생성·저장</h3>
<ol>
<li>받은 <code>code</code>로 OAuth 서버에 토큰 교환(Access/Refresh 토큰 발급)  </li>
<li>사용자 정보 조회 후, 회원 가입/로그인 처리  </li>
<li>UUID 기반 임시 토큰(<code>tempToken</code>) 생성  </li>
<li>Redis에 아래 형태로 저장<pre><code class="language-text">key   = temp_token:{UUID}
value = memberSeq
TTL   = 5분</code></pre>
</li>
</ol>
<h3 id="5-백엔드-→-앱-임시-토큰-전달">5. 백엔드 → 앱: 임시 토큰 전달</h3>
<ul>
<li><code>response.sendRedirect(&quot;{딥링크}?temp_token={UUID}&quot;)</code>  </li>
<li>앱은 딥링크를 받아 내부 로직으로 전달  </li>
</ul>
<h3 id="6-앱-→-백엔드-임시-토큰으로-최종-로그인-요청">6. 앱 → 백엔드: 임시 토큰으로 최종 로그인 요청</h3>
<pre><code class="language-json">// 요청 예시
{
&quot;tempToken&quot;: &quot;{UUID}&quot;,
&quot;deviceId&quot;: &quot;디바이스식별자&quot;
}</code></pre>
<ul>
<li>deviceId를 함께 보내면 기기별 토큰 관리가 가능</li>
</ul>
<h3 id="7-백엔드-임시-토큰-검증-→-jwt-발급">7. 백엔드: 임시 토큰 검증 → JWT 발급</h3>
<ol>
<li>Redis에서 <code>getAndDelete(&quot;temp_token:{UUID}&quot;)</code> 호출  </li>
<li>값이 존재하면 <code>memberSeq</code> 획득, 없으면 에러(만료 또는 중복 사용)  </li>
<li><code>JwtTokenProvider.createTokens(memberSeq)</code>로 Access/Refresh JWT 생성  </li>
<li>Redis에 아래 형태로 저장<pre><code class="language-text">key   = refresh:{memberSeq}:{deviceId}
value = refreshToken
TTL   = (config에 설정된 기간)</code></pre>
</li>
<li>응답으로 토큰 및 회원정보 전송</li>
<li>앱은 받은 JWT를 로컬에 저장하고, 보호된 화면으로 이동하여 로그인 완료</li>
</ol>
<hr>
<h2 id="왜-oauth-콜백에서-바로-jwt를-주지-않고-임시-토큰을-쓰는가">왜 OAuth 콜백에서 바로 JWT를 주지 않고 임시 토큰을 쓰는가?</h2>
<blockquote>
<p>OAuth 콜백 단계에서 JWT와 회원정보를 바로 내려주지 않고, <strong>임시 토큰(temp token)</strong> 만 발급해 앱에서 다시 호출하도록 만든 이유</p>
</blockquote>
<h3 id="1-브라우저-url-노출-방지">1. 브라우저 URL 노출 방지</h3>
<ul>
<li>JWT를 쿼리 파라미터나 리다이렉트 URL로 직접 전달하면  <ul>
<li>URL 히스토리에 남아 악의적 접근에 노출  </li>
<li>중간자 공격(MITM) 또는 XSS에 의한 탈취 위험 증가  </li>
</ul>
</li>
<li><strong>임시 토큰</strong>은  <ul>
<li>UUID 형태의 랜덤 값  </li>
<li>TTL(예: 5분)이 짧음  </li>
<li>Redis에만 저장 → 노출돼도 즉시 만료  </li>
</ul>
</li>
</ul>
<h3 id="2-앱↔백엔드-보안-채널-분리">2. 앱↔백엔드 보안 채널 분리</h3>
<ul>
<li>콜백: 브라우저/WebView 컨텍스트  </li>
<li>로그인 API: 네이티브 앱 컨텍스트에서 HTTPS 호출  </li>
<li>JWT를 콜백 URL에 담으면 딥링크 과정에서 또 노출 위험  </li>
<li><strong>해결책</strong>  <ol>
<li>콜백 단계에서는 임시 토큰만 전달  </li>
<li>앱에서 임시 토큰 + <code>deviceId</code>를 HTTPS POST로 전송 → 최종 JWT 발급  </li>
</ol>
</li>
<li>이로써 <strong>민감한 JWT 전송 구간</strong>을 앱↔백엔드 통신으로 한정, 보안을 강화  </li>
</ul>
<h3 id="3-디바이스-바인딩--토큰-수명-관리">3. 디바이스 바인딩 &amp; 토큰 수명 관리</h3>
<ul>
<li>Refresh Token은 <code>memberSeq + deviceId</code> 조합으로 Redis에 저장하여 특정 기기에서만 유효  </li>
<li>콜백 단계에선 <code>deviceId</code>가 없으므로  <ol>
<li>콜백 → 임시 토큰 발급 (기기 정보 없이)  </li>
<li>앱 → 임시 토큰 + <code>deviceId</code>로 로그인 요청 → “이 기기만 유효한 Refresh Token” 생성  </li>
</ol>
</li>
<li><strong>이중 단계</strong> 처리를 통해 기기별 토큰 관리를 깔끔하게 구현  </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[99클럽 코테 스터디 21일차 TIL] Pascal's Triangle]]></title>
            <link>https://velog.io/@dayoung_sarah/99%ED%81%B4%EB%9F%BD-%EC%BD%94%ED%85%8C-%EC%8A%A4%ED%84%B0%EB%94%94-21%EC%9D%BC%EC%B0%A8-TIL-Pascals-Triangle</link>
            <guid>https://velog.io/@dayoung_sarah/99%ED%81%B4%EB%9F%BD-%EC%BD%94%ED%85%8C-%EC%8A%A4%ED%84%B0%EB%94%94-21%EC%9D%BC%EC%B0%A8-TIL-Pascals-Triangle</guid>
            <pubDate>Mon, 12 Aug 2024 01:52:43 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/c63c75ee-3f28-46c7-a49d-b734b9c8f3f6/image.png" alt=""></p>
<h3 id="문제">문제</h3>
<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/0ff7a9a3-f792-458a-a1b3-00b2a54ed40f/image.png" alt=""></p>
<h3 id="답">답</h3>
<pre><code class="language-java">import java.util.ArrayList;
import java.util.List;

class Solution {
    public List&lt;List&lt;Integer&gt;&gt; generate(int numRows) {
        List&lt;List&lt;Integer&gt;&gt; triangle = new ArrayList&lt;&gt;();

        if (numRows == 0) {
            return triangle;
        }

        List&lt;Integer&gt; firstRow = new ArrayList&lt;&gt;();
        firstRow.add(1);
        triangle.add(firstRow);

        for (int i = 1; i &lt; numRows; i++) {
            List&lt;Integer&gt; prevRow = triangle.get(i - 1);
            List&lt;Integer&gt; newRow = new ArrayList&lt;&gt;();

            newRow.add(1);

            for (int j = 1; j &lt; i; j++) {
                newRow.add(prevRow.get(j - 1) + prevRow.get(j));
            }

            newRow.add(1);

            triangle.add(newRow);
        }

        return triangle;
    }
}</code></pre>
<h3 id="해결방안">해결방안</h3>
<p>numRows가 0인 경우 빈값, 1인 경우 1만 list 에 추가해서 반환
그 다음부터 for문으로 리스트를 만들어야 했다. 처음과 끝은 항상 1이 오도록 셋팅하고, 중간에 들어가는 값은 이중for문으로 만들어서 셋팅해주었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[99클럽 코테 스터디 20일차 TIL] Greedy]]></title>
            <link>https://velog.io/@dayoung_sarah/99%ED%81%B4%EB%9F%BD-%EC%BD%94%ED%85%8C-%EC%8A%A4%ED%84%B0%EB%94%94-18%EC%9D%BC%EC%B0%A8-TIL-Greedy</link>
            <guid>https://velog.io/@dayoung_sarah/99%ED%81%B4%EB%9F%BD-%EC%BD%94%ED%85%8C-%EC%8A%A4%ED%84%B0%EB%94%94-18%EC%9D%BC%EC%B0%A8-TIL-Greedy</guid>
            <pubDate>Sun, 11 Aug 2024 01:57:16 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/d62f9b9f-a9d7-4570-b03e-138ee1a7a927/image.png" alt=""></p>
<h3 id="문제">문제</h3>
<ul>
<li><strong>문제 설명</strong></li>
</ul>
<p>점심시간에 도둑이 들어, 일부 학생이 체육복을 도난당했습니다. 다행히 여벌 체육복이 있는 학생이 이들에게 체육복을 빌려주려 합니다. 학생들의 번호는 체격 순으로 매겨져 있어, 바로 앞번호의 학생이나 바로 뒷번호의 학생에게만 체육복을 빌려줄 수 있습니다. 예를 들어, 4번 학생은 3번 학생이나 5번 학생에게만 체육복을 빌려줄 수 있습니다. 체육복이 없으면 수업을 들을 수 없기 때문에 체육복을 적절히 빌려 최대한 많은 학생이 체육수업을 들어야 합니다.</p>
<p>전체 학생의 수 n, 체육복을 도난당한 학생들의 번호가 담긴 배열 lost, 여벌의 체육복을 가져온 학생들의 번호가 담긴 배열 reserve가 매개변수로 주어질 때, 체육수업을 들을 수 있는 학생의 최댓값을 return 하도록 solution 함수를 작성해주세요.</p>
<ul>
<li><strong>제한사항</strong></li>
</ul>
<p>전체 학생의 수는 2명 이상 30명 이하입니다.
체육복을 도난당한 학생의 수는 1명 이상 n명 이하이고 중복되는 번호는 없습니다.
여벌의 체육복을 가져온 학생의 수는 1명 이상 n명 이하이고 중복되는 번호는 없습니다.
여벌 체육복이 있는 학생만 다른 학생에게 체육복을 빌려줄 수 있습니다.
여벌 체육복을 가져온 학생이 체육복을 도난당했을 수 있습니다. 이때 이 학생은 체육복을 하나만 도난당했다고 가정하며, 남은 체육복이 하나이기에 다른 학생에게는 체육복을 빌려줄 수 없습니다.</p>
<ul>
<li><strong>입출력 예</strong></li>
</ul>
<table>
        <tbody>
            <tr>
                <td>5</td>
                <td>[2, 4]</td>
                <td>[1, 3, 5]</td>
                <td>5</td>
            </tr>
            <tr>
                <td>5</td>
                <td>[2, 4]</td>
                <td>[3]</td>
                <td>4</td>
            </tr>
            <tr>
                <td>3</td>
                <td>[3]</td>
                <td>[1]</td>
                <td>2</td>
            </tr>
        </tbody>
</table>


<ul>
<li><strong>입출력 예 설명</strong></li>
</ul>
<p>예제 #1
1번 학생이 2번 학생에게 체육복을 빌려주고, 3번 학생이나 5번 학생이 4번 학생에게 체육복을 빌려주면 학생 5명이 체육수업을 들을 수 있습니다.</p>
<p>예제 #2
3번 학생이 2번 학생이나 4번 학생에게 체육복을 빌려주면 학생 4명이 체육수업을 들을 수 있습니다.</p>
<h3 id="답">답</h3>
<pre><code class="language-java">import java.util.HashSet;
import java.util.Set;

class Solution {
    public int solution(int n, int[] lost, int[] reserve) {
        Set&lt;Integer&gt; lostSet = new HashSet&lt;&gt;();
        Set&lt;Integer&gt; reserveSet = new HashSet&lt;&gt;();

        for (int l : lost) {
            lostSet.add(l);
        }
        for (int r : reserve) {
            if (lostSet.contains(r)) {
                lostSet.remove(r); 
            } else {
                reserveSet.add(r);
            }
        }


        for (int r : reserveSet) {
            if (lostSet.contains(r - 1)) {
                lostSet.remove(r - 1); 
            } else if (lostSet.contains(r + 1)) {
                lostSet.remove(r + 1); 
            }
        }

        return n - lostSet.size();
    }
}</code></pre>
<h3 id="해결방안">해결방안</h3>
<p>HashSet을 사용해서 여벌을 빌려주면 도난당한자에서 제거하였다.
HashSet 데이터셋 할때, 여벌있는자도 도난당한 경우를 생각해서, 포함여부로 도난리스트에서 먼저 제거했다.
그 후 여벌set을 기준으로 for문을 돌려서 도난set을 제거해나갔다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[99클럽 코테 스터디 19일차 TIL] Greedy]]></title>
            <link>https://velog.io/@dayoung_sarah/99%ED%81%B4%EB%9F%BD-%EC%BD%94%ED%85%8C-%EC%8A%A4%ED%84%B0%EB%94%94-17%EC%9D%BC%EC%B0%A8-TIL-%EA%B9%8A%EC%9D%B4%ED%83%90%EC%83%89</link>
            <guid>https://velog.io/@dayoung_sarah/99%ED%81%B4%EB%9F%BD-%EC%BD%94%ED%85%8C-%EC%8A%A4%ED%84%B0%EB%94%94-17%EC%9D%BC%EC%B0%A8-TIL-%EA%B9%8A%EC%9D%B4%ED%83%90%EC%83%89</guid>
            <pubDate>Fri, 09 Aug 2024 07:35:51 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/57eddd9b-77dc-46f9-9e10-897e18984b5f/image.png" alt=""></p>
<h3 id="문제">문제</h3>
<ul>
<li><strong>문제 설명</strong></li>
</ul>
<p>과일 장수가 사과 상자를 포장하고 있습니다. 사과는 상태에 따라 1점부터 k점까지의 점수로 분류하며, k점이 최상품의 사과이고 1점이 최하품의 사과입니다. 사과 한 상자의 가격은 다음과 같이 결정됩니다.</p>
<p>한 상자에 사과를 m개씩 담아 포장합니다.
상자에 담긴 사과 중 가장 낮은 점수가 p (1 ≤ p ≤ k)점인 경우, 사과 한 상자의 가격은 p * m 입니다.
과일 장수가 가능한 많은 사과를 팔았을 때, 얻을 수 있는 최대 이익을 계산하고자 합니다.(사과는 상자 단위로만 판매하며, 남는 사과는 버립니다)</p>
<p>예를 들어, k = 3, m = 4, 사과 7개의 점수가 [1, 2, 3, 1, 2, 3, 1]이라면, 다음과 같이 [2, 3, 2, 3]으로 구성된 사과 상자 1개를 만들어 판매하여 최대 이익을 얻을 수 있습니다.</p>
<p>(최저 사과 점수) x (한 상자에 담긴 사과 개수) x (상자의 개수) = 2 x 4 x 1 = 8
사과의 최대 점수 k, 한 상자에 들어가는 사과의 수 m, 사과들의 점수 score가 주어졌을 때, 과일 장수가 얻을 수 있는 최대 이익을 return하는 solution 함수를 완성해주세요.</p>
<ul>
<li><strong>제한사항</strong>
3 ≤ k ≤ 9
3 ≤ m ≤ 10
7 ≤ score의 길이 ≤ 1,000,000
1 ≤ score[i] ≤ k
이익이 발생하지 않는 경우에는 0을 return 해주세요.</li>
</ul>
<ul>
<li><strong>입출력 예</strong></li>
</ul>
<table>
        <thead>
            <tr>
                <th>k</th>
                <th>m</th>
                <th>score</th>
                <th>result</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>3</td>
                <td>4</td>
                <td>[1, 2, 3, 1, 2, 3, 1]</td>
                <td>8</td>
            </tr>
            <tr>
                <td>4</td>
                <td>3</td>
                <td>[4, 1, 2, 2, 4, 4, 4, 4, 1, 2, 4, 2]</td>
                <td>33</td>
            </tr>
        </tbody>
</table>



<ul>
<li><strong>입출력 예 설명</strong>
입출력 예 #1</li>
</ul>
<p>문제의 예시와 같습니다.
입출력 예 #2</p>
<p>다음과 같이 사과 상자를 포장하여 모두 팔면 최대 이익을 낼 수 있습니다.</p>
<table>
        <thead>
            <tr>
                <th>사과 상자</th>
                <th>가격</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>[1, 1, 2]</td>
                <td>1 x 3 = 3</td>
            </tr>
            <tr>
                <td>[2, 2, 2]</td>
                <td>2 x 3 = 6</td>
            </tr>
            <tr>
                <td>[4, 4, 4]</td>
                <td>4 x 3 = 12</td>
            </tr>
            <tr>
                <td>[4, 4, 4]</td>
                <td>4 x 3 = 12</td>
            </tr>
        </tbody>
</table>
따라서 (1 x 3 x 1) + (2 x 3 x 1) + (4 x 3 x 2) = 33을 return합니다.


<h3 id="답">답</h3>
<pre><code class="language-java">import java.util.Arrays;

class Solution {
    public int solution(int k, int m, int[] score) {
        Arrays.sort(score);

        int maxProfit = 0;
        int n = score.length;

        for (int i = n - m; i &gt;= 0; i -= m) {
            int minScoreInBox = score[i];
            maxProfit += minScoreInBox * m; 
        }

        return maxProfit;
    }
}</code></pre>
<h3 id="해결방안">해결방안</h3>
<p>오름차순으로 정렬한 후, for문을 도는데 뒤에서 부터 m개씩 묶고, score[i]는 묶은 배열중 가장 작은값이기에 * m 을 해줘서 이를 총 합한다.  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[99클럽 코테 스터디 18일차 TIL] 깊이탐색]]></title>
            <link>https://velog.io/@dayoung_sarah/99%ED%81%B4%EB%9F%BD-%EC%BD%94%ED%85%8C-%EC%8A%A4%ED%84%B0%EB%94%94-1%EC%9D%BC%EC%B0%A8-TIL-%EA%B9%8A%EC%9D%B4%ED%83%90%EC%83%89</link>
            <guid>https://velog.io/@dayoung_sarah/99%ED%81%B4%EB%9F%BD-%EC%BD%94%ED%85%8C-%EC%8A%A4%ED%84%B0%EB%94%94-1%EC%9D%BC%EC%B0%A8-TIL-%EA%B9%8A%EC%9D%B4%ED%83%90%EC%83%89</guid>
            <pubDate>Thu, 08 Aug 2024 13:45:16 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/1e0161d2-e4b3-475e-8542-f53c879c55df/image.png" alt=""></p>
<h3 id="문제">문제</h3>
<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/134f1525-19ca-451c-bf22-ac44972b6c68/image.png" alt=""><img src="https://velog.velcdn.com/images/dayoung_sarah/post/38baabd3-4073-49a4-bf5c-0b76c72d84ee/image.png" alt=""></p>
<h3 id="답">답</h3>
<pre><code class="language-java">
public class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode() {}
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}


class Solution {
    private TreeNode newRoot = null;
    private TreeNode current = null;

    public TreeNode increasingBST(TreeNode root) {
        inOrderTraversal(root);
        return newRoot;
    }

    private void inOrderTraversal(TreeNode node) {
        if(node == null) return;

        inOrderTraversal(node.left);

        if(newRoot == null) {
            newRoot = new TreeNode(node.val);
            current = newRoot;
        } else {
            current.right = new TreeNode(node.val);
            current = current.right;
        }

        inOrderTraversal(node.right);
    }
}</code></pre>
<h3 id="문제해결">문제해결</h3>
<ul>
<li><p><strong>이진 탐색 트리(Binary Search Tree, BST)</strong>
왼쪽 서브트리의 모든 노드들은 현재 노드보다 작다.
오른쪽 서브트리의 모든 노드들은 현재 노드보다 크다.</p>
</li>
<li><p><strong>중위 순회(in-order)</strong>
트리를 순회하는 방법 중 하나로, 트리의 노드들을 왼쪽에서 오른쪽으로 순서대로 방문하는 방식이다.
이 순회 방법을 사용하면 이진 탐색 트리에서는 노드들이 오름차순으로 방문하게 된다.</p>
</li>
</ul>
<pre><code>- 이진 탐색 트리 구조
    5
   / \
  3   7
 / \   \
2   4   8

- 중위 순회
2 -&gt; 3 -&gt; 4 -&gt; 5 -&gt; 7 -&gt; 8</code></pre><p>문제를 해결하기 위해 </p>
<ol>
<li>이진 탐색 트리를 중위 순회한 후,</li>
<li>모든 노드를 오름차순으로 나열하는 새로운 트리를 만들어야 한다.
(새로운 트리에서는 각 노드가 오직 오른쪽 자식만을 가지며, 왼쪽 자식은 없다.)</li>
</ol>
<ul>
<li><p>새로운 트리 만들기
새로운 트리를 만들기 위해서 어떻게 접근해야 할지 아예 감이 안 잡혀서, gpt한테 힌트를 얻었다.
우선 class 내부에 변수 newRoot를 선언하여 새로운 트리를 선언한다.</p>
</li>
<li><p>오른쪽 트리만 있는 구조 만들기
그렇다면 오름차순으로 나열한 오른쪽 트리만 있는 구조는 어떻게 만들어야 할지 또 난관에 봉착했다.</p>
</li>
</ul>
<ol>
<li><p>current 내부변수 추가
이를 위해 class 내부 변수로 current 를 선언해서, 새로운 노드를 current.right 에만 추가하고, 이를 current로 변경해준다.</p>
</li>
<li><p>inOrderTraversal 메서드 내부에 재귀함수 호출
트리 구조에서는 재귀함수를 빼 놓을 수 없는데, 재귀함수를 언제 호출하며, 인자값을 무엇으로 주어주느냐 고민했다. 먼저 오름차순으로 정렬해야 하기 때문에 node.left를 먼저 인자로 넣어주고, 그 후 newRoot/current 를 처리하는 로직을 배치한 후, 마지막에 node.right 를 인자로 넣어서 재귀함수를 또 한번 호출해준다.</p>
</li>
<li><p>newRoot/current 내부변수 조작
newRoot 는 최상단 노드로 오름차순 정렬시 첫번째 값이 들어간다.
일단 newRoot의 null 여부로 분기를 가르고, null 인경우 new TreeNode로 값을 생성하고, current 를 newRoot로 할당한다.
만약 newRoot가 null이 아닐 경우, 새로운 트리는 오른쪽 트리만 가지기 때문에, current.right 에 new TreeNode를 추가하고, current 는 current.right 으로 재할당해준다.</p>
</li>
</ol>
<h3 id="회고">회고</h3>
<p>트리 구조 문제 좀 풀었다고, 이제 풀수 있을거란 오만한 생각을 함.
계속 새로운 문제를 접하면서 언젠가 능숙하게 풀 수 있는 날이 오기를..
오늘 문제는 gpt 문제풀이를 보고도 이해하는데 한참 걸렸다. 복습 여러번 해야 할듯
아좌좌~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[99클럽 코테 스터디 17일차 TIL] 깊이 탐색]]></title>
            <link>https://velog.io/@dayoung_sarah/99%ED%81%B4%EB%9F%BD-%EC%BD%94%ED%85%8C-%EC%8A%A4%ED%84%B0%EB%94%94-17%EC%9D%BC%EC%B0%A8-TIL-%EA%B9%8A%EC%9D%B4-%ED%83%90%EC%83%89</link>
            <guid>https://velog.io/@dayoung_sarah/99%ED%81%B4%EB%9F%BD-%EC%BD%94%ED%85%8C-%EC%8A%A4%ED%84%B0%EB%94%94-17%EC%9D%BC%EC%B0%A8-TIL-%EA%B9%8A%EC%9D%B4-%ED%83%90%EC%83%89</guid>
            <pubDate>Thu, 08 Aug 2024 00:35:57 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/2536430c-edf5-489c-9715-642da0e8c6bd/image.png" alt=""></p>
<h3 id="문제">문제</h3>
<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/494a3539-c676-40ae-871b-bdc13dad9be7/image.png" alt=""></p>
<h3 id="답">답</h3>
<pre><code class="language-java">import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

public class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode() {}
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

class Solution {
    public List&lt;Integer&gt; inorderTraversal(TreeNode root) {
        List&lt;Integer&gt; result = new ArrayList&lt;&gt;();
        Stack&lt;TreeNode&gt; stack = new Stack&lt;&gt;();
        TreeNode current = root;

        while (current != null || !stack.isEmpty()) {
            while (current != null) {
                stack.push(current);
                current = current.left;
            }

            current = stack.pop();
            result.add(current.val);

            current = current.right;
        }

        return result;
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[99클럽 코테 스터디 16일차 TIL] 완전탐색]]></title>
            <link>https://velog.io/@dayoung_sarah/99%ED%81%B4%EB%9F%BD-%EC%BD%94%ED%85%8C-%EC%8A%A4%ED%84%B0%EB%94%94-16%EC%9D%BC%EC%B0%A8-TIL-%EC%99%84%EC%A0%84%ED%83%90%EC%83%89</link>
            <guid>https://velog.io/@dayoung_sarah/99%ED%81%B4%EB%9F%BD-%EC%BD%94%ED%85%8C-%EC%8A%A4%ED%84%B0%EB%94%94-16%EC%9D%BC%EC%B0%A8-TIL-%EC%99%84%EC%A0%84%ED%83%90%EC%83%89</guid>
            <pubDate>Tue, 06 Aug 2024 22:49:56 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dayoung_sarah/post/ba6571a5-6036-415a-be17-ecf3c40da01b/image.png" alt=""></p>
<h3 id="문제">문제</h3>
<ul>
<li><strong>문제 설명</strong></li>
</ul>
<p>명함 지갑을 만드는 회사에서 지갑의 크기를 정하려고 합니다. 다양한 모양과 크기의 명함들을 모두 수납할 수 있으면서, 작아서 들고 다니기 편한 지갑을 만들어야 합니다. 이러한 요건을 만족하는 지갑을 만들기 위해 디자인팀은 모든 명함의 가로 길이와 세로 길이를 조사했습니다.</p>
<p>아래 표는 4가지 명함의 가로 길이와 세로 길이를 나타냅니다.</p>
<table>
        <thead>
            <tr>
                <th>명함 번호</th>
                <th>가로 길이</th>
                <th>세로 길이</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>1</td>
                <td>60</td>
                <td>50</td>
            </tr>
            <tr>
                <td>2</td>
                <td>30</td>
                <td>70</td>
            </tr>
            <tr>
                <td>3</td>
                <td>60</td>
                <td>30</td>
            </tr>
            <tr>
                <td>4</td>
                <td>80</td>
                <td>40</td>
            </tr>
        </tbody>
</table>


<p>가장 긴 가로 길이와 세로 길이가 각각 80, 70이기 때문에 80(가로) x 70(세로) 크기의 지갑을 만들면 모든 명함들을 수납할 수 있습니다. 하지만 2번 명함을 가로로 눕혀 수납한다면 80(가로) x 50(세로) 크기의 지갑으로 모든 명함들을 수납할 수 있습니다. 이때의 지갑 크기는 4000(=80 x 50)입니다.</p>
<p>모든 명함의 가로 길이와 세로 길이를 나타내는 2차원 배열 sizes가 매개변수로 주어집니다. 모든 명함을 수납할 수 있는 가장 작은 지갑을 만들 때, 지갑의 크기를 return 하도록 solution 함수를 완성해주세요.</p>
<ul>
<li><strong>제한사항</strong></li>
</ul>
<p>sizes의 길이는 1 이상 10,000 이하입니다.
sizes의 원소는 [w, h] 형식입니다.
w는 명함의 가로 길이를 나타냅니다.
h는 명함의 세로 길이를 나타냅니다.
w와 h는 1 이상 1,000 이하인 자연수입니다.</p>
<ul>
<li><strong>입출력 예</strong><table>
      <thead>
          <tr>
              <th>Sizes</th>
              <th>Result</th>
          </tr>
      </thead>
      <tbody>
          <tr>
              <td>[[60, 50], [30, 70], [60, 30], [80, 40]]</td>
              <td>4000</td>
          </tr>
          <tr>
              <td>[[10, 7], [12, 3], [8, 15], [14, 7], [5, 15]]</td>
              <td>120</td>
          </tr>
          <tr>
              <td>[[14, 4], [19, 6], [6, 16], [18, 7], [7, 11]]</td>
              <td>133</td>
          </tr>
      </tbody>
</table>



</li>
</ul>
<ul>
<li><strong>입출력 예 설명</strong></li>
</ul>
<p>입출력 예 #1
문제 예시와 같습니다.</p>
<p>입출력 예 #2
명함들을 적절히 회전시켜 겹쳤을 때, 3번째 명함(가로: 8, 세로: 15)이 다른 모든 명함보다 크기가 큽니다. 따라서 지갑의 크기는 3번째 명함의 크기와 같으며, 120(=8 x 15)을 return 합니다.</p>
<p>입출력 예 #3
명함들을 적절히 회전시켜 겹쳤을 때, 모든 명함을 포함하는 가장 작은 지갑의 크기는 133(=19 x 7)입니다.</p>
<h3 id="답">답</h3>
<pre><code class="language-java">class Solution {
    public int solution(int[][] sizes) {
        int maxWidth = 0;
        int maxHeight = 0;

        for (int[] size : sizes) {
            int w = size[0];
            int h = size[1];

            if (w &lt; h) {
                int temp = w;
                w = h;
                h = temp;
            }
            maxWidth = Math.max(maxWidth, w);
            maxHeight = Math.max(maxHeight, h);
        }

        return maxWidth * maxHeight;
    }
}</code></pre>
]]></description>
        </item>
    </channel>
</rss>