<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>youngGyu.log</title>
        <link>https://velog.io/</link>
        <description>BackEnd developer</description>
        <lastBuildDate>Sun, 03 May 2026 15:57:30 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>youngGyu.log</title>
            <url>https://velog.velcdn.com/images/young_gyu/profile/8947a311-071b-4649-8bcd-b80103a334d0/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. youngGyu.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/young_gyu" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[주간 회고 - 1]]></title>
            <link>https://velog.io/@young_gyu/%EC%A3%BC%EA%B0%84-%ED%9A%8C%EA%B3%A0-1</link>
            <guid>https://velog.io/@young_gyu/%EC%A3%BC%EA%B0%84-%ED%9A%8C%EA%B3%A0-1</guid>
            <pubDate>Sun, 03 May 2026 15:57:30 GMT</pubDate>
            <description><![CDATA[<h2 id="첫-회고를-시작하며">첫 회고를 시작하며</h2>
<p>회고를 시작하기 전에 회고의 목적은 아래와 같다.</p>
<p>** 1. 한 주 동안의 작업에 대해 셀프 피드백 ** 
** 2. 글쓰기 실력 증진 **</p>
<p>지속가능한 개발자가 되기 위해 회고를 시작했다. 첫 번째로 한 주 동안 성장하고 있는가에 대해 셀프 피드백을 하기 위한 목적이 있다. 두 번째로 소프트 스킬을 늘리기 위해 회고한다. 소프트 스킬 중 글쓰기 실력을 늘리는 것이 회고의 목적이다.</p>
<p>회고를 쓰는 방법은 독백 양식으로 정했다. 내 생각을 글로 전달, 정리하기 위해 가장 효과적인게 독백 형식이라 생각하기 때문이다.</p>
<h2 id="한-주-동안-무엇을-이루기-위해-어떻게-노력-했는가-">한 주 동안 무엇을 이루기 위해 어떻게 노력 했는가 ?</h2>
<h3 id="면접-준비">면접 준비</h3>
<p>이번 주의 가장 큰 과제는 면접 준비였다. 삼성 SDS와 현대 오토에버에 최근 면접 일정이 잡혔다. 면접은 두 가지 부류가 있다. 인성 면접과 기술 면접으로 나는 인성 면접을 기술 면접보다 더 어려워한다. 지난 두 번의 인성 면접을 복기해보면 부끄러운 대답이 많고 설득력이 부족함을 느낀다. 이전 경험을 바탕으로 설득력있는 말하기의 필요성을 느끼게 됐다. </p>
<p>인성 면접에서 프로젝트 갈등 경험, 성격 장단점 등과 유사한 부분을 항상 어려워했다. 하지만, 네이버 부스트 캠프를 최근에 수료하여 경험을 돌이켜보니 얘기할 것들이 많았다. 회의 내용을 기반으로 <strong>회의 요약, 심리적 안전감 피드백을 하는 n8n 워크플로우를 만든 것</strong>과 <strong>48명의 동료들과 동료 평가를 주고 받은 것</strong>으로 경험에 기반한 말하기를 할 수 있도록 준비중이다.</p>
<p>기술 면접은 스터디로 준비중이다. 최근 DB 면접 스터디를 시작했고 이전엔 자바 면접 스터디를 완료했다. 면접 스터디간 말하기 연습과 새로운 정보들도 많이 학습한다고 느낀다. 하지만, 휘발성이 강해 2~3주 지나면 내용을 잊는 것 같다. 지식적인 휘발은 설득력있는 말하기, 두괄식 말하기를 불가능하게 만드므로 반복 학습의 중요성을 느낀다.</p>
<hr>
<h3 id="주간-회고의-시작">주간 회고의 시작</h3>
<p>이번주에 가장 칭찬할 점은 회고의 중요성을 깨닫고 주간 회고를 시작한 점이다. 읽다만 ‘함께 자라기’ 책을 다시 읽게 됐다. 과거에도 책장에서 눈을 많이 마주쳤지만 애써 모른척하곤 했다. 독서를 미루다가 읽어야할 시점이다 싶어서 다시 읽게 됐다. </p>
<blockquote>
<p>회고는 피드백이다</p>
</blockquote>
<p>이 책을 읽으며 가장 인상 깊은 내용은 소프트웨어 공부는 근본적으로 어렵다는 것이다. 사람, 동물들이 학습을 하기 위해선 <strong>피드백이 필요</strong>하다. 동물에게 개인기를 부리기 위해 특정 행동을하면 간식이라는 피드백을 준다. 이를 통해 동물은 학습할 수 있는 것이다. </p>
<p>하지만, 소프트웨어 개발은 어떤가? 지금 작성한 코드, 설계의 문제점을 깨닫기 위해 1~2달의 시간이 소요될 수 있다. 문제점을 깨달았을 땐 과거의 의도를 잊을 수 있고 피드백 주기가 너무 길어 학습 효과가 적다.</p>
<p>그래서, 소프트웨어 개발을 잘하기 위해선 회고가 필요하다. 책은 스스로 지난 과제, 학습한 것들에 대해 피드백을 남기고 메타인지를 훈련하는 것이 회고임을 소개한다.</p>
<p>이 내용을 읽으면서 순간적으로 독서가 즐거웠다. 동료 개발자들에게 나중에 이 책을 추천해야겠다. </p>
<p>이 책의 가장 큰 장점은 방향성을 알려준다. 시간이 지나 AI가 아무리 발전해도 인간이 소프트웨어를 만드는데 기여한다면 개발자라는 직업이 존재하는 한 배울점이 많은 도서임을 느낀다. 나중에 이 책에 대해 독후감을 남기며 인상 깊은 내용을 정리해보려 한다.</p>
<hr>
<h2 id="한-주-동안-몰입을-방해한-요소는-무엇인가-">한 주 동안 몰입을 방해한 요소는 무엇인가 ?</h2>
<h3 id="심리적인-압박">심리적인 압박</h3>
<p> 면접 준비를 미뤘다. 면접 준비를 월~수 동안 제대로 하지 못했다. 면접 준비의 막막함 때문이다. 기존에 면접을 진행하며 아무리 고민해도 답변하기 힘든 질문들이 있었다. 실제로 면접에서도 해당 질문들을 잘 말하지 못했다. 이러한 부분들이 이번 면접 준비에 있어 심리적 압박으로 다가왔던 것 같다. </p>
<p>그래도, 이후에는 면접 준비를 꽤 했다. 인성 면접 기반으로 준비했고 부스트캠프의 경험을 되짚어보니 경험에 기반한 답변을 더 준비할 수 있게 되어 성장함이 느껴졌다. 이젠 어느정도 자신감이 붙기 시작했다. 면접 기회는 소중한 것이니 남은 기간동안 부끄럽지않게 준비하자.</p>
<hr>
<h3 id="늦잠">늦잠</h3>
<p>매일 취준 스터디원과 데일리 스크럼을 10시에 진행한다. 하지만, 서로 합의하에 늦잠을 자며 데일리 스크럼을 미룬 적이 몇 번 있었다. 더 말할 것 없이 부끄러운 일인 것 같다. 늦게 일어남에 따라 육체가 피곤하고 작업량이 준 것을 느꼈다. 앞으로 이런 일이 없도록 주의해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[네이버 Green Developer 공개 후기]]></title>
            <link>https://velog.io/@young_gyu/%EB%84%A4%EC%9D%B4%EB%B2%84-Green-Developer-%EA%B3%B5%EA%B0%9C-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@young_gyu/%EB%84%A4%EC%9D%B4%EB%B2%84-Green-Developer-%EA%B3%B5%EA%B0%9C-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sat, 27 Dec 2025 06:40:59 GMT</pubDate>
            <description><![CDATA[<p>네이버클라우는 예비 개발자를 지원하는 Green Developer 프로그램을 운영중입니다. 덕분에 저는 네이버 부스트캠프 소속으로 Green Developer 혜택을 받아 네이버 클라우드 플랫폼에서 20만원 크레딧을 받을 수 있었습니다.</p>
<h3 id="프로젝트를-소개해-주세요">프로젝트를 소개해 주세요</h3>
<p> 예비 개발자의 CS 기술 면접 학습 경험을 개선하기 위한 학습 서비스를 만들고 있습니다.
기존의 기술 면접 준비는 정해진 질문을 수동적으로 읽는 방식에 머무는 경우가 많았고,
학습자가 실제로 이해했는지 검증할 수 있는 구조가 부족하다는 문제의식에서 출발했습니다.</p>
<p>이를 해결하기 위해 질문–답변–퀴즈로 이어지는 능동적 학습 플로우를 설계했으며,
LLM을 활용해 콘텐츠를 자동 생성하되, Human-in-the-Loop 검수 구조를 도입해
콘텐츠의 신뢰성과 기술적 정확성을 확보했습니다.</p>
<blockquote>
<p>콘텐츠 자동 생성 과정<br><img src="https://velog.velcdn.com/images/young_gyu/post/b384c094-3fab-4e8b-b157-8fada230e0b2/image.png" alt=""> </p>
</blockquote>
<p>또한 사용자의 학습 이력을 기반으로 한 개인화 추천과
메일을 통한 1일 1문제 진입 구조를 결합해,
지속적인 학습과 재방문을 유도하는 것을 목표로 합니다.</p>
<h3 id="ncloud에서-어떤-서비스를-활용하셨나요">Ncloud에서 어떤 서비스를 활용하셨나요</h3>
<p>•    Server (Linux 기반 VM)
•    VPC
•    Subnet
•    ACG
•    Storage
•    Cloud DB for MySQL</p>
<h3 id="ncloud-서비스를-어떻게-적용-하였나요">Ncloud 서비스를 어떻게 적용 하였나요?</h3>
<p> 서비스는 VPC 기반의 단일 리전 아키텍처로 구성되었습니다.
백엔드 API 서버는 Ncloud Server 인스턴스 위에 배포되었으며,
 데이터베이스는 Cloud DB for MySQL을 사용해
애플리케이션 서버와 분리된 구조로 운영해습니다.
Subnet을 통해 내부/외부 네트워크 접근을 분리하고 DB는 VPC 내부에서만 접근 할 수 있도록 구성했습니다.</p>
<h3 id="ncloud-사용-중-특히-만족했던-점과-아쉬웠던-점은-무엇인가요">Ncloud 사용 중 특히 만족했던 점과, 아쉬웠던 점은 무엇인가요?</h3>
<p> Ncloud는 국내 서비스답게 공식문서의 가독성이 좋아 초보 개발자들도 공식문서를 보면 쉽게 사용가능할 수 있다는 점이 좋았습니다. 콘솔 UI가 직관적이어서 인프라 경험이 많지 않은 개발자도 부담 없이 접근할 수 있었습니다. 또한, Clova Studio를 사용할 수 있어 외부 서비스를 사용하지 않고도 생성형 AI 개발을 빠르고 편하게 할 수 있습니다.</p>
<h3 id="green-developers-프로그램-참여-소감-말씀-부탁-드립니다">Green Developers 프로그램 참여 소감 말씀 부탁 드립니다.</h3>
<p>Green Developers 프로그램을 통해 실제 서비스를 기준으로 클라우드 인프라를 설계하고 운영해보는 경험을 할 수 있었습니다. 크레딧의 사용에 대해 클라우드 자원 선택과 비용, 구조에 대해 한 단계 더 깊이 고민하는 계기가 되었습니다.</p>
<h3 id="마지막-한-말씀-부탁-드립니다">마지막 한 말씀 부탁 드립니다.</h3>
<p>프로젝트를 통해 AI를 단순한 자동화 도구가 아닌, 사람의 판단을 보조하는 파이프라인의 일부로 설계하는 경험을 할 수 있었습니다. 앞으로도 Ncloud를 활용해 추천 시스템 고도화, 트래픽 대응 구조 개선 등
실제 운영 환경을 고려한 기능 확장을 지속적으로 진행할 계획입니다.
Green Developers 프로그램을 통해 기획–개발–운영을 하나의 흐름으로 경험할 수 있었던 점에 감사드립니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVCC(Multi Version Concurrency Control)와 Read View 알아보기]]></title>
            <link>https://velog.io/@young_gyu/MVCCMulti-Version-Concurrency-Control%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@young_gyu/MVCCMulti-Version-Concurrency-Control%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Fri, 26 Dec 2025 12:10:49 GMT</pubDate>
            <description><![CDATA[<h1 id="mvcc의-등장-배경">MVCC의 등장 배경</h1>
<p> MVCC는 효과적인 동시성 제어를 위해 등장했다. MVCC 이전엔 동시성 제어를 위해 락 기반 동시성 제어 프로토콜인<strong>2 Phase-Locking</strong>을 사용했다. 락 기반 동시성 제어는 쿼리에 개발자들이 락을 명시하지 않아도 <strong>read-lock, write-lock</strong>이 자동으로 걸려 SQL 구문을 실행했다. 이러한 락 기반 동시성 제어의 문제점은 조회-쓰기간 잦은 <strong>락 경합</strong>으로 <strong>트랜잭션 처리량이 낮다</strong>는 것이다. </p>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/f2d58bea-a1d6-44b4-8615-01d592c9777b/image.png" alt=""></p>
<p> 조회-쓰기의 락 경합은(베타락, 공유락을 생각해보자) 데이터를 단순히 읽기만 하면되는데 쓰기 작업이 끝날 때 까지 기다려야 하고, 반대로 쓰기작업은 데이터 쓰기만하면 되는데 데이터를 읽는 작업을 기다리는 것이 특징적이다.</p>
<p> 이러한 구조에서 서비스의 동시 접속자 수가 늘어난다면 락 경합도 그만큼 늘어날 것이고 처리량이 급격히 낮아질 것이다.</p>
<p> MVCC는 락 기반 동시성 제어보다 처리량을 높이면서 동시성을 제어하기 위해 등장했다. 락 기반 동시성 제어는 트랜잭션 충돌시 같은 데이터를 동시에 조회하는 것을 락으로 방지했다면, MVCC는 <strong>같은 데이터를 여러개의 버전</strong>으로 나누고 트랜잭션마다 특정 버전을 보여줌으로 일관된 읽기를 보장한다.</p>
<h1 id="mvccmulti-version-concurrency-control란">MVCC(Multi Version Concurrency Control)란?</h1>
<p> MVCC의 멀티 버전은 하나의 레코드에 대해 시간에 따른 여러 개의 버전이 존재함을 의미한다. MySQL InnoDB에선 언두로그를 사용해 MVCC를 구현한다. </p>
<h3 id="언두-로그">언두 로그</h3>
<p>언두 로그는 InnoDB에서 DML로 변경되기 이전 버전의 <strong>데이터를 별도로 백업하는 로그</strong>이다. 언두 로그는 트랜잭션, 격리 수준을 보장하기 위해 사용한다.</p>
<ul>
<li><p><strong>트랜잭션 보장</strong> - 트랜잭션이 예외에 의해 롤백되어야 할 상황이라면 데이터를 변경 이전으로 되돌려야한다. 이때, 언두 로그를 사용해 이전 버전의 데이터를 불러와 복구한다.</p>
</li>
<li><p><strong>격리 수준 보장</strong> - 서로 다른 트랜잭션에서 데이터를 변경하는 도중에 데이터를 조회하게 되면 트랜잭션의 격리 수준에 맞게 언두 로그의 데이터를 읽어 반환한다. 이 부분은 아래에 더 설명하겠다.</p>
<p>언두 로그는 MVCC의 버전별 레코드를 저장하여 <strong>특정 시점의 스냅샷</strong> 역할을 한다. 트랜잭션 롤백에 사용되거나 격리 수준에 따른 읽기 결과를 결정하는데 사용한다. </p>
<p>MVCC는 언두 로그를 사용한다. 트랜잭션에서 레코드의 값 변경 요청이 있을 때 데이터를 변경하기 전의 값을 언두로그에 기록하고 InnoDB 버퍼의 값을 변경한다. 언두로그에 기록된 값은 어떤 트랜잭션도 언두로그의 데이터를 사용하지 않는다면</p>
</li>
</ul>
<h2 id="read-view">Read View</h2>
<h3 id="read-view-필드-정의">Read View 필드 정의</h3>
<p> MVCC는 레코드의 변경 버전을 기록하고 각 트랜잭션마다 읽을 수 있는 범위를 한정한다. 이를 위해 Read View개념이 필요하다.</p>
<p> Read View를 이해하기 위해 <strong>m_ids, min_trx_id, max_trx_id, creator_trx_id</strong> 4가지 상태 필드를 알아야한다.</p>
<ul>
<li><strong>m_ids</strong> - Read View 생성 시점에 <strong>실행중인 트랜잭션 ID들의 리스트</strong>이다.</li>
<li><strong>min_trx_id</strong> - 실행중인 트랜잭션 ID중 <strong>가장 작은 값</strong>이다.</li>
<li><strong>max_trx_id</strong>  - 실행중인 트랜잭션 ID중 <strong>가장 큰 값</strong>이다.</li>
<li><strong>creator_trx_id</strong> - Read View를 생성할 트랜잭션 ID이다.</li>
</ul>
<h3 id="innodb의-히든-컬럼">InnoDB의 히든 컬럼</h3>
<p>Read View를 만들기 위한 준비가 아직 끝나지 않았다. InnoDB의 <strong>클러스터드 인덱스에 레코드마다 히든 컬럼</strong>이 존재한다. 히든 컬럼을 알아보자.</p>
<ul>
<li><strong>DB_TRX_ID</strong>(6 Byte) - 해당 행을 마지막으로 UPDATE 하거나 INSERT한 트랜잭션 ID 값이다.</li>
<li><strong>DB_ROLL_PTR</strong>(7 Byte) - 롤백 세그먼트에 기록된 언두 로그 레코드를 가리키는 포인터이다. 이 포인터를 체인처럼 사용해 순차적으로 언두 로그를 탐색할 수 있다.</li>
<li>DB_ROW_ID(6 Byte) - 개발자가 PK를 명시적으로 지정하지 않으면 히든 PK 값이 저장된다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/d71fa6a0-46ad-4073-ad49-3b3956e5f2c3/image.png" alt=""></p>
<h3 id="read-view-생성-규칙">Read View 생성 규칙</h3>
<ol>
<li><p><strong>trx_id &lt; min_trx_id</strong> - 해당 레코드를 수정한 트랜잭션은 Read View 생성 이전에 이미 커밋된 트랜잭션이므로 현재 트랜잭션에서 값을 읽을 수 있다.</p>
</li>
<li><p><strong>trx_id ≥ max_trx_id</strong> - 해당 레코드를  수정한 트랜잭션은 Read View 생성 이후에 시작된 트랜잭션에 의해 수정되었으므로 현재 트랜잭션에서 읽을 수 없다. (미래의 변경사항을 현재 트랜잭션이 읽을 수 없다.)</p>
</li>
<li><p><strong>min_trx_id ≤ trx_id ≤ max_trx_id</strong> - 이 경우는 m_ids 목록을 추가 탐색해야한다. 만약, m_ids에 포함되어 있다면 현재 실행중인 트랜잭션이라는 것을 의미한다. 따라서, 이 버전을 읽을 수 없다.</p>
<p> 만약, m_ids에 포함되어 있지 않다면 Read View 생성 시점에 이미 커밋된 트랜잭션이므로 이 버전은 읽을 수 있다.</p>
</li>
</ol>
<p> read committed 환경에서도 MVCC를 사용하여 레코드를 읽는다. 하지만, 격리 환경에 맞춘 데이터 읽기를 지원하기 위해 Read View 생성 과정이 다르다.</p>
<p> REPETABLE READ 격리 레벨에서는 <strong>Read View를 트랜잭션이 첫 SELECT 쿼리를 실행할 때 만들고</strong> 이후의 SELECT 쿼리는 READ VIEW를 재활용하여 <strong>일관된 읽기</strong>를 지원한다. 하지만, READ COMMITTED 격리 레벨에서는 SELECT 쿼리를 실행할 때 마다 READ VIEW를 만들어 non repetable read 문제가 발생할 수 있다. </p>
<h2 id="언두-로그는-언제-삭제되는가">언두 로그는 언제 삭제되는가</h2>
<p> 언두 로그는 해당 버전을 참조할 수 있는 트랜잭션이 없을 때 삭제된다. 언두로그는 현재 실행중인 트랜잭션 중에서 가장 오래된 Read View가 언두로그 버전을 볼 수 없다면 아무도 사용할 수 없다. 아무도 사용하지 않는 언두 로그들은 purge 스레드에 의해 삭제당한다.</p>
<p> 이러한 Read View, 언두 로그의 특징들을 생각했을 때 <strong>트랜잭션은 최대한 짧게 실행</strong>되어야한다. 특정 트랜잭션이 긴 시간동안 끝나지 않고 매우 많은 레코드를 수정하고 있다고 생각해보자. 많은 행의 버전이 바뀌었고 다른 트랜잭션들도 데이터를 수정할 것이기 때문에 언두 로그는 점점 더 거대하게 쌓일 것이다. 이렇게 쌓인 언두 로그는 공간적인 비용을 차지하는 것 뿐만이 아닌 <strong>조회 성능을 악화</strong>시킨다. MVCC에 의해 SELECT 조회 시 알맞은 Read View를 만들기 위해 언두 로그 체인을 타고 타고 내려가며 읽을 수 있는 과거 버전을 찾을 것이기 때문이다.</p>
<h3 id="consistent-read와-current-read"><strong>Consistent Read와</strong> <strong>Current Read</strong></h3>
<p><strong>Consistent Read</strong>는 <strong>MVCC 기반의 스냅샷을 읽는 것</strong>을 말한다. 트랜잭션의 Read View를 바탕으로 트랜잭션마다 볼 수 있는 버전을 만든다. 이러한 방식으로 논리적인 읽기의 일관성을 지킬 수 있다. Repetable Read에서 일반적인 SELECT 문을 실행시키면 MVCC Consistent Read가 적용된다.</p>
<p><strong>Current Read</strong>는** 현재 시점의 최신 커밋된 데이터를 읽는 것<strong>을 의미한다. **UPDATE,DELETE</strong>와 같은 <strong>DML</strong> 문은 MVCC의 영향을 받지 않는다. 항상 최신 존재하는 데이터를 기준으로 동작한다. MVCC는 오직 읽기 동시성을 늘리기 위한 목표이며 DML에 영향을 주지 않음을 기억하자.</p>
<p>Consistent Read와 Current Read의 차이를 아는 것은 중요하다. 트랜잭션에서 Consitent Read, Current Read를 혼용해서 사용할 경우 DBMS가 개발자의 의도와 다르게 작동할 수 있기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/f5e9ff1d-ab05-4530-a9bc-ddd82a26357d/image.png" alt=""></p>
<p>Consistent Read와 Current Read를 더 알아보기 위해 예제를 만들었다.</p>
<p> REPETABLE READ 레벨에서 트랜잭션을 시작한 후에 <strong>첫 번째 SELECT문이 실행</strong>되면 InnoDB는 <strong>해당 시점의 Read View를 만든다</strong>. 만들어진 Read View는 트랜잭션 종료까지 유지되어 이후의 SELECT도 이 스냅샷을 기준으로 데이터를 읽는다. 예제를 보면 tx1에서 INSERT로 두 개의 레코드를 삽입했지만 tx2의 두번쨰 SELECT에서도 여전히 데이터를 읽을 수 없음을 알 수 있다. </p>
<p> 하지만, tx2의 UPDATE 문은 이전과 다르게 작동한다. InnoDB에서 UPDATE,DELETE는 <strong>Current Read</strong>로 실제로 존재하는 최신 커밋된 레코드를 기준으로 동작하고 레코드 락을 획득한다. 그래서, 트랜잭션에서 보이지 않던 데이터여도 다른 트랜잭션이 INSERT를 커밋한 경우 UPDATE 문을 실행시킬 수 있는 것이다.</p>
<p> 이렇게 UPDATE로 데이터를 수정하고 SELECT를 실행하면 신기한 현상이 일어난다. Read View에 의해 이전에 안보이던 레코드중 UPDATE한 레코드 하나만 보이기 시작하는 것이다. InnoDB는 트랜잭션이 자기가 직접 수정한 레코드는 Read View와 무관하게 볼 수 있다. 그래서, 새로 삽입한 레코드 두 개중 UPDATE한 레코드만 보이고 나머지 레코드는 보이지 않는 것이다. </p>
<p> 이 예제는 InnoDB에서 트랜잭션 내부에 <strong>Consitence Read, Current Read</strong>를 사용함으로 동시성과 데이터 정합성을 모두 만족하려는 결과이며,  REPETABLE READ는 모든 SQL문이 같은 데이터를 바라보는 것이 아닌 Consistent Read를 사용하는 <strong>일반적인 SELECT에서만 적용</strong>된다는 얘기임을 알 수 있다. 데이터를 변경하는 DML은 Current Read를 사용하여 언제나 최신 커밋된 데이터를 보는 것을 주의해야한다. 만약, REPETABLE READ 레벨의 트랜잭션에서 최신 커밋된 레코드 데이터를 일고 싶은 경우에는 <strong>SELECT … FOR ( SHARE, UPDATE)</strong> 락을 걸어 레코드 값을 읽을 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TCP 혼잡제어와 원리(타호, 리노, 큐빅)]]></title>
            <link>https://velog.io/@young_gyu/TCP-%ED%98%BC%EC%9E%A1%EC%A0%9C%EC%96%B4%EC%99%80-%EC%9B%90%EB%A6%AC%ED%83%80%ED%98%B8-%EB%A6%AC%EB%85%B8-%ED%81%90%EB%B9%85</link>
            <guid>https://velog.io/@young_gyu/TCP-%ED%98%BC%EC%9E%A1%EC%A0%9C%EC%96%B4%EC%99%80-%EC%9B%90%EB%A6%AC%ED%83%80%ED%98%B8-%EB%A6%AC%EB%85%B8-%ED%81%90%EB%B9%85</guid>
            <pubDate>Wed, 03 Dec 2025 18:10:36 GMT</pubDate>
            <description><![CDATA[<h1 id="tcp-혼잡제어congestion-control가-필요한-이유">TCP 혼잡제어(Congestion Control)가 필요한 이유</h1>
<p> TCP <strong>혼잡제어</strong>는 네트워크가 혼잡하여 패킷을 정상적으로 보낼 수 없을 때 네트워크에 참여한 송신자의 패킷 송신량을 제어하여 과도한 네트워크 트래픽을 안정적이게 만든다. </p>
<p> 사람들은 누구나 동영상, 미디어를 빨리 보고 싶을 것이다. 만약, TCP 혼잡제어가 존재하지 않는다면 송신자들은 자신의 <code>bandwidth(최대 대역폭)</code>를 사용하여 패킷을 보내고자 할 것이고. 매우 혼잡해진 네트워크로 <code>Congestion Collapse(네트워크 붕괴)</code>가 발생해 정상적으로 패킷을 송수신하지 못할 것이다. 또한, 송신자는 패킷 손실이 발생하면 재전송을 시도할 것이고 재전송 패킷들에 의해 네트워크를 더 혼잡하게 만드는 악순환이 만들어진다. 머지않아 네트워크를 아무도 정상적으로 사용할 수 없을 것이다.</p>
<h1 id="tcp-tahoe타호">TCP Tahoe(타호)</h1>
<p><strong>TCP Tahoe</strong>는 혼잡 제어를 구현한 초기 알고리즘이다. TCP는 연결마다 <strong>cwnd(TCP 송신자의 내부 변수)</strong>를 통해 아직 ACK를 수신하지 못한 패킷 수를 제어한다. 즉, 송신할 수 있는 최대 패킷 수를 제어할 수 있다.</p>
<h3 id="tcp-tahoe-혼잡-제어-과정">TCP Tahoe 혼잡 제어 과정</h3>
<p>TCP Tahoe의 혼잡 제어는 두 가지 상태로 이루어진다.</p>
<ol>
<li><strong>Slow Start (느린 시작)</strong></li>
<li><strong>Congestion Avoidance (혼잡 회피)</strong></li>
</ol>
<p>TCP Tahoe는 연결을 시작할 때 <code>Slow Start(느린 시작)</code> 상태에 돌입한다. 초기 연결시에는 cwnd 값이 1로 한 번에 하나의 패킷밖에 보내지 못한다. 하지만, <strong>Slow Start</strong> 상태에서는 ACK를 받으면 <strong>cwnd 크기를 2배로 늘린다.</strong> 따라서 매 RTT마다 지수적으로 cwnd 크기가 증가하는 구조이다. 초기에 보낼 수 있는 데이터양은 적지만 지수적으로 증가하는 특징이 있다.</p>
<p>cwnd 크기가 <code>ssthresh(slow start thresh)</code> 값을 초과하게 되면 <code>Congestion Avoidance(혼잡 회피)</code> 상태에 돌입한다. <strong>혼잡 회피 상태</strong>에서는 ACK를 받으면 <strong>cwnd 크기를 1 증가시킨다.</strong> Slow Start에서 2배씩 지수적으로 최대 송신 패킷 수를 늘린 것에 비하면 매우 보수적인 수치이다. 네트워크 혼잡으로 패킷 손실이 발생할 것을 대비하여 선형적으로 천천히 cwnd 값을 늘리는 상태이다.</p>
<h3 id="패킷-손실이-발생하면">패킷 손실이 발생하면?</h3>
<p> <code>Slow Start</code>, <code>Congestion Avoidance</code> 상태 덕분에 cwnd 값은 지속적으로 증가하게 된다. 만약, 네트워크가 혼잡하여 패킷 손실이 발생하면 어떻게 되는 것인가? TCP Tahoe는 <strong>ssthresh 값을 현재 cwnd의 절반으로 바꾸고 cwnd 값을 1로 초기화한다.</strong> 다시 Slow Start 상태가 되어 천천히 패킷을 보내기 시작한다.</p>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/163b1810-00e7-4fdb-856a-d784be13c886/image.png" alt=""></p>
<h3 id="패킷-손실은-어떻게-판단하는가">패킷 손실은 어떻게 판단하는가?</h3>
<p>Sender의 타임아웃 이벤트가 발생하면 정해진 시점까지 ACK가 도착하지 않았으므로 패킷 손실로 간주한다. 또한, 타임아웃 전에 중복 ACK를 3개 받으면 특정 패킷이 손실이 난 것으로 간주한다.</p>
<blockquote>
<p>왜 중복 ACK를 3번 받으면 패킷 손실로 간주할까? 중복 ACK 1~2번은 세그먼트의 수신 순서가 네트워크 혼잡, 경로의 차이에 의해 일어날 수 있는 자연스러운 현상이다. TCP는 세그먼트를 받을 때 누락된 세그먼트가 있다면 누락된 세그먼트의 번호를 ACK로 보낸다. Sender가 Segment [1,2,3,4,5]를 순서대로 보낸 경우 Receiver에게 [1,3,2,4,5] 순서로 도착했다면 ACK는 각각 세그먼트를 받는 시점에 [2,2,4,5,6]를 보낼 것이다. 이렇듯 중복 ACK 수신은 TCP 입장에서 자연스러운 현상이다. 
 하지만, 3번의 중복 ACK 수신 부터는 일반적이지 않고 이전의 패킷이 손실 났을 가능성이 높아 패킷 손실로 간주한다.</p>
</blockquote>
<h1 id="tcp-reno리노">TCP Reno(리노)</h1>
<p> <strong>TCP Reno</strong>는 <strong>TCP Tahoe</strong>의 비효율성을 개선하는 혼잡제어 알고리즘이다. Tahoe는 ‘타임아웃’, ‘중복 ACK 3회’ 발생시 cwnd사이즈를 1로 초기화하고 slow start 상태로 되돌아갔다. 하지만, Reno는 Tahoe와 다르게 <strong>중복 ACK 3번</strong>은 데이터가 수신되고 있기 때문에 <code>경미한 혼잡</code>으로 간주한다. Reno는 경미한 혼잡인 경우 <code>Fast Recovery</code> 라는 새로운 상태에 진입하게 된다.</p>
<h3 id="fast-recovery빠른-회복">Fast Recovery(빠른 회복)</h3>
<p>Fast Recovery에선 cwnd를 1로 초기화시키지 않는다. ssthresh는 현재 cwnd 크기의 절반으로(ssthresh = cwnd/2 )로 줄이지만 cwnd는 ‘ssthresh + 3’크기로 변경해 Congestion Avoidance 상태로 돌아간다. 물론, 타임아웃(심각한 혼잡)때는 sstrhesh = cwnd/2, cwnd =1 로 동일하다.</p>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/bde9411f-5e6b-4acd-849b-a43feccf36c6/image.png" alt=""></p>
<h1 id="tcp-cubic큐빅">TCP CUBIC(큐빅)</h1>
<p> TCP Reno, TCP Tahoe와 같은 알고리즘은 대역폭을 완전히 채우는 데 시간이 오래 걸리는 문제가 있다. 또한, RTT 기반으로 cwnd값을 증가 시키기 때문에 장거리 통신에서 효율이 낮을 수 밖에 없다. 이러한 문제를 해결하고 고속 네트워크 환경에 최적화된 혼잡 제어 알고리즘이 <code>TCP CUBIC</code>이다. CUBIC은 Linux 운영체제의 기본값으로 사용하며 매우 범용적인 현대의 TCP 혼잡제어 알고리즘이다.</p>
<h3 id="tcp-cubic-혼잡-제어-과정">TCP CUBIC 혼잡 제어 과정</h3>
<p>CUBIC은 Reno와 슬로 스타트, 빠른 복구 단계는 동일하지만 <strong>혼잡 회피 단계가 수정</strong>되었다.</p>
<p> CUBIC은 Reno, Tahoe처럼 혼잡회피 상태에서 값을 선형적으로 증가시키지 않고, <strong>큐빅 함수(3차 함수)</strong>를 사용하여 cwnd를 증가시킨다. 손실이 마지막으로 감지되었을 때 cwnd 크기를 <strong>Wmax</strong>로 설정한다. 혼잡 윈도 크기가 <strong>Wmax</strong>에 도달하는 미래의 시점을 <strong>K</strong>라고 할 때 큐빅 함수로 <strong>K</strong> 값을 구한다. </p>
<p> 큐빅은 현재 시점 <strong>t</strong>가 <strong>K</strong>에 가까울 때 보다 <strong>t</strong>가 <strong>K</strong>에서 더 멀리 떨어지면 혼잡 윈도 크기의 증가가 훨씬 커진다. 큐빅은 손실 전의 혼잡 윈도 크기인 <strong>Wmax</strong>에 가까워지도록 TCP 전송 속도를 <strong>빠르게 증가</strong> 시키고 Wmax에 가까워지면 대역폭을 <strong>조심스럽게 탐지한다.</strong></p>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/7fc80841-f4de-495a-9753-84b0fffb4cb0/image.png" alt=""></p>
<p>2000년대 초반은 TCP 리노를 대부분 사용했다. 하지만, 현대에서는 큐빅을 대부분 사용한다. 큐빅은 Linux 등 많은 운영체제에서 기본적으로 사용된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[브라우저 쿠키 이해하기]]></title>
            <link>https://velog.io/@young_gyu/%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%BF%A0%ED%82%A4-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@young_gyu/%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%BF%A0%ED%82%A4-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 24 Sep 2025 06:57:25 GMT</pubDate>
            <description><![CDATA[<h2 id="쿠키란-무엇인가">쿠키란 무엇인가?</h2>
<blockquote>
<p>1970년대 <strong>유닉스(UNIX) 시스템</strong>에서 “매직 쿠키(Magic Cookie)”라는 표현이 사용되었는데, 이는 <strong>프로세스 간 교환되는 작은 데이터 조각</strong>을 뜻했습니다. </p>
<p> 이때 “쿠키”라는 단어를 쓴 이유는, <strong>작고 간단하지만 중요한 정보가 들어있는 ‘과자 조각(cookie)’</strong>이라는 은유 때문이었습니다.
기존 컴퓨터 과학에서 쓰던 “매직 쿠키” 개념을 차용해 <strong>브라우저와 서버가 주고받는 작은 데이터</strong>에 “쿠키”라는 이름을 붙였습니다.</p>
</blockquote>
<p> 쿠키는 <code>서버가 클라이언트(브라우저)에 보내는 데이터 조각</code>이다. HTTP는 무상태성(stateless)을 가진다. 무상태성 때문에 웹 사이트에서 로그인을 했지만 자격 증명이 정상적으로 이루어졌는지 증거가 존재하지 않는다. 그래서, 서버는 통신과정에서 무상태성을 극복하기 위해 쿠키를 사용한다. 서버는 로그인이 성공했다면 세션에 해당 유저의 정보를 저장한다. </p>
<p> 해당 사용자가 인증된 유저임을 알리기 위해 서버는 세션 ID를 쿠키에 담아 클라이언트에게 보낸다. 클라이언트는 사이트의 다른 기능을 사용할 때 HTTP 요청에 쿠키를 같이 보내어 ‘나는 로그인 된 사용자이며 이 세션 ID에 내 정보가 담겨있다’를 알린다.<br>다시 말해 쿠키는 사용자(클라이언트)를 식별하기 위해 서버에서 보내는 작은 데이터 조각이다.</p>
<p>쿠키는 브라우저의 파일에 저장된다. 윈도우 환경에서 크롬 브라우저 쿠키가 실제 저장되는 공간을 캡쳐했다. Cookies 파일안에 우리가 서버로부터 받은 쿠키를 저장한다.
<img src="https://velog.velcdn.com/images/young_gyu/post/5411f010-4505-4a7d-8cbf-4fa137d897db/image.png" alt=""></p>
<h2 id="쿠키의-용도">쿠키의 용도</h2>
<p> 쿠키는 세가지 형태로 사용된다. 추적은 <code>서드 파티 쿠키</code> , 인증, 개인화는 <code>퍼스트 파티 쿠키</code>를 사용한다.</p>
<ol>
<li><strong>추적(Tracking)</strong> → 방문 기록, 광고용 브라우징 히스토리 관리</li>
<li><strong>인증(Authentication)</strong> → 로그인 세션 유지, 재인증 방지</li>
<li><strong>개인화(Personalization)</strong> → 다크 모드, 언어 설정 등 개인화</li>
</ol>
<h2 id="쿠키-생성">쿠키 생성</h2>
<p> 서버는 HTTP 응답의 ‘Set-Cookie’ 헤더를 통해  쿠키를 브라우저에게 저장하도록 지시할 수 있다. 쿠키의 속성을 통해 쿠키를 사용할 기간과 어떤 도메인에서 사용할지 결정할 수 있다. </p>
<p> 서버는 브라우저에 쿠키를 영구히 저장하게 할 수 있다. 하지만, 브라우저 정책에 따라 실제 저장하는 기간은 달라진다. 크롬은 최대 400일을 사파리는 최대 7일의 쿠기를 저장할 수 있다. 또한, 사용자가 직접 브라우저의 쿠키를 지울 수도 있다. (크롬은 1년에 한 번 정도 방문하는 사이트(예: 연간 수도/전기 사용량 보고 사이트)를 정상작동 하기 위해 400일의 시간을 둔다고 한다.)</p>
<img src="https://velog.velcdn.com/images/young_gyu/post/329cb968-a5ab-4eea-9444-9b3b1d463e90/image.png" width="80%" height="70%"/>

<h2 id="쿠키의-종류">쿠키의 종류</h2>
<h3 id="서드-파티-쿠키">서드 파티 쿠키</h3>
<p>서드 파티 쿠키는 사용자가 방문한 웹 사이트 이외의 웹사이트(광고 배너)에서 발행한 쿠키이다. 주로 광고 목적으로 사용된다. 서드 파티 쿠키를 사용하기 위해 법률적으로 규제화 되어 사용자의 동의를 맡는 경우도 있다. (해외 사이트의 쿠키 동의) </p>
<h3 id="퍼스트-파티-쿠키">퍼스트 파티 쿠키</h3>
<p>퍼스트 파티 쿠키는 사용자가 방문한 웹 사이트에서 발행한 쿠키이다. 사용자 인증을 위해 주로 사용한다. 브라우저에서 HTTP 요청을 하면 도메인을 만족하는 쿠키를 서버에 같이 전송한다. </p>
<h2 id="samesite">SameSite</h2>
<p>클라이언트가 Cross Orgin로 자원을 요청을하는 경우 쿠키 전송을 제한할 수 있다. SameSite 속성은 CSRF를 방지하기 위한 장치이다.</p>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/30d35cd8-f7a5-4052-9071-d49621ad9963/image.png" alt=""></p>
<p><code>SameSite</code> 속성에는 세 가지 옵션이 있다.</p>
<ul>
<li><p><strong>Lax (기본값)</strong></p>
<ul>
<li>쿠키 전송을 일부 안전한 요청으로만 제한합니다.</li>
<li>예: 브라우저 주소창에 직접 입력한 최상위 HTTP GET/HEAD 요청, 혹은 사용자가 클릭하여 명시적으로 도메인이 바뀌는 경우.</li>
</ul>
</li>
<li><p><strong>Strict</strong></p>
<ul>
<li>쿠키는 <strong>오직 원본 사이트</strong>로만 전송됩니다.</li>
<li>어떤 교차 도메인 요청에서도 쿠키는 절대 전송되지 않습니다.</li>
</ul>
</li>
<li><p><strong>None</strong></p>
<ul>
<li>어떤 HTTP 메서드이든 상관없이, 교차 출처 사이트로도 쿠키가 전송됩니다.</li>
</ul>
<p>현실적으로 Lax를 많이 사용한다. Lax는 CSRF를 부분적으로 방어하며. CSRF 토큰을 사용하여 보완할 수 있다.</p>
</li>
</ul>
<h2 id="서드-파티-쿠키로-광고-수집을-하는-원리">서드 파티 쿠키로 광고 수집을 하는 원리</h2>
<ol>
<li>사용자가 news.com 페이지에 접속</li>
<li>페이지 내부에 <code>&lt;iframe src=&quot;https://ads.com/banner&quot;&gt;</code> 같은 외부 리소스 포함</li>
<li>브라우저는 ads.com에도 HTTP 요청을 보냄</li>
<li>이때 ads.com 서버가 Set-Cookie 헤더를 내려주면, <strong>ads.com 도메인에 대한 쿠키가 생성됨</strong></li>
<li>이후 사용자가 다른 사이트(blog.com)에 들어갔는데 또 같은 ads.com 광고가 있으면,<ul>
<li>브라우저는 자동으로 ads.com 쿠키를 함께 전송</li>
<li>→ ads.com은 “이 사용자가 news.com에도 갔었네”라고 알 수 있음</li>
</ul>
</li>
</ol>
<h2 id="쿠키의-단점">쿠키의 단점</h2>
<p> 쿠키의 송신 조건을 만족하는 모든 HTTP 요청에 쿠키가 함께 전송된다. 웹사이트는 쿠키를 복수개 발행할 수 있다. 이로 인해 동일한 데이터가 반복적으로 전송되며, 추가적인 대역폭 소비가 발생한다. 쿠키의 path 속성을 통해 쿠키의 사용 범위를 최소화해 보안 효과와 네트워크 비용을 절감할 수 있다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[REST/RESTful 그리고 REST의 6가지 제약 조건 정복하기]]></title>
            <link>https://velog.io/@young_gyu/RESTRESTful-%EA%B7%B8%EB%A6%AC%EA%B3%A0-REST%EC%9D%98-6%EA%B0%80%EC%A7%80-%EC%A0%9C%EC%95%BD-%EC%A1%B0%EA%B1%B4-%EC%A0%95%EB%B3%B5%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@young_gyu/RESTRESTful-%EA%B7%B8%EB%A6%AC%EA%B3%A0-REST%EC%9D%98-6%EA%B0%80%EC%A7%80-%EC%A0%9C%EC%95%BD-%EC%A1%B0%EA%B1%B4-%EC%A0%95%EB%B3%B5%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 28 Aug 2025 17:22:34 GMT</pubDate>
            <description><![CDATA[<h1 id="여는말">여는말</h1>
<p>요즘 개발자 채용 공고에서 REST API 개발 경험은 필수적인 요소로 자리잡은 것 같다. 하지만, REST란 무엇인가? 스스로 물어봤을 때 추상적인 개념들만 떠올랐고 제대로 공부해본 경험은 없는 것 같았다. 좋은 API 설계를 위해 REST를 더 공부해야함을 느꼈고 이번 기회에 구조화된 글로 나타내보려고 했다.</p>
<h1 id="rest는-네트워크-아키텍처다">REST는 네트워크 아키텍처다</h1>
<p> <strong>REST(Representational State Transfer)</strong>는 무엇일까? REST는 프로토콜이 아닌 아키텍처 스타일이다. REST 아키텍처는 ‘로이 필딩’의 박사 논문(<a href="https://ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm?utm_source=chatgpt.com)%EC%97%90%EC%84%9C">https://ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm?utm_source=chatgpt.com)에서</a> 처음 제시되었다.</p>
<p>위키피디아( <a href="https://ko.wikipedia.org/wiki/REST">https://ko.wikipedia.org/wiki/REST</a> )는 REST를 아래와 같이 정의하고있다. ‘</p>
<blockquote>
<p><strong>REST(REpresentational State Transfer)</strong> 는 엄격한 의미로 <strong>REST</strong>는 <strong>네트워크 아키텍처</strong> 원리의 모음이다. 여기서 &#39;네트워크 아키텍처 원리&#39;란 자원을 정의하고 자원에 대한 주소를 지정하는 방법 전반을 일컫는다.</p>
</blockquote>
<h1 id="restful의-정의">RESTful의 정의</h1>
<p> 그렇다면, <strong>RESTful</strong>은 무엇일까? 엄밀히 말하면 로이 필딩의 논문에서 정의한 <strong>제약조건</strong>들을 충족한 서비스를 <strong>RESTful</strong>하다고 부를 수 있다. 하지만, 현대에는 <strong>RESTfu</strong>l 용어가 느슨하게 사용된다. 클라이언트는 HTTP 표준 메서드와 URL로 API에 데이터를 요청하고 서버는 <strong>일관된 인터페이스</strong>를 제공한다면 <strong>RESTful</strong>하다고 말할 수 있다. </p>
<p> RESTful의 정의에서 말한 <strong>제약조건</strong>은 위키피디아에서 말한 <strong>네트워크 아키텍쳐 원리</strong>와 동일하다.</p>
<h1 id="rest의-6가지-제약조건">REST의 6가지 제약조건</h1>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/a04b5824-e258-420d-90b6-4d95d20f1670/image.png" alt=""></p>
<h2 id="1-클라이언트-서버-구조client-server">1. 클라이언트-서버 구조(Client-Server)</h2>
<p> 클라이언트와 서버를 나눠 <strong>관심사를 분리</strong>한다. 자원을 제공하는 쪽이 서버, 자원을 요청하는 쪽이 클라이언트다. 클라이언트는 데이터 저장에 신경 쓰지 않는다. 서버는 사용자 인터페이스나 사용자 상태에 신경 쓰지 않는다. 결과적으로, 인터페이스가 변경되지 않는 한 클라이언트와 서버는 <strong>독립적</strong>으로 <strong>교체, 개발, 확장</strong>될 수 있다.</p>
<h2 id="2-무상태성-stateless">2. <strong>무상태성 (Stateless)</strong></h2>
<p>REST의 핵심은 <strong>무상태</strong>이다. 무상태성은 요청을 처리하는 데 필요한 모든 상태(요청을 처리하기 위해 서버가 알아야 하는 정보)는 <strong>요청 자체</strong>에 포함되어야 한다. 무상태성은 이전 요청과 현재 요청이 관계가 없다. 과거와 상호작용하지 않는다.</p>
<p>요청을 처리하는 데 필요한 상태 정보는 URI, 쿼리 파라미터, 바디, 헤더에 담길 수 있다.</p>
<ul>
<li>URI는 리소스를 고유하게 식별하고, 본문은 리소스의 상태(또는 상태 변경)를 포함한다.</li>
<li>서버는 요청 간 세션 상태를 유지하지 않는다.</li>
</ul>
<p>REST에서는 상태를 유지하지 않고 클라이언트가 요청마다 필요한 모든 상태를 포함시키므로 어떤 서버든 클라이언트의 요청을 처리할 수 있다. 여러 대의 서버로 구성된 시스템에서 로드 밸런서가 어떤 서버로 요청을 보내든 상관없으므로 <strong>확장성이 높다.</strong></p>
<p> 서버 구현이 <strong>단순</strong>해지며 특정 서버가 다운되더라도 다른 서버가 문제없이 요청을 처리할 수 있어 <strong>안정성이 높다</strong>. 요청에 필요한 모든 정보가 있으므로 요청의 의미를 파악할 수 있어 테스트, 디버깅에 용이하여 <strong>가시성이 높다.</strong></p>
<p><strong>무상태성의 단점, 트레이드 오프</strong></p>
<ol>
<li>여러 요청에 걸쳐 반복적으로 데이터를 전송하게 된다. 네트워크 오버헤드가 증가한다.       ex) 인증 토큰</li>
<li>애플리케이션 상태가 클라이언트에 놓이게 되어 서버가 일관된 애플리케이션 동작을 제어하기 힘들다. 클라이언트의 버전이 달라질 수 있고 버전에 따라 다른 로직을 수행할 경우가 생길 수 있다.  </li>
</ol>
<h2 id="3-캐시cache">3. 캐시(Cache)</h2>
<p> 네트워크 효율성을 개선하기 위해 클라이언트는 응답을 캐시할 수 있어야 한다. 응답은 응답 데이터가 캐시 가능한지 여부를 나타내야한다.  만약, 응답이 캐시 가능하다면 클라이언트는 캐시를 이용해 응답 데이터를 재사용할 수 있다. 클라이언트-서버 요청/응답 상호작용의 <strong>지연(latency)</strong>를 줄일 수 있다. 캐시는 주로 HTTP GET 메서드 요청에 사용한다.(GET은 멱등하고 리소스의 상태를 변경하지 않기 때문이다.)</p>
<p> 그러나, 트레이드오프 역시 존재한다.  캐시에 오래된 데이터가 남아 있다면 서버로부터 얻을 수 있는 데이터와 다를 수 있어 <strong>신뢰성(reliablility)</strong>이 떨어질 수 있다.</p>
<h2 id="4-통합-인터페이스-uniform-interface">4. 통합 인터페이스 (Uniform Interface)</h2>
<h3 id="4a-자원-식별-resource-identification"><strong>4.a. 자원 식별 (Resource Identification)</strong></h3>
<p>리소스는 URI를 통해 식별한다.  모든 리소스는 고유한 <strong>URI</strong>를 가지며, 클라이언트는 URI를 통해 리소스에 접근한다.</p>
<h3 id="4b-표현을-통한-리소스-조작-manipulation-of-resources-through-representations"><strong>4.b. 표현을 통한 리소스 조작 (Manipulation of Resources Through Representations)</strong></h3>
<p>클라이언트는 실제 리소스가 아닌, 리소스를 나타내는 <strong>JSON/XML</strong> 형식으로 된 표현을 통해 리소스를 조작한다. 서버는 표현을 받아 실제 리소스를 변경한다.</p>
<h3 id="4c-자기-서술적-메시지-self-descriptive-messages"><strong>4.c. 자기 서술적 메시지 (Self-descriptive Messages)</strong></h3>
<p>메시지(요청/응답)는 메시지를 어떻게 처리할지 스스로 설명할 수 있어야한다. 응답은 캐시 가능한지 여부를 명시적으로 포함해야한다.</p>
<p>ex) Content-Type: application/json  ⇒ 이 요청/응답의 본문은 JSON 형식이다.</p>
<h3 id="4d-hateoas-hypermedia-as-the-engine-of-application-state"><strong>4.d. HATEOAS (Hypermedia As The Engine Of Application State)</strong></h3>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/a6066345-d26c-4d09-89ca-36af9555a582/image.png" alt=""></p>
<p>애플리케이션의 상태는 하이퍼미디어(링크)를 통해 전이된다. 서버로부터 받은 응답안에 클라이언트가 할 수 있는 다음 행동이 링크로 포함되어있는 걸 말한다. </p>
<p>ex) 게시글 상세 조회 요청시 응답 예시</p>
<pre><code>{
  &quot;id&quot;: &quot;1&quot;,
  &quot;title&quot;: &quot;REST API 설명&quot;,
  &quot;content&quot;: &quot;...&quot;,
  &quot;_links&quot;: {
    &quot;self&quot;: { &quot;href&quot;: &quot;/posts/1&quot; },
    &quot;comments&quot;: { &quot;href&quot;: &quot;/posts/1/comments&quot; },
    &quot;edit&quot;: { &quot;href&quot;: &quot;/posts/1&quot;, &quot;method&quot;: &quot;PUT&quot; },
    &quot;delete&quot;: { &quot;href&quot;: &quot;/posts/1&quot;, &quot;method&quot;: &quot;DELETE&quot; }
  }
}</code></pre><h2 id="5-계층화-시스템layered-system">5. 계층화 시스템(Layered System)</h2>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/b6ad62b6-c983-4211-adcf-331ed040869c/image.png" alt=""></p>
<p> 클라이언트는 자원을 요청할 때 어떤 서버와 통신하는지 알 수 없어야 한다. 클라이언트는 최종 서버에 연결된 것인지 중간 서버를 거친 것인지 알 수 없다.  중간 서버는 프록시, 로드 밸런서, CDN 등이 올 수 있다.  계층 시스템은 여러 계층을 거쳐서 통신하기 때문에 데이터 처리 과정에 <strong>오버헤드가 발생</strong>한다. 하지만 캐시 제약 조건을 지원하는 네트워크 시스템에서는 중개자에 공유 캐시를 두어 이 단점을 상쇄할 수 있다.</p>
<h2 id="6-주문형-코드code-on-demand-optional">6. 주문형 코드(Code on Demand, Optional)</h2>
<p>서버는 클라이언트에 코드를 전달하여 기능을 확장할 수 있다. 서버는 javascript 코드를 전달하고 클라이언트 측에서 스크립트를 실행할 수 있다. 이 제약조건은 의무가 아니며 선택적으로 적용할 수 있다.</p>
<h1 id="rest를-통해-얻을-수-있는-것">REST를 통해 얻을 수 있는 것</h1>
<p> 이렇게 어려운 REST 제약조건을 잘 지켜 RESTful한 서버를 만들었다. 그렇다면 REST를 통해 어떤 이점을 누릴 수 있을까? 로이 핃딩의 논문에서는 다음과 같이 말하고 있다.</p>
<p>컴포넌트는 독립적으로 동작하며 인터페이스를 통해 다른 요소와 상호작용하는 단위이다. </p>
<ul>
<li>컴포넌트 간 상호작용의 <strong>확장성(</strong>scalability)</li>
<li>인터페이스의 <strong>일반성</strong>(generality of interfaces)</li>
<li>컴포넌트의 <strong>독립적인 배포</strong>(independent deployment)</li>
<li><strong>중개자</strong>(intermediary components)를 활용한 <strong>상호작용 지연(latency) 감소</strong>, <strong>보안 강화(security enforcement</strong>), <strong>레거시 시스템 캡슐화(encapsulation of legacy systems)</strong></li>
</ul>
<h1 id="느낀점">느낀점</h1>
<p> 제대로 공부하기 전엔 REST를 좋은 API 설계를 위한 원칙 정도로 알고 있었다. 하지만, REST 아키텍처의 논문을 기반으로 공부해보니 API 설계 뿐만이 아닌 네트워크 전반적인 설계 철학을 배울 수 있었다. 특히, 로이 필딩은 REST 뿐만이 아닌 HTTP 스펙도 개발했다고 한다. 그래서 개념적으로 HTTP와 REST의 연결되는 부분을 서로서로 더 잘 이해하게 된 것 같다. 다만, 엄밀히 말해 RESTful이라는 것은 위의 6가지(Code on Demand는 선택적) 제약조건을 지킨 것이다. HEATEOAS 제약조건을 처음 알았을 때 충격적이였다. 한번도 개발해본 경험이 없고 들은적도 없어 나에게 새로운 개발 패러다임이였다. 현대 빅테크 기업들의 서버들도 HATEOAS를 잘지키고 있는지 어떤식으로 개발하는지 궁금하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ CSR/SSR 그리고 MPA/SPA 이해하기]]></title>
            <link>https://velog.io/@young_gyu/MPASPA-%EA%B7%B8%EB%A6%AC%EA%B3%A0-CSRSSR</link>
            <guid>https://velog.io/@young_gyu/MPASPA-%EA%B7%B8%EB%A6%AC%EA%B3%A0-CSRSSR</guid>
            <pubDate>Tue, 26 Aug 2025 12:00:51 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>MPA/SPA 과 CSR/SSR에 대해 헷갈리는 것들이 많았다. 공부했던 내용이라 생각했지만 이후에 CSR, SSR 글을 보면 개념이 뒤섞일 때가 많았다. 공부하는 과정에서 발산한 생각을 구조화한 글로 수렴하도록 만들고자 한다.</p>
</blockquote>
<h2 id="렌더링">렌더링</h2>
<p>CSR/SSR을 공부하기 전에 렌더링이 무엇인지 알아볼 필요가 있다. 백엔드를 공부하는 나의 관점에서 렌더링은 브라우저의 렌더링이라 생각했다. 하지만, CSR/SSR에서 말하는 렌더링은 브라우저의 렌더링과는 사뭇 달랐다. 브라우저 렌더링을 설명하는 것은 글의 요지를 해칠 수 있지만 공부했던 과정이므로 간략하게 설명하겠다. </p>
<h3 id="브라우저-렌더링-과정">브라우저 렌더링 과정</h3>
<p>브라우저 렌더링은 아래의 과정으로 이루어진다.</p>
<ol>
<li>DOM Tree 생성 - HTML을 파싱하고 객체로 만들어(노드화) DOM(Document Object Model)을 생성한 다. </li>
<li>CSSOM Tree 생성 - DOM Tree를 만드는 과정과 유사하게 CSS 텍스트를 파싱하고 스타일 시트 객체로 만들어 CSSOM(CSS Object Model)을 생성한다.</li>
<li>Rendering Tree 구성 - Dom Tree와 CSSOM Tree로 화면에 표시할 노드만 포함하여 렌더링 트리를 만든다. 이때, display: none과 같은 속성을 가진 요소들은 제거된다.</li>
<li>레이아웃 - 렌더 트리를 기반으로 각 요소의 크기, 위치, 간격 등을 계산하여 화면에 어디에 위치할지 결정한다.</li>
<li>패인트 - 레이아웃 단계의 정보를 바탕으로 요소들을 화면의 실제 픽셀로 변환한다. </li>
</ol>
<h3 id="csrssr이-말하는-렌더링은-무엇인가">CSR/SSR이 말하는 렌더링은 무엇인가</h3>
<p> 앞서 말했듯이 CSR/SSR의 렌더링은 브라우저의 렌더링과 다르다. 그렇다면, CSR/SSR이 말하는 렌더링은 무엇인가? 나는 렌더링을 <strong>‘사용자에게 보이는 화면(UI)을 만드는 행위’</strong>로 이해했다. CSR/SSR의 차이를 알아보기 전에 렌더링이 무엇인지 생각하는 것은 전체적인 과정을 이해하는데 중요하다고 생각한다. </p>
<p> CSR(Client Side Rendering)은 <strong>‘클라이언트에서 UI를 만든다’</strong>, SSR(Server Side Rendering)은 <strong>‘서버에서 UI를 만든다’</strong>로 큰 틀을 잡고 시작하기 위해 렌더링을 설명했다. </p>
<h2 id="csrclient-side-rendering">CSR(Client Side Rendering)</h2>
<h3 id="csr은-무엇인가">CSR은 무엇인가</h3>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/3ac79d74-c054-49f9-96f9-cab116954cfd/image.png" alt=""></p>
<p>CSR은 클라이언트가 JS로 DOM 생성에 관여하여 화면을 그리는 방식이다.</p>
<p>CSR은 아래의 과정을 거친다.</p>
<ol>
<li>클라이언트는 페이지 요청시 서버로부터 최소한의 HTML을 받는다.</li>
<li>HTML을 파싱한다.</li>
<li>JS를 다운받고 백엔드 서버의 API로 데이터를 요청한다.</li>
<li>데이터(JSON/XML)을 받으면 브라우저의 DOM API를 이용해 DOM 트리를 구성한다</li>
</ol>
<h3 id="csr의-장단점">CSR의 장단점</h3>
<ul>
<li>CSR은 HTML을 받고 API로 데이터를 또 다시 요청하기 때문에 초기 응답이 느리다.</li>
<li>HTML 태그의 값이 비어있기 때문에 SEO에 불리하다.</li>
<li>화면을 변경할 땐 필요한 데이터만 가져오므로 빠르게 느껴지고 서버 부하가 감소한다.</li>
</ul>
<blockquote>
<p>구글의 크롤러는 JS를 실행할 수 있어서 구글에서 SEO는 그나마 낫다고 한다.</p>
</blockquote>
<h3 id="csr의-예시">CSR의 예시</h3>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/e89e4609-faa4-417b-ab90-0391a4f25087/image.png" alt=""></p>
<p> CSR은 SNS 서비스의 피드를 떠올리면 된다. 최소한의 HTML을 받기 때문에 SNS 게시물은 처음에 화면에 표시되지 않는다. Javascript를 실행하고 API에서 게시물 데이터를 받아와야 화면에 표시되기 시작한다. 이후 무한스크롤과 같은 기능으로 게시물을 넘기다보면 페이지를 다시 로드하지않고 추가적인 게시물들을 볼 수 있다. </p>
<h2 id="ssrserver-side-rendering">SSR(Server Side Rendering)</h2>
<h3 id="ssr은-무엇인가">SSR은 무엇인가</h3>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/e78e0b58-be9e-4c93-8090-7a6596e9dfec/image.png" alt=""></p>
<p>SSR은 클라이언트가 서버로부터 완성된 HTML을  받아 화면에 그리는 방식이다. 서버는 템플릿 엔진(JSP,EJS)의 도움으로 동적인 데이터를 HTML에 삽입할 수 있다. </p>
<p>SSR은 아래의 과정을 거친다.</p>
<ol>
<li>클라이언트는 페이지를 요청한다.</li>
<li>서버는 DB를 조회하여 게시글 데이터를 불러온다.</li>
<li>서버는 불러온 데이터를 템플릿 엔진의 도움으로 HTML을 완성한다.</li>
<li>브라우저는 완성된 HTML을 받아 바로 DOM  트리를 구성하고 화면에 그린다.</li>
</ol>
<h3 id="ssr의-장단점">SSR의 장단점</h3>
<ul>
<li>SSR은 서버에서 HTML을 완성해야하기 때문에 서버의 부하가 증가한다.</li>
<li>SEO에 유리하고 클라이언트는 다시 데이터를 요청할 필요가 없어 빠른 초기 로딩이 가능하다.</li>
<li>새로운 요청이 있을 때마다 서버에서 HTML을 다시 만들어 보내주기 때문에, 화면이 깜박이고 UX는 떨어진다.</li>
</ul>
<h3 id="ssr의-예시">SSR의 예시</h3>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/68037ed8-fcac-4a5f-969d-d56f2b83e94e/image.png" alt=""></p>
<p>이커머스 웹사이트를 생각하면 된다. 서버는 제품 정보 데이터를 HTML에 완성하여 클라이언트에 보낸다. 클라이언트는 컴퓨터 성능, 네트워크 환경에 덜 영향을 받으면서 전체 페이지를 즉시 볼 수 있다.</p>
<h2 id="mpa">MPA</h2>
<p>MPA는 여러 페이지로 구성된 웹 애플리케이션을 말한다.</p>
<p>각 페이지마다 별도의 URL, HTML을 가지고 있어 SEO에 유리하다. 페이지 이동시 전체 페이지를 새로 요청한다. </p>
<h3 id="장단점">장단점</h3>
<ul>
<li>사용자는 웹 사이트의 특정 부분을 공유해서 볼 수 있다. 예) 쿠팡의 특정 상품을 카카오톡에 공유한다.</li>
<li>페이지 전환마다 화면이 깜빡여 UX가 떨어진다.</li>
</ul>
<h2 id="spa">SPA</h2>
<p>SPA는 하나의 페이지로 구성된 웹 애플리케이션을 말한다. 이후의 페이지 이동은 클라이언트가 JS로 DOM을 변경하고 특정 영역을 업데이트하는 식으로 진행한다. 최초 페이지를 로드할 땐 index.html만 받고, 이후에는 데이터(JSON)을 서버와 주고 받는다.</p>
<h3 id="장단점-1">장단점</h3>
<ul>
<li>페이지를 새로고침 하지않고 콘텐츠를 업데이트한다. 그래서, 애플리케이션의 반응성이 뛰어나고 사용자는 매끄러운 인터랙션을 경험할 수 있다.</li>
<li>SEO가 불리하고 초기에 많은 리소스들을 다운로드하므로 초기 로딩이 느리다.</li>
</ul>
<h2 id="현대의-ssr">현대의 SSR</h2>
<p>  웹은 비즈니스를 이루기 위해 만든다. 검색 엔진 상위에 표시되지 않는다면 비즈니스에 큰 타격을 줄 것이다. SSR의 장점은 서버에서 완성된 HTML을 내려주어 SEO가 최적화되는 것이다.</p>
<p> 이 장점을 ‘SPA + CSR’ 구조에서 이용하기 위해 SSR을 일부 사용한다. 첫 메인 페이지는 SSR로 내려받아 SEO와 초기 로딩 속도를 높인다. 이후에 사용자 인터랙션, 페이지 전환시 ‘SPA + CSR’ 을 사용하여 부드럽게 동작하게 만든다. 대표적으로 ‘next.js’ 프레임워크가 있다.</p>
<h3 id="mpa와-ssr-spa와-csr의-관계"><strong>MPA와 SSR, SPA와 CSR의 관계</strong></h3>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/901a8f37-6c29-48f5-82bd-588b7062687a/image.png" alt=""></p>
<p> MPA = SSR, SPA = CSR이라고 단순히 생각하기 쉽다. 하지만 이것들은 서로 다른 개념이다.</p>
<blockquote>
<ul>
<li>MPA vs SPA:<ul>
<li>MPA는 페이지 구조의 차이 (여러 HTML 페이지 vs 하나의 HTML 페이지).</li>
<li>SPA는 하나의 HTML만 사용하고, URL 변경 시에도 JS로 화면 일부만 바꾼다.</li>
</ul>
</li>
</ul>
</blockquote>
<ul>
<li><p>SSR vs CSR:</p>
<ul>
<li>SSR/CSR은 렌더링 시점과 위치의 차이 (서버에서 HTML 완성 vs 클라이언트에서 JS로 HTML 완성).</li>
<li>즉, SSR/CSR은 “누가 화면을 완성하는가”에 관한 이야기다.</li>
</ul>
<p>따라서 MPA는 보통 SSR과 함께 쓰였고, SPA는 보통 CSR을 사용했지만, SPA+SSR, MPA+CSR 같은 조합도 가능하다. 예를 들어 Next.js는 SPA 구조를 가지면서도 SSR/SSG를 지원하여 SEO 문제를 해결한다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Publish-Subscribe, Observer의 차이]]></title>
            <link>https://velog.io/@young_gyu/Publish-Subscribe-Observer%EC%9D%98-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@young_gyu/Publish-Subscribe-Observer%EC%9D%98-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Sat, 02 Aug 2025 13:29:49 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/young_gyu/post/3a08324f-14b0-4a71-87fc-65a3927b1a1b/image.png" alt=""></p>
<h1 id="observer-패턴">Observer 패턴</h1>
<p>Observer패턴은 어떤 객체의 상태가 변하면(이벤트 발생) 이를 의존하고 있는 다른 객체들에게 알림을 전달하는 패턴입니다.</p>
<p>예를 들어, 뉴스 구독 시스템이 있습니다. 뉴스가 신규 발행되면 사용자에게 알림 메시지를 전송하는 시스템 입니다.</p>
<ul>
<li>뉴스(Subject)는 새로운 뉴스를 발행한다.</li>
<li>사용자(Observer)는 뉴스를 구독하고 새로운 뉴스가 발행되면 알림을 받는다.</li>
</ul>
<p>Subject는 자신을 구독한 Observer를 직접 알고 있습니다. 이벤트가 발생하면 직접 Observer를 호출하여 알림을 보냅니다. 이 구조는 강한 결합(tightly coupled) 관계입니다.</p>
<p>하지만, Subject는 Observer에게 알림만 보낼 뿐,  Subject는 그 내부 동작에는 관심이 없습니다. 즉, 동기적으로 알림을 보내고 로직은 Observer에 위임합니다.</p>
<p>Observer 패턴의 예시로 프론트엔드에서 버튼 클릭 이벤트가 대표적입니다. 버튼은 Subject, 콜백함수는 Observer 역할을 합니다. 이처럼 Observer 패턴은 주로 단일 애플리케이션 내에서 동기적으로 작동합니다.</p>
<h1 id="publish-subscribe-패턴">Publish-Subscribe 패턴</h1>
<p>Pub-Sub 패턴은 생산자(Publisher)와 소비자(Subscriber) 사이에 중개자를 두는 구조입니다. Publisher는 메시지를 직접 Subscriber에게 보내지 않고, 중개자에게 발행(publish) 합니다.Subscriber는 중개자로부터 메시지를 수신(subscribe)하는 구조입니다.</p>
<p>이 구조에서 중개자 덕분에 Publisher와 Subscriber는 서로의 존재를 알지 못합니다. 느슨한 결합도(loose coupling)를 가지며 시스템의 확장성이 좋아 Publisher, Subscriber가 늘어나도 문제될 것이 없습니다.</p>
<p>RabbitMQ,Kafka와 같은 메시지 브로커가 이 구조를 따릅니다. MSA환경의 Kafka에서 한 서버가 이벤트를 발행하면 다른 서버는 이벤트를 구독하여 처리합니다. 이때 서버 간에 직접적인 연결은 없습니다. Pub-Sub는 발행자, 구독자간 서로 존재를 알지 못하므로 주로 비동기로 동작합니다.</p>
<h2 id="마무리">마무리</h2>
<p>Observer 패턴과 Pub-Sub 패턴은 이벤트 기반 아키텍쳐를 설계할 때 많이 사용됩니다. 하지만, 결합도와 동작 방식이 다르기 때문에 선호하는 상황 역시 다릅니다. 내부 모듈 간 통신에는 Observer, 서로 다른 모듈 간 독립성과 확장성이 중요한 시스템에는 Pub-Sub 구조를 사용하는 것이 좋습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JWT를 이용한 동일 계정 다중 디바이스 로그인 유지 방법]]></title>
            <link>https://velog.io/@young_gyu/JWT%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%8F%99%EC%9D%BC-%EA%B3%84%EC%A0%95-%EB%8B%A4%EC%A4%91-%EB%94%94%EB%B0%94%EC%9D%B4%EC%8A%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9C%A0%EC%A7%80-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@young_gyu/JWT%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%8F%99%EC%9D%BC-%EA%B3%84%EC%A0%95-%EB%8B%A4%EC%A4%91-%EB%94%94%EB%B0%94%EC%9D%B4%EC%8A%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9C%A0%EC%A7%80-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Wed, 09 Oct 2024 04:53:53 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-배경">문제 배경</h2>
<p> 서비스의 회원 로그인, 세션 유지 방법으로 JWT를 사용한다면 refresh 토큰을 사용하여 access, refresh 토큰을 재발급 받아 세션 유지를 사용할 것이다. 기본적으로 client는 access 토큰이 더 이상 유효하지 않다는 응답을 server로 부터 받으면 server에게 refresh 토큰을 보내고 server는 해당 refresh 토큰을 검증하고 회원을 식별한 뒤 aceess, refresh 토큰을 재발급하여 client에게 전달하게 된다. (단, 보안상의 이유로 검증하는 과정에서 sever의 DB에 refresh 토큰을 저장하여 관리하고 한 번 사용한 refresh 토큰은 DB에서 제거한다.)</p>
<p> 우리 서비스(I LUV BOOK)은 웹, 모바일 앱과 같은 다른 환경에서 같은 계정을 통해 로그인할 수 있고 동시에 사용할 수 있어야 하는 요구 사항이 있다. I LUV BOOK은 생성형 AI를 통한 영어 동화 교육 플랫폼이다. <strong>부모 계정을 통해 로그인하고 아이들의 기기에서 자유롭게 사용</strong>해야 하기 때문에 해당 요구사항이 만들어졌다.</p>
<p>그럼 앞서 말한 기본적인 JWT의 세션 유지 방식으로 I LUV BOOK의 로그인 유지에 관한 요구사항을 만족시킬 수 있는지 시나리오를 생각해보자.</p>
<ol>
<li>첫 째 아이와 둘 째 아이가 모두 같은 계정으로 I LUV BOOK을 동시에 사용중이다.</li>
<li>첫 째 아이는 웹으로 I LUV BOOK을 접속하였고 서비스를 사용 후 I LUV BOOK에서 로그아웃 버튼을 눌렀다.</li>
<li>둘 째 아이는 앱을 계속 사용하다가 I LUV BOOK 서버로 부터 Acees token이 만료 되었다는 응답을 받았다.</li>
<li>둘 째 아이는 Refresh Token으로 토큰을 갱신하려고 했지만. 리프레시 토큰이 유효하지 않다는 응답을 받았다. 둘 째 아이는 앱을 계속 사용하기 위해 다시 로그인해야하는 불편함이 생겼다.</li>
</ol>
<h2 id="문제-정의">문제 정의</h2>
<p>이런 문제점이 왜 생긴 것일까 ?🤔🤔</p>
<p>서버는 토큰 재발급 과정에서 본인이 서명한 refresh 토큰인지 확인하고 DB에 해당 refresh 토큰이 있는지 확인한다. 하지만, 첫 째 아이가 웹에서 로그아웃 버튼을 눌렀기 때문에 refresh 토큰이 파기되어 둘 째 아이는 더 이상 refresh 토큰이 유효하지 않다는 응답을 받게 되는 것이다.</p>
<p>어떻게 이 문제를 해결할 수 있을까 ?🤔🤔</p>
<p>기본적인 구현에서는 디바이스 별로 독립적인 토큰 관리가 이루어지는 것이 아니라 계정 단위로 토큰의 관리가 이루어지기 때문에 디바이스간 간섭을 하는 경우가 생김을 알 수 있다. 즉, 계정 단위의 토큰 관리를 디바이스 단위로 토큰을 관리 할 수 있도록 바꿔주면 된다.</p>
<h2 id="문제-해결-방법">문제 해결 방법</h2>
<p> 나의 경우는 디바이스 단위로 토큰을 관리하기 위해서는 디바이스간 차별적인 어떤 값이 있으면 그걸 이용하면 좋겠다고 생각했다. 처음 생각한 아이디어는 디바이스의 IP 주소를 이용하면 어떨까 생각을 했다. Controller 단에서 HTTP header의 IP 주소를 빼내어 토큰 생성을 해도 괜찮을 것 같았다. 하지만, 머지 않아 IP 주소를 활용하고싶은 생각은 접게 됐다. 나의 네트워크 CS 지식으로는 완벽하게 디바이스를 구별하는 것이 불가능해 보였기 때문이다. </p>
<pre><code>만약, 같은 가정의 두 아이가 같은 공유기를 통해 I LUV BOOK 서비스를 이용한다면 NAT에 의해 하나의 공인 IP로 서버에 요청을 보낼 것이기 때문에 두 아이 모두 같은 IP 주소일 것이므로 디바이스를 구별할 수 없다고 생각했다.</code></pre><p>결론적으로, 나는 디바이스를 식별하기 위해 독립적인 고유 식별 번호를 토큰에 같이 말아주기로 했다. 토큰을 생성할 때 uuid를 생성하여 uuid 값을 access, refresh 토큰에 같이 말아 넣어 생성하였다. 이렇게하면 사용자가 로그아웃시 헤더에 있는 access 토큰에서 uuid를 꺼내 refershtoken을 저장한 db에서 해당 acccess 토큰과 동일한 uuid로 매핑된 refresh 토큰을 삭제할 수 있게 된다. 사실 이것이 최선의 방법인지는 잘 모르겠다. 하지만, 여러 고민끝에 나온 지금의 방법이다.</p>
<h2 id="기대-효과">기대 효과</h2>
<p>UUID를 통한 다중 디바이스 로그인을 구현으로 얻을 수 있는 기대효과는 다음과 같다.</p>
<ol>
<li>보안성 강화 </li>
</ol>
<p>토큰 탈취가 의심되는 access 토큰의 uuid를 읽어 동일한 uuid를 가진 refresh 토큰을 DB에 삭제하는 등 보안의 이점이 있다.</p>
<ol>
<li>확장성과 추적</li>
</ol>
<p>uuid와 같이 디바이스 타입(모바일,웹), 로그인 위치와 같은 정보들을 추가하여 테이블로 관리해 디바이스별 로그인 기록을 관리, 분석할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[재귀함수, 꼬리재귀 그리고 재귀함수의 비용]]></title>
            <link>https://velog.io/@young_gyu/%EC%9E%AC%EA%B7%80%ED%95%A8%EC%88%98-%EA%BC%AC%EB%A6%AC%EC%9E%AC%EA%B7%80-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%9E%AC%EA%B7%80%ED%95%A8%EC%88%98%EC%9D%98-%EB%B9%84%EC%9A%A9</link>
            <guid>https://velog.io/@young_gyu/%EC%9E%AC%EA%B7%80%ED%95%A8%EC%88%98-%EA%BC%AC%EB%A6%AC%EC%9E%AC%EA%B7%80-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%9E%AC%EA%B7%80%ED%95%A8%EC%88%98%EC%9D%98-%EB%B9%84%EC%9A%A9</guid>
            <pubDate>Tue, 26 Mar 2024 07:26:49 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p> 재귀 함수는 처음에 코딩 공부를 할 때 벽과 같은 존재였다. 디버깅이 약했던 과거의 나는 내가 짠 몇 줄 안되는 코드에서 도대체 무슨 일이 일어나는지 알 수가 없었다. </p>
<p> 재귀 함수와 나는 개인적인 인연이 있다. 대학교 입학 준비 당시 면접에서 면접관님이 나한테 “재귀 함수가 뭐냐?” 라고 물었던 것이 5년이 지난 아직까지 기억이 생생하게 난다. 그때의 나는 … “함수가 실행되었을 때 자기 자신을 도출하는 함수입니다.” 라고 답변했고. 면접관님이 도출이란 표현을 호출로 정정해야 한다고 코멘트를 주셨는데 그때 당시 부끄러운 기억이  아직까지난다. </p>
<p> 백트래킹 및 CS를 공부한 지금은 재귀 함수가 뭔 놈인지 이제서야 알게 되었다. 내가 지금 알고 있는 재귀 함수에 관한 내용을 소개하려고 한다.  </p>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/63a7d620-da09-44a4-87d1-8666d288e0dd/image.png" alt=""></p>
<pre><code>                           (엘리베이터에서의 재귀 함수)</code></pre><h2 id="재귀-함수란-무엇인가-">재귀 함수란 무엇인가 ?</h2>
<h3 id="재귀-함수의-정의">재귀 함수의 정의</h3>
<p><strong>재귀 함수(recursive)</strong>는 함수 안에서 자기 자신을 호출하는 함수이다.</p>
<p> 자기 자신을 계속해서 호출하기 때문에 재귀의 기저 조건(탈출 조건)이 없다면 탈출할 수 없다. 반드시 재귀 함수를 정의할 때는 기저 조건을 완벽하게 세워야 한다. 만약 재귀 함수가 무한 루프를 돈다면 기저 조건을 올바르게 세우지 못한 것이다.</p>
<h3 id="재귀-함수의-예">재귀 함수의 예</h3>
<pre><code>def fibo(n):

    if n == 1:
        return 1

    if n== 2:
        return 1

    return fibo(n-1) + fibo(n-2)

print(fibo(5))
print(fibo(10))</code></pre><p> 피보나치수열은 재귀 함수의 대표적인 예시이다. 피보나치수열은 n 번째 수는 n-1 번째 수와 n-2 번째 수의 합임을 정의로 가지는 수열이다. 피보나치수열의 첫 번째 수와 두 번째 수는 1이므로 기저 조건으로 설정해주고 피보나치수열의 정의를 그대로 리턴문에 옮겨주면 구현이 끝이난다. </p>
<h3 id="재귀-함수의-장점">재귀 함수의 장점</h3>
<p> 재귀 함수는 특정한 문제 및 자료구조(트리)에서 굉장히 쉽게 코드를 짤 수 있도록 도와준다. 아무래도 함수의 재사용성을 굉장히 높여 문제를 풀기 때문에 코드가 간결할 수 밖에 없다. 또한 하노이의 탑, 순열, 피보나치와 같은 문제 자체가 재귀적인 문제들을 해결할 때도 매우 쉽게 코드를 구현할 수 있다. 즉, 짧지만 매우 강력한 코드를 완성할 수 있다.</p>
<p> 또한 재귀 함수로 문제를 해결한다는 것은 반복적인 코드로 전체 문제를 해결한다는 것이다. 그 말은 문제를 작은 문제로 분해하여 큰 문제를 해결한다는 것으로 생각할 수 있다. 큰 문제를 작은 문제로 나누었기 때문에 코드 가독과 문제 풀이가 쉬워질 수 있다.</p>
<h3 id="재귀-함수의-단점">재귀 함수의 단점</h3>
<p> 아이러니하게도 위의 장점이 단점이 될 수도 있다. 재귀 함수가 익숙하지 않은 초보 프로그래머 입장에서 재사용성을 높여 코드가 간결하기 때문에 디버깅 시 재귀 함수의 깊이에 따른 스택 값(변수 값)을 추적하기 어려울 수 밖에 없다. 또한 알맞은 기저 조건을 세우기 역시 힘들 것이다.</p>
<p> 시간, 메모리 자원의 입장에서도 손해이다. 함수를 사용했을 때 드는 비용을 생각해 보자. 함수의 비용으로는 대표적으로 컨텍스트 스위칭이 있을 것이다. </p>
<blockquote>
<p>컨텍스트 스위칭은 다음 작업을 하기 위해 현재의 작업을 중지하고 작업 내용을 저장해놨다가 다음 작업이 끝났을 때 저장해놓았던 현재의 작업 내용을 다시 불러오는 것을 의미한다.</p>
</blockquote>
<p> 재귀 함수에서의 컨텍스트 스위칭 비용은 일반 함수의 비용보다 훨씬 크다고 말할 수 있다. 함수는 함수의 내용물을 완전히 실행하고 return시 할당받은 메모리를 반환한다. 재귀 함수는 함수가 다 끝나기 전에 새로 함수를 다시 시작하므로 기저 조건을 만족할 때 까지 메모리를 계속 차지하게 된다.</p>
<h2 id="재귀-함수의-비용을-줄이는-방법">재귀 함수의 비용을 줄이는 방법</h2>
<h3 id="꼬리-재귀">꼬리 재귀</h3>
<p> 코드의 가독과 간결함의 장점과 스택 메모리와 시간을 많이 소모하는 단점을 가진 재귀함수의 단점을 극복할 수 있는 방법이 있다. 바로 ‘<strong>꼬리 재귀’(tail recursion)</strong>로 불리는 재귀 함수의 최적화 방법이다.</p>
<p> 꼬리 재귀의 원리는 알고 보면 간단하다. 컨텍스트 스위칭을 해야 하는 이유가 무엇인가 🤔? 새로운 함수를 호출하고 내가 다음 작업들을 이어서 해야 하기 때문에 이전 작업 정보를 저장하는 것이다. 그러면 다음 작업이 없다면 컨텍스트 스위칭을 안 해도 되는 것으로 생각할 수 있다. 말이 쉽지 꼬리 재귀로 코드 짜는 것은 쉽지 않은 것 같다.</p>
<pre><code>def fibo(n, cur=0, next=1):
    if n == 0:
        return cur

    return fibo(n-1, next, cur+next)

print(fibo(5))
print(fibo(10))</code></pre><p> 피보나치수열을 꼬리 재귀로 나타낸 코드이다. 꼬리 재귀에서 가장 중요한 점은 반드시 return 부분에 재귀문을 실행해야 하며 ‘+’와 같은 다른 연산은 없어야 한다. 다른 연산을 실행한다면 위에 말했던 아직 끝내지 못한 작업을 끝내기 위해 컨텍스트 스위칭을 시도할 것이다 😢😢😢</p>
<h3 id="꼬리-재귀가-불가능한-경우">꼬리 재귀가 불가능한 경우</h3>
<p> 꼬리 재귀는 언어에 따라 사용하지 못할 수 있다. 꼬리 재귀는 코드를 어떻게 짜도 결국 컴파일러에 의존하게 된다. 슬프게도 언어에 따라 컴파일러가 다르므로 언어마다 꼬리 재귀의 지원 여부가 다르다. 사실 위의 피보나치수열을 꼬리 재귀로 만든 코드도 꼬리 재귀의 형태를 가지고 있지만 꼬리 재귀를 실행하지는 않는다😟… Python, JAVA 및 많은 프로그래밍 언어들 꼬리 재귀를 지원하지 않는다.</p>
<h2 id="결론">결론</h2>
<p>재귀 함수는 컨텍스트 스위칭 때문에 엄청난 비용이 든다. 이를 줄이기 위한 꼬리 재귀라는 최적화 방법이 있지만 언어마다 꼬리 재귀를 지원해 주지 않을 수 있다. 재귀 함수를 사용한 알고리즘 문제 풀이시 시간 초과가 난다면 정답이 될 수 있는 부분만 탐색하도록 가지 치기를 생각해 보자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] 백준 6987) 올림픽]]></title>
            <link>https://velog.io/@young_gyu/Python-%EB%B0%B1%EC%A4%80-6987-%EC%98%AC%EB%A6%BC%ED%94%BD</link>
            <guid>https://velog.io/@young_gyu/Python-%EB%B0%B1%EC%A4%80-6987-%EC%98%AC%EB%A6%BC%ED%94%BD</guid>
            <pubDate>Mon, 25 Mar 2024 11:57:38 GMT</pubDate>
            <description><![CDATA[<h2 id="🔗문제-링크">🔗문제 링크</h2>
<p><a href="https://www.acmicpc.net/problem/6987">6987번: 월드컵</a>
<img src="https://velog.velcdn.com/images/young_gyu/post/670695bb-b44f-41fd-ab3e-b4d1f05042b1/image.png" alt=""></p>
<h2 id="✅-문제-유형">✅ 문제 유형</h2>
<p>백트래킹, 브루트포스 </p>
<h2 id="🤔-문제-풀이">🤔 문제 풀이</h2>
<p> 6개의 팀이 축구 조별 예선 과정에서 승,무,패를 획득할 수 있는 경우의 수를 찾는 문제입니다. 6개의 팀이 모두 한 경기씩 치르기 때문에 조합을 생각하여 6C2 = 15 로 한 조에 치러지는 모든 경기의 수를 알 수 있습니다. </p>
<p> 한 경기당 3개의 결과(승, 무, 패)가 나타날 수 있습니다. 한 경기당 3개 총 경기는 15개이므로 시간 복잡도는 3^15 로 표현할 수 있습니다. 3^15 는 14,000,000이므로 1초에 코드가 1억 번 돌아간다고 생각하면 코드를 잘 짠다면 충분히 돌아갈 것으로 예상할 수 있습니다.</p>
<p> 저는 모든 경기의 경우의 수를 완전 탐색하여 <strong>result</strong> 딕셔너리에 기록하였습니다. 그렇기 때문에 입력된 경기 내용이 result 딕셔너리에 있다면 1을 출력하고 딕셔너리에 경기 내용이 없다면 0을 출력하는 코드를 설계했습니다. </p>
<p> 모든 경기의 경우의 수 탐색은 완전 탐색 기법의 하나인 백트래킹으로 구현하였습니다. 백트래킹 코드는 아래와 같습니다. 현재 경기의 결과를 note 리스트의 위치에 알맞게 수정합니다. 그리고 재귀 함수를 통해 다음 대진표의 경기를 진행시킵니다. </p>
<pre><code>
---
Input  
a,b : 경기를 치를 두 팀  
status : 승,무,패를 결정
note : 현재까지 기록한 경기 기록
depth : 현재까지 진행한 경기의 수
---

def match(a,b,status,note,depth) :

# 현재 경기 기록

    if status == 0: #승
        aIDX = position[a]
        bIDX = position[b]
        note[aIDX] += 1
        note[bIDX+2] += 1
        if answer[aIDX] &lt; note[aIDX] or answer[bIDX+2] &lt; note[bIDX+2] :
            return

    elif status == 1: #무
        aIDX = position[a]
        bIDX = position[b]
        note[aIDX+1] += 1
        note[bIDX+1] += 1

        if answer[aIDX+1] &lt; note[aIDX+1] or answer[bIDX+1] &lt; note[bIDX+1] :
            return

    elif status == 2:  # 패
        aIDX = position[a]
        bIDX = position[b]
        note[aIDX + 2] += 1
        note[bIDX] += 1

        if answer[aIDX+2] &lt; note[aIDX+2] or answer[bIDX] &lt; note[bIDX] :
            return

    if depth == 15: #15경기를 모두 진행했다면
        result[str(note)] = 1
        return

    nextA ,nextB = allMatch[depth] #다음 경기의 두 상대

    match(nextA,nextB,0,note[:],depth+1) #다음 경기로 재귀
    match(nextA, nextB, 1, note[:], depth + 1) 
    match(nextA, nextB, 2, note[:], depth + 1)</code></pre><h2 id="🚀-문제-해결">🚀 문제 해결</h2>
<pre><code class="language-jsx"># 트러블 슈팅 이전의 코드
def match(a,b,status,note,depth) :

# 현재 경기 기록

    if status == 0: #승
        aIDX = position[a]
        bIDX = position[b]
        note[aIDX] += 1
        note[bIDX+2] += 1

    elif status == 1: #무
        aIDX = position[a]
        bIDX = position[b]
        note[aIDX+1] += 1
        note[bIDX+1] += 1

    elif status == 2:  # 패
        aIDX = position[a]
        bIDX = position[b]
        note[aIDX + 2] += 1
        note[bIDX] += 1

    if depth == 15: #15경기를 모두 진행했다면
        result[str(note)] = 1
        return

    nextA ,nextB = allMatch[depth] #다음 경기의 두 상대

    match(nextA,nextB,0,note[:],depth+1) #다음 경기로 재귀
    match(nextA, nextB, 1, note[:], depth + 1) 
    match(nextA, nextB, 2, note[:], depth + 1)</code></pre>
<p> 저는 맨 처음에 match 함수 안에 승, 무, 패를 기록하여 모든 경기의 경우의 수를 조사하는 알고리즘을 짰고 복잡도가 3^15 이기 때문에 통과할 줄 알았습니다. 하지만 시간 초과가 났고 로컬에서 테스트 케이스 입력 후 결과가 나오기까지 10초가 지나 결과가 나왔습니다. </p>
<p> 우선 재귀문을 단순하게 만들었기 때문에 기본적인 코드의 연산수(1초당 1억) 문제는 아닌 걸로 생각했습니다. 혹시 내가 설계한 시간 복잡도와 실제 코드가 돌아가는 시간 복잡도가 다른가? 라는 생각이 들어 전역 변수를 CNT를 재귀문에 선언했고, 함수 시작 시 CNT 값을 +1 해주는 로직을 추가해 총 함수의 호출이 몇 번 일어나는지 검사해 봤습니다. </p>
<p> 검사 결과 처음 설계단에서 생각했던 복잡도가 정확히 맞았고, 총 함수의 호출은(3^15 + 3^14 + ‘’’ ‘’’ + 3^2+3^1) 로 총 2000만 번이 조금 넘게 함수의 호출이 이루어짐을 확인할 수 있었습니다. </p>
<p>1초에 1억이 돌아간다는 가정하에 함수의 코드도 간단하기 때문에 충분히 코드가 돌아갈 것으로 예상되었지만, 왜 몇 십 배 느린 실행 속도를 보였을까요? 바로 함수의 오버헤드 때문입니다. 함수는 컨텍스트 스위칭을 해야 하는 오버헤드를 가지고 있기 때문에 컨텍스트 스위칭이 2000만 번 넘게 실행 되었으니 매우 느렸을 수 밖에 없습니다. </p>
<p>그렇다면 어떻게 위의 문제를 해결했을까요? 🤔 저는 두 가지 방법을 생각했습니다.</p>
<blockquote>
</blockquote>
<pre><code>🌟 i) 꼬리재귀를 이용한 컴파일러 최적화
   ii) 완전 탐색 과정 가지치기



 재귀 함수의 컨텍스트 스위칭을 없애기 위해 컴파일러를 최적화하는 재귀 방식인 꼬리 재귀도 해당 코드에서는 승, 무, 패 세 가지로 나뉘기 때문에 꼬리 재귀를 사용할 수 없었다고 생각해 2번 가지치기 방법을 선택하였습니다.

 가지치기의 경우 생각해봅시다. 현재 알고리즘은 모든 경기의 내용을 탐색하고 있습니다. 잘 생각해 보면 굳이 그럴 필요가 있을까요 ? 만약 A팀이 5승을 거두었다고 생각해 봅시다. A 팀이 5승을 거둔 경우가 있는지 찾기 위해 재귀하는 과정에서 1패라도 패한 경우 이후의 내용을 탐색해 봤자 더 의미가 있을까요 ? 위의 가지치기 과정을 코드로 구현한다면 현재 팀의 승, 무, 패를 기록할 때 기록한 값과 입력 시 주어지는  승, 무, 패 각각을 비교해 기록 내용이 크다면 더 이상의 탐색은 무의미하기 때문에 함수를 종료시키면 됩니다.

```jsx
if status == 0: #승
        aIDX = position[a]
        bIDX = position[b]
        note[aIDX] += 1
        note[bIDX+2] += 1
        #아래의 if문을 이용한 가지치기
        if answer[aIDX] &lt; note[aIDX] or answer[bIDX+2] &lt; note[bIDX+2] :
            return</code></pre><h2 id="📖-코드">📖 코드</h2>
<pre><code>
result = {}
team = [&#39;A&#39;,&#39;B&#39;,&#39;C&#39;,&#39;D&#39;,&#39;E&#39;,&#39;F&#39;]
allMatch = []

for i in range(6):
    for j in range(i+1,6):
        allMatch.append([team[i],team[j]])

position = {&#39;A&#39;: 0, &#39;B&#39;: 3, &#39;C&#39;: 6, &#39;D&#39;: 9, &#39;E&#39;: 12, &#39;F&#39;: 15}

def match(a,b,status,note,depth) :

    if status == 0: #승
        aIDX = position[a]
        bIDX = position[b]
        note[aIDX] += 1
        note[bIDX+2] += 1
        if answer[aIDX] &lt; note[aIDX] or answer[bIDX+2] &lt; note[bIDX+2] :
            return

    elif status == 1: #무
        aIDX = position[a]
        bIDX = position[b]
        note[aIDX+1] += 1
        note[bIDX+1] += 1

        if answer[aIDX+1] &lt; note[aIDX+1] or answer[bIDX+1] &lt; note[bIDX+1] :
            return

    elif status == 2:  # 패
        aIDX = position[a]
        bIDX = position[b]
        note[aIDX + 2] += 1
        note[bIDX] += 1

        if answer[aIDX+2] &lt; note[aIDX+2] or answer[bIDX] &lt; note[bIDX] :
            return

    if depth == 15:
        result[str(note)] = 1
        return

    nextA ,nextB = allMatch[depth]

    match(nextA,nextB,0,note[:],depth+1)
    match(nextA, nextB, 1, note[:], depth + 1)
    match(nextA, nextB, 2, note[:], depth + 1)

for i in range(4):
    answer = list(map(int,input().split()))
    note = answer[:]

    match(&#39;A&#39;, &#39;B&#39;, 0, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 1)
    match(&#39;A&#39;, &#39;B&#39;, 1, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 1)
    match(&#39;A&#39;, &#39;B&#39;, 2, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 1)

    if result.get(str(answer)) != None :
        print(1, end=&#39; &#39;)
    else:
        print(0, end=&#39; &#39;)</code></pre><h2 id="배운-것">배운 것</h2>
<ol>
<li>백트래킹할 때 깊은 복사,얕은 복사시를 한 번 더 짚고 넘어가자.</li>
<li>시간이 애매할 때는 가지치기를 생각하자.</li>
<li>꼬리재귀는 저렇게 반복적인 호출에서 불가능하다.</li>
</ol>
<h3 id="refactoring">refactoring</h3>
<p>위의 과정을 가독이 좋게 리팩터링 하였습니다.
(1600 Byte -&gt; 1000 Byte)</p>
<pre><code>result = {}
team = [&#39;A&#39;, &#39;B&#39;, &#39;C&#39;, &#39;D&#39;, &#39;E&#39;, &#39;F&#39;]
allMatch = []

for i in range(6):
    for j in range(i + 1, 6):
        allMatch.append([team[i], team[j]])

position = {&#39;A&#39;: 0, &#39;B&#39;: 3, &#39;C&#39;: 6, &#39;D&#39;: 9, &#39;E&#39;: 12, &#39;F&#39;: 15}

def match(depth, note):
    if depth == 15:
        result[str(note)] = True
        return

    for i in range(len(note)):
        if note[i] &gt; answer[i]:
            return

    a, b = allMatch[depth]
    aIDX = position[a]
    bIDX = position[b]

    # 승
    note_win = note[:]
    note_win[aIDX] += 1
    note_win[bIDX + 2] += 1
    match(depth + 1, note_win[:])

    # 무
    note_draw = note[:]
    note_draw[aIDX + 1] += 1
    note_draw[bIDX + 1] += 1
    match(depth + 1, note_draw[:])

    # 패
    note_lose = note[:]
    note_lose[aIDX + 2] += 1
    note_lose[bIDX] += 1
    match(depth + 1, note_lose[:])

for _ in range(4):
    answer = list(map(int, input().split()))
    match(0,[0]*18)

    if str(answer) in result:
        print(1, end=&#39; &#39;)
    else:
        print(0, end= &#39; &#39;)
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] 백준 17619) 개구리 점프]]></title>
            <link>https://velog.io/@young_gyu/Python-%EB%B0%B1%EC%A4%80-17619-%EA%B0%9C%EA%B5%AC%EB%A6%AC-%EC%A0%90%ED%94%84</link>
            <guid>https://velog.io/@young_gyu/Python-%EB%B0%B1%EC%A4%80-17619-%EA%B0%9C%EA%B5%AC%EB%A6%AC-%EC%A0%90%ED%94%84</guid>
            <pubDate>Wed, 13 Mar 2024 07:32:04 GMT</pubDate>
            <description><![CDATA[<h2 id="🔗문제-링크">🔗문제 링크</h2>
<p><a href="https://www.acmicpc.net/problem/17619">17619번: 개구리 점프</a></p>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/b1398cca-d146-4f0d-ba6a-a2b219781249/image.png" alt="Untitled"></p>
<h2 id="✅-문제-유형">✅ 문제 유형</h2>
<p>라인 스위핑, 유니온 파인드, 정렬</p>
<p>유니온 파인드가 궁금하다면 ? → <a href="https://velog.io/@young_gyu/Python-Union-Find-Disjoint-Set%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80">유니온 파인드</a></p>
<h2 id="🤔-문제-풀이">🤔 문제 풀이</h2>
<p> 개구리가 점프해서 통나무를 건널 수 있는지를 판단하는 문제입니다. 개구리가 점프하는 조건은 통나무의 y좌표에 상관없이 수직 방향으로 뛸 수 있습니다. 그러므로 통나무 A와 통나무 B끼리 겹치는 x좌표가 있어야지 서로 건널 수 있게 됩니다.</p>
<p> 그렇다면 유니온 파인드로 어떻게 문제를 풀 수 있을까요? 유니온 파인드는 집합 알고리즘입니다. 서로 건널 수 있다면 같은 집합으로 분류하고 건널 수 없다면 다른 집합으로 분류하면 됩니다. </p>
<h2 id="🚀-문제-해결">🚀 문제 해결</h2>
<p> 통나무를 정렬하고 이웃한 통나무끼리 건널 수 있는지 확인해  합집합 연산을 한다면 문제가 해결될 것으로 생각하고 아래와 같은 코드를 짰습니다</p>
<pre><code class="language-jsx">for i in range(N):

    cnum,cs,ce,cy = tree[i]
    nnum,ns,ne,ny = tree[i+1]

    if ce &gt;= ns : # union 해야함
        merge(cnum,nnum)</code></pre>
<p> 여기서 저는 <strong>이웃한 통나무끼리</strong> 통나무를 건널 수 있는 조건을 비교해 합집합 연산을 하는 것이 아니라 현재 집합에서 통나무가 가장 오른쪽으로 뻗어있는 길이를 사용해 조건을 비교해야 하는 것을 깨닫고 코드를 아래와 같이 수정했습니다 🙂🙂🙂</p>
<pre><code class="language-jsx">for i in range(N):
    cnum,cs,ce,cy = tree[i]
    MAXEND = max(ce,MAXEND)
    nnum,ns,ne,ny = tree[i+1]

    if MAXEND &gt;= ns : # 통나무1이 속한 집합에서 통나무2로 건널 수 있다. 합집합 연산 실행 
        merge(cnum,nnum)</code></pre>
<h2 id="📖-코드">📖 코드</h2>
<pre><code>N,Q = map(int,input().split())

def find(x): # 자신이 속한 집합을 찾는다
    if arr[x] == x :
        return x

    arr[x] = find(arr[x])
    return arr[x]

def merge(a,b):
    a = find(a)
    b = find(b)

    if a != b: # 다른 집합이라면 합집합 연산 수행
        MIN,MAX = min(a,b) , max(a,b)
        arr[MAX] = MIN

arr=[i for i in range(N+1)]
tree =[[i,0,0,0] for i in range(N+1)]

for i in range(1,N+1):
    a,b,c = map(int,input().split())
    tree[i][1],tree[i][2],tree[i][3] = a,b,c

tree.sort(key = lambda x : x[1]) # 통나무의 시작 지점 순으로 정렬

MAXEND = 0

for i in range(N):
    cnum,cs,ce,cy = tree[i]
    MAXEND = max(ce,MAXEND)
    nnum,ns,ne,ny = tree[i+1]

    if MAXEND &gt;= ns : # 통나무1이 속한 집합에서 통나무2로 건널 수 있다. 합집합 연산 실행 
        merge(cnum,nnum)

for i in range(1,N+1):
    find(i)

for i in range(Q):
    a,b = map(int,input().split())

    if arr[a] == arr[b]: # 같은 집합이라면 통나무를 건널 수 있다.
        print(1)
    else:
        print(0)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] Union Find? Disjoint Set은 무엇인가 ?]]></title>
            <link>https://velog.io/@young_gyu/Python-Union-Find-Disjoint-Set%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@young_gyu/Python-Union-Find-Disjoint-Set%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Tue, 12 Mar 2024 17:08:29 GMT</pubDate>
            <description><![CDATA[<h1 id="🤔union-find">🤔Union Find?</h1>
<p> 유니온 파인드, 혹은 분리 집합이라 불리는 알고리즘은 두 원소가 같은 집합에 속하는지 판별해주는 알고리즘이다. 두 원소를 같은 집합으로 분류하는 <strong>merge(합집합)</strong> 연산과 원소가 어느 집합에 속해있는지 알려주는 <strong>find</strong> 연산이 사용된다. 간단한 예제를 보면서 알고리즘을 알아보자. </p>
<h1 id="✅예제">✅예제</h1>
<p><a href="https://www.acmicpc.net/problem/1717">1717번: 집합의 표현</a></p>
<h3 id="문제-조건">문제 조건</h3>
<p>1 ≤ n ≤ 100,0000</p>
<p>1≤ m ≤ 100,000</p>
<p>0 ≤ a≤ b ≤ n</p>
<p>a, b는 정수</p>
<h3 id="알고리즘-개요">알고리즘 개요</h3>
<p> n+1 개의 원소가 있고 m개의 연산이 실행된다. 연산은 두 가지로 두 원소가 포함된 집합을 합집합 하는 연산과 두 원소가 같은 집합에 속해있는지 확인하는 연산이 있다. 집합을 표현하기 위해서 n+1개의 배열을 선언한다. 배열의 값은 해당 원소(인덱스)가 무슨 집합에 속해있는지 가리킨다.  </p>
<p> 집합을 가리키기 위해서는 집합의 대표 번호를 정한다. 이때 해당 집합의 가장 작은 숫자를 집합의 대표 번호로 사용한다. 단, 자신이 집합의 대표 번호라면 -1로 선언한다. </p>
<p> 예를 들면, 집합 Set1 = { 1, 2, 3, 5} ,  Set2 = { 0, 4}  가 존재한다고 가정해보자. 아래의 사진과 같이 arr 배열로 집합을 표기할 수 있을 것이다.</p>
<img src="https://velog.velcdn.com/images/young_gyu/post/dfacaa95-f398-4a34-b576-1be9804f5e14/image.jpg" width="50%" height="50%">





<h3 id="알고리즘-구현">알고리즘 구현</h3>
<p> 해당 알고리즘의 구현을 위해 두 가지 연산이 필요하다.</p>
<ol>
<li><strong>find</strong> 연산 &gt; 원소가 어느 집합에 속해있는지 알려주는 연산.</li>
<li><strong>merge</strong> 연산 &gt; 두 원소를 같은 집합으로 분류하는 연산.</li>
</ol>
<h3 id="find-연산">find 연산</h3>
<pre><code class="language-python">def find(node):

    if arr[node] == -1:
            return node

    arr[node] = find(arr[node])
    return arr[node]</code></pre>
<p> 자신이 속한 집합을 찾는 함수이다. 만약 집합이 S1 = {1,2,3,5} 라면  find(2)는 1을 반환해야 할 것이다. find는 2번 원소가 해당한 집합의 번호(1,2,3,5 중 가장 작은 수)를 알려주어야 하기 때문이다.</p>
<p>  find에서 가장 핵심적인 코드는 <strong>arr[node] = find(arr[node])</strong> 이다. 재귀함수라 의미를 파악하기 어렵기 때문에 처음 유니온 파인드를 공부하면 이 부분에서 의문을 가질 수 있을 것이다. 하지만 문제를 몇 번 풀다 보면 금방 이해할 것이다. <strong>당장 이해가 안 된다면 꼭 외우도록 하자 !</strong> </p>
<h3 id="merge-연산">merge 연산</h3>
<pre><code class="language-python">def merge(a,b):

        a = find(a)
        b = find(b)

        if a==b: # 이미 두 원소가 같은 집합에 속해있기 때문에 합집합 연산을 수행하지 않는다.
            return

        big_node = max(a,b)
        small_node = min(a,b)

        arr[big_node] = small_node</code></pre>
<p> 두 원소의 집합 번호를 비교하고 큰 노드가 작은 노드를 가리키도록 설계하면 된다.</p>
<h1 id="전체-코드">전체 코드</h1>
<pre><code class="language-python">import sys
sys.setrecursionlimit(10**6)
input=sys.stdin.readline

def find(node):
    if arr[node]==-1:
        return node
    else:
        arr[node]=find(arr[node])
        return arr[node]

def merge(a,b):
    a=find(a)
    b=find(b)

    if a==b: #이미 a,b 같은 집합에 소속되어있다
        return

    # 노드를 잇는 과정
    big_node=max(a,b)
    small_node=min(a,b)
    arr[big_node]=small_node

def is_union(a,b):
    a=find(a)
    b=find(b)

    if a==b:
        print(&quot;YES&quot;)
    else:
        print(&quot;NO&quot;)

n,m = map(int,input().split())
arr=[-1]*(n+1)

for i in range(m):
    op,a,b=map(int,input().split())
    if op == 0:
        merge(a,b)
    else:
        is_union(a,b)
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] 백준 17615) 볼 모으기]]></title>
            <link>https://velog.io/@young_gyu/Python-%EB%B0%B1%EC%A4%80-17615-%EB%B3%BC-%EB%AA%A8%EC%9C%BC%EA%B8%B0</link>
            <guid>https://velog.io/@young_gyu/Python-%EB%B0%B1%EC%A4%80-17615-%EB%B3%BC-%EB%AA%A8%EC%9C%BC%EA%B8%B0</guid>
            <pubDate>Fri, 08 Mar 2024 03:30:06 GMT</pubDate>
            <description><![CDATA[<h2 id="🔗문제-링크">🔗문제 링크</h2>
<p><a href="https://www.acmicpc.net/problem/17615">17615번: 볼 모으기</a></p>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/1098c679-2418-418b-9381-96a6559562f6/image.png" alt=""></p>
<h2 id="✅-문제-유형">✅ 문제 유형</h2>
<p><strong>구현</strong></p>
<h2 id="🤔-문제-풀이">🤔 문제 풀이</h2>
<p> 백준의 알고리즘 분류는 그리디 알고리즘으로 되어 있지만 딱히 제 풀이는 그리디한 것 같지는 않습니다. 저는 문제에서 요구하는 바를 작은 문제로 나누고 작은 문제의 결과를 비교해 최종 해를 찾았습니다. </p>
<p>문제를 4개의 작은 문제로 나누었습니다.</p>
<p>i )  정렬 결과가 RRRRBBBB , R공을 옮긴다.<br>ii )  정렬 결과가 RRRRBBBB, B공을 옮긴다.
iii ) 정렬 결과가  BBBBRRRR, R공을 옮긴다.
iiii ) 정렬 결과가  BBBBRRRR, B공을 옮긴다.</p>
<p> 이렇게 나눈 4개의 작은 문제들 중 공을 옮긴 횟수가 가장 작은 것이 최종 해가 됩니다. 그렇다면 작은 문제의 해는 어떻게 구할까요 ? case 1의 경우를 생각해봅시다. R이 왼쪽 B가 오른쪽에 정렬이 되어 있어야합니다. 만약 입력이 RRRBRBBB 라면 R공을 1번 옮기니 1이 될 것이고, 입력이 BBBRBRRR 라면 R공을 4번 옮겨야하니 4가 될 것입니다. 이를 통해 왼쪽부터 입력 문자열을 반복문을 통해 탐색했을 때 B가 한 번도 등장하지 않았다면 R공을 왼쪽으로 움직일 필요가 없습니다. 하지만 B가 한 번이라도 등장했다면 R공을 왼쪽으로 옮겨야 할 것입니다. 이를 코드로 나타내면 아래와 같습니다. </p>
<pre><code>CNT = 100000000000
# i case

bit = 0
cnt = 0

for i in ball :
    if i == &#39;B&#39;:
        bit = 1
    if bit == 1 and i == &#39;R&#39; :
       cnt += 1
CNT = min (cnt,CNT)
#print(&quot;i &quot;, cnt)</code></pre><p>위와 같은 방식으로 나머지 작은 문제를 해결한다면 최종 해를 구할 수 있습니다.</p>
<h2 id="📖-코드">📖 코드</h2>
<pre><code>n = int(input())
ball = list(input())

# i ) RRRRBBBB R공 옮기기
# ii ) RRRRBBBB B공 옮기기
# iii ) BBBBRRRR R공 옮기기
# iiii ) BBBBRRRR B공 옮기기

CNT = 100000000000
# i case
bit = 0
cnt = 0

for i in ball :
    if i == &#39;B&#39;:
        bit = 1
    if bit == 1 and i == &#39;R&#39; :
       cnt += 1
CNT = min (cnt,CNT)
#print(&quot;i &quot;, cnt)

# ii case
bit = 0
cnt = 0
for i in range(n-1,-1,-1):
    if ball[i] == &#39;R&#39;:
        bit = 1
    if bit == 1 and ball[i] == &#39;B&#39; :
        cnt += 1

CNT = min(cnt,CNT)
#print(&quot;ii &quot;, cnt)

# iii case

bit = 0
cnt = 0
for i in range(n-1,-1,-1):
    if ball[i] == &#39;B&#39;:
        bit = 1
    if bit == 1 and ball[i] == &#39;R&#39; :
        cnt += 1
#print(&quot;iii &quot;,cnt)

CNT = min(cnt,CNT)

# iiii case
bit = 0
cnt = 0

for i in ball :
    if i == &#39;R&#39;:
        bit = 1
    if bit == 1 and i == &#39;B&#39; :
       cnt += 1

#print(&quot;iiii &quot;,cnt)
CNT = min (cnt,CNT)
print(CNT)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] 백준 28217) 두 정삼각형]]></title>
            <link>https://velog.io/@young_gyu/Python-%EB%B0%B1%EC%A4%80-28217-%EB%91%90-%EC%A0%95%EC%82%BC%EA%B0%81%ED%98%95</link>
            <guid>https://velog.io/@young_gyu/Python-%EB%B0%B1%EC%A4%80-28217-%EB%91%90-%EC%A0%95%EC%82%BC%EA%B0%81%ED%98%95</guid>
            <pubDate>Tue, 16 Jan 2024 19:01:31 GMT</pubDate>
            <description><![CDATA[<h2 id="🔗문제-링크">🔗문제 링크</h2>
<p><img src="https://velog.velcdn.com/images/young_gyu/post/e40ac623-8894-42b6-816f-008e4d1374ef/image.png" alt=""></p>
<p><a href="https://www.acmicpc.net/problem/28217">28217번: 두 정삼각형</a></p>
<h2 id="✅-문제-유형">✅ 문제 유형</h2>
<p><strong>구현, 시뮬레이션</strong></p>
<h2 id="🤔-문제-풀이">🤔 문제 풀이</h2>
<p>구현 문제로 특정한 알고리즘은 쓰이지 않습니다. 다만 구현을 위한 특별한 아이디어는 필요합니다 ! 처음 문제를 보고 삼각형 회전을 3번 시키면 원형으로 돌아오기 때문에 수학적인 규칙이 있지는 않을까? 생각하고 규칙을 기반으로 알고리즘을 생각했으나 딱히 규칙을 발견하지 못 했습니다. </p>
<p>그래서 <strong>‘회전</strong>’의 의미에 대해서 집중해 보았는데요. 밑에 그림은 시계 방향으로 한바퀴 회전했을 때의 정삼각형이 어떤 <strong>인덱스</strong>(편의상 인덱스의 시작은 1)를 가지는지 나타내 보았습니다. </p>
<img src="https://velog.velcdn.com/images/young_gyu/post/d9a27cef-5aa0-4386-b679-414e2b7696ff/image.png" width="50%" height="50%">


<p>정삼각형의 밑변은 6,3,1 이 됩니다 ! 한번 더 회전하면 밑변이 어떻게 구성될까요? 1,2,4입니다. 이 부분을 조금 더 생각해보면 다음 그림과 같이 회전에 대해서 정의할 수 있습니다. 3줄 친 부분이 곧 밑변이 되는 인덱스들을 나타낸 것입니다. </p>
<img src="https://velog.velcdn.com/images/young_gyu/post/8632c9bc-87a0-4827-ad93-3fe4f3bc93dc/image.png" width="50%" height="%">

<p>회전과 인덱스들의 관계에 대해서 이해했으니 이걸 파이썬 코드로 이중 반복문을 통해 구현하면 문제는 90% 끝입니다 ! 회전 코드는 rotate 함수로 정의하였습니다.</p>
<h2 id="📖-코드">📖 코드</h2>
<pre><code>import sys
input = sys.stdin.readline

# 문제 입력
N = int(input())


A=[]
B=[]

for i in range(N):
    A.append(list(map(int,input().split())))

for i in range(N):
    B.append(list(map(int,input().split())))


# 회전의 경우 반시계나 시계나 돌리나 사실 같은 경우의 수가 나옴.
# 하나의 회전 방향만을 고려한다.

# 반시계로 회전 구현.
def rotate(A):
    tmp = []
    for i in range(1,N+1):
        tmp.append([i for i in range(i)])

    for i in range(N): # 아래 선분의 기준, 인덱스의 기준
        for j in range(i,N): # 선분의 요소 대입
            tmp[N-i-1][j-i] = A[j][i]

    return tmp

# 현재 정삼각형과 B의 차이가 얼마인지 구하는 함수.
def calculator(A,B):
    cnt = 0

    for i in range(N):
        for j in range(i,N):
            if A[j][i] != B[j][i] :
                cnt+=1

    return cnt

MIN = 1e9 # 매우 큰 값 설정

# 대칭하지 않은 삼각형과 B의 차 구하기.
for i in range(3):
    MIN = min(MIN,calculator(A,B))
    A = rotate(A)

# 대칭한 삼각형 값 구하기.

symmetry = [] # 대칭한 삼각형
for i in range(1,N+1):
    symmetry.append([i for i in range(i)])

# 대칭 구현
for i in range(N):
    for j in range(i+1):
        symmetry[i][i-j] = A[i][j]

# 대칭한 삼각형과 B의 차 구하기
for i in range(3):
    MIN = min(MIN,calculator(symmetry,B))
    symmetry = rotate(symmetry)

print(MIN)</code></pre>]]></description>
        </item>
    </channel>
</rss>