<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>smj_716.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 10 Mar 2026 07:39:51 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>smj_716.log</title>
            <url>https://velog.velcdn.com/images/smj_716/profile/93eb520f-d7c4-45f2-b1dd-87c624ee31bb/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. smj_716.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/smj_716" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[무신사 AI ROOKIE ENGINEERING 합격 회고]]></title>
            <link>https://velog.io/@smj_716/%EB%AC%B4%EC%8B%A0%EC%82%AC-AI-ROOKIE-ENGINEERING-%ED%95%A9%EA%B2%A9-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@smj_716/%EB%AC%B4%EC%8B%A0%EC%82%AC-AI-ROOKIE-ENGINEERING-%ED%95%A9%EA%B2%A9-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Tue, 10 Mar 2026 07:39:51 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/smj_716/post/8e4cea0b-b7a3-4d86-bae9-081bf3188368/image.png" alt=""></p>
<p>올해 초에 진행된 <strong>무신사 AI ROOKIE NATIVE ENGINEERING 전형에 최종 합격</strong>하게 되었다🎉
채용 전환형 인턴을 뽑는 전형이라 과정이 길었다. 
그래서 그 과정들을 회고해보려고 한다.</p>
<hr>
<h2 id="지원-계기">지원 계기</h2>
<p>2022년 대학 입학 당시만 해도 ChatGPT가 없던 시절이였다. 
하지만 졸업하는 시점인 지금은 바이브 코딩이 뜨고 Claude, Cursor, MCP, n8n 같은 도구들이 많이 생겨나고 있다. </p>
<p>단 몇 년 만에 명령어 몇 줄로 프로젝트를 완성하는 시대를 체감하다 보니, 앞으로는 이러한 AI 도구들을 목적에 맞게 잘 활용하는 능력이 개발자의 경쟁력을 결정하지 않을까 생각했다. </p>
<p>대부분의 공고들과 달리 <strong>AI agent를 활용해 개발하는 개발자를 뽑는다는</strong> <a href="https://www.musinsacareers.com/ko/musinsa-rookie">무신사의 공고</a>를 보고 설레는 마음으로 지원했다!!</p>
<p>전형 과정은 아래와 같이 총 4단계였다.
<strong>1.</strong> 서류 전형
<strong>2.</strong> 1차 온라인 코딩테스트
<strong>3.</strong> 2차 온라인 코딩테스트 (AI Agent 활용)
<strong>4.</strong> 오프라인 인터뷰</p>
<hr>
<h2 id="1차-코딩테스트">1차 코딩테스트</h2>
<p>시험은 구름 devth 사이트로 1시간 동안 진행되었고, 2문제었다.
문제 유형은 복잡한 알고리즘 문제가 아닌 <strong>많은 입출력과 예외 상황들을 구현하는 문제</strong>였다.</p>
<p>여기서도 전형의 목적이 드러났다❗</p>
<p>복잡한 알고리즘은 AI를 사용하면 1초만에 풀 수 있다. </p>
<p>🌟 하지만 <strong>주어진 요구사항을 제대로 분석하고, 코드로 정확하게 구현할 수 있는지</strong>는 사람의 개입이 필요하기에 그런 역량을 보는 것 같았다. </p>
<p>첫 번째 문제는 입출력부터 복잡하다고 느꼈기에 최대한 예제들을 참고해서 요구사항을 빠트리지 않으려고 노력했고, 35분이 걸렸다. 
두번째 문제도 비슷한 유형이라 코드를 반쯤 입력하는 중에 시험이 종료되었다... 허허🤣</p>
<p>다행히 1문제만 잘 구현했다면 합격인 것 같았다..!</p>
<img src="https://velog.velcdn.com/images/smj_716/post/0364d38d-7867-4085-97c3-da096fb796e0/image.png" width="450"> 

<hr>
<h2 id="2차-코딩테스트">2차 코딩테스트</h2>
<p>시험이 3시간 동안 진행되었고, AI agent를 사용하는 시험이었기에 
여러 API들을 구현하는 문제가 아닐까 예상했었다. </p>
<p>이번 전형은 백과 프론트를 나누지 않고 뽑는 전형이라 프론트 영역이 나올까 걱정은 했지만 그래도 백엔드 구현에 집중하자고 마음 먹었다.</p>
<p>회사에서 Codex를 지원해주지만 평소에 사용하던 Claude로 진행했다. </p>
<p>👉 Claude에게 요구사항이 긴 예상 문제를 추천해달라고 했고, 그렇게 매일 1문제씩 타이머를 맞춰 구현하는 연습을 했다.</p>
<p>하지만 요구사항이 많으면 많을수록 말을 듣지 않았다...</p>
<p><strong>그래서 3가지 정도를 미리 대비해두었다.</strong></p>
<p>✔️ <strong>단계별 md 파일 전략</strong></p>
<img src="https://velog.velcdn.com/images/smj_716/post/345ea012-34da-4705-ae6b-c32749f8ec1c/image.png" width="300"> 

<p>요구사항 자체를 AI가 해석하고, 스스로 진행 순서를 md 파일로 나누어
한 단계가 구현될 때마다 내가 확인하는 방식으로 진행했다.</p>
<p>✔️ <strong>Claude.md 작성</strong>
공통적으로 지켜야 할 최소한의 규칙들을 미리 작성해서 아키텍처 구조나 공통 응답 방식, 예외 처리 같은 부분들은 나의 개발 방식에 맞추도록 했다. 
(그래도 지켜지지 않는 부분은 시험 당시에 즉각 claude.md에 다시 추가했다)</p>
<p>✔️ <strong>TDD 개발</strong>
필요하지 않은 메서드나 클래스를 미리 만들어 오류가 발생하는 경우가 빈번하게 발생했다. 좋은 방법이 없을까 고민하다가, 평소에 좋은 개발 방식인 것은 알고 있지만 사람이 직접 하면 속도가 느리기에 쉽게 하지 못했던 TDD 개발 방식을 지키도록 명령했다. </p>
<p><a href="https://github.com/musinsatech/2026-musinsa-rookie/blob/main/PROBLEM.md"><strong>2026-musinsa-rookie 2차 코딩테스트 문제</strong></a></p>
<p>다행히 연습한 방식대로 문제가 나왔고, 구현은 쉽게 할 수 있었다. 다만‼️</p>
<ul>
<li>명시되는 요구사항을 <strong>어떤 스택을 사용하여 어떻게 구현할지</strong></li>
<li>또 <strong>명시되지 않은 요구사항은 어디까지 구현할지</strong></li>
</ul>
<p>는 스스로 판단해야 했기에 그 부분이 어려웠다. 
하지만 <strong>이게 시험의 포인트가 아닐까</strong> 생각한다 🌟</p>
<p>AI agent를 사용하면 백엔드 경험이 없더라도 누구나 기능이 돌아가게 만들 수 있는 시대다. 하지만 </p>
<ul>
<li>어떤 방식으로 락을 걸지? </li>
<li>락을 어디까지 적용할지? </li>
<li>현재 상황에서 우선순위가 무엇인지?</li>
</ul>
<p>와 같은 것들은 사람의 개입이 꼭 필요하다. 
그것이 개발의 질을 높이는 것이라 생각하기에 무신사가 그런 사람을 원하는 것 같았다.</p>
<p>실제 구현과 점검은 1시간 반 정도 걸렸지만, 요구하는 문서들이 많았기에 <strong>문서를 쓰는 데 시간을 많이 썼다.</strong></p>
<ul>
<li>내가 어떻게 AI를 사용했고, </li>
<li>요구사항들을 어떻게 해석했고,</li>
<li>어디까지 구현할지에 대한 근거들을 자세하게 작성했다.</li>
</ul>
<p>자격증 시험과 날짜가 비슷해서 잠을 줄여가며 연습했고, 최선을 다했기에 결과에 대한 후회는 없을 것이라 생각했다. </p>
<img src="https://velog.velcdn.com/images/smj_716/post/b2f1a1e9-3f5a-4c06-bc14-829ffe572d60/image.png" width="500"> 

<p>근데.. 진짜 합격..!!🤩
AI로 채점을 해서 그런지 결과가 빨리 나왔고, 설 연휴 전에 좋은 소식을 받아 너무너무 기뻤다. </p>
<hr>
<h2 id="인터뷰">인터뷰</h2>
<p>사전에 인터뷰 관련 메일을 받았다.
인터뷰는 <strong>1,2차 코딩테스트와 제출한 포트폴리오</strong>에 대한 질문이라고 했다. </p>
<p>그래서 노션에 회고를 하며 예상 질문들을 잔뜩 적고 답하면서 연습했다.
추가로 <strong>AI에 대한 나의 생각이나 기본적으로 나오는 기술 질문, 컬쳐핏 질문들</strong>도 준비했다. </p>
<img src="https://velog.velcdn.com/images/smj_716/post/4eb2ab38-febc-4a27-9907-e85ae8f37c42/image.png" width="400"> 

<p>면접 경험이 많이 없었기에 유튜브에 면접 팁들을 많이 찾아봤고, 카드를 입에 물고 웃는 연습까지... ㅋㅋㅋㅋ 했다^^😁 (너무 간절해서...)</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/smj_716/post/c5e16b07-1c52-4076-af64-d3927d7c0ac8/image.jpg" alt=""></th>
<th><img src="https://velog.velcdn.com/images/smj_716/post/fd1ca38e-546e-4063-9889-6980a3d6459c/image.jpg" alt=""></th>
</tr>
</thead>
</table>
<p>면접은 <strong>1:1로 1시간</strong> 동안 진행되었다. 
<strong>크게 세 가지</strong>에 대해 길게 대화했다. </p>
<h3 id="❓-2차-코딩테스트에서-나왔던-수강신청-로직">❓ 2차 코딩테스트에서 나왔던 수강신청 로직</h3>
<ul>
<li>왜 이렇게 구현했는지 </li>
<li>이렇게 구현했을 때의 문제점이 무엇인지 </li>
<li>그 문제점을 해결하려면 어떻게 해야 하는지 </li>
<li>해결 방법을 적용했을 때의 Trade-off가 무엇인지 등 꼬리 질문들이 이어졌다.</li>
</ul>
<p>하지만 힌트도 주시고 편한 분위기라 꼬리 질문들에 대한 내 생각을 이야기하는 과정이 토론하는 것처럼 재밌었다.</p>
<h3 id="❓-포트폴리오에-작성한-수강신청-시스템">❓ 포트폴리오에 작성한 수강신청 시스템</h3>
<p>이전에 <strong>대용량 트래픽 수강신청 시스템을 개발한 경험</strong>이 있었기에 그 프로젝트와 비교하는 질문들도 있었다. 또 그 당시 <strong>JMeter</strong>를 이용해서 대용량 트래픽을 테스트 했었는데,</p>
<ul>
<li>그때 JMeter를 어떻게 사용했고</li>
<li>어떤 기준으로 결과를 분석했고</li>
<li>어떤 테스트 시나리오들이 있었는지도 물어보셨다. </li>
</ul>
<h3 id="❓-ai-활용-방법과-한계점">❓ AI 활용 방법과 한계점</h3>
<p>위에 적어둔 2차 코딩 테스트의 3가지 전략을 AI 활용 방법으로 설명했다.
하지만 이 방법이 완벽한 것은 아니기에,
AI가 올바르게 구현하도록 하기 위한 또 다른 방법으로 *<em>무신사 블로그에서 본 글을 언급하며 대답했다. *</em></p>
<p><a href="https://techblog.musinsa.com/%EC%A7%88%ED%92%8D%EB%85%B8%EB%8F%84%EC%9D%98-ai-claude-%EC%97%90%EA%B2%8C-%EC%97%84%EA%B2%A9%ED%95%9C-%EC%84%A0%EC%83%9D%EB%8B%98-%EC%9E%A5%EC%B0%A9%ED%95%98%EA%B8%B0-61c3d533fc40?source=publication_content_feed----f107b03c406e-----3-----------------------------------"><strong>질풍노도의AI에게 엄격한 선생님 장착하기</strong></a></p>
<p><em>(면접 전에 기술 블로그를 읽고, 예상 질문 답변과 함께 답할 수 있는 부분이 있는지 살펴보는 것도 도움이 되었다)</em></p>
<p>이 외에는 간단한 <strong>컬쳐핏 질문 두 문제 정도와 마지막 질문이 있는지</strong> 여쭈어보셨다.</p>
<p>어떤 팀에 소속되어 있는지 여쭈어봤는데 29CM의 결제 직전까지의 모든 사용자 단계를 개발하는 팀이었다.😲</p>
<p>이전에 대용량 트래픽 시스템을 개발하면서 확장 가능한 코드를 작성하느라 많이 노력했지만 7천명 이상에서는 한계가 있었다. 그래서 29CM의 쿠폰 발급처럼 트래픽이 몰리는 상황에서는 어떻게 구현되는지 공부해보고 싶다고 말씀드렸다.</p>
<hr>
<h2 id="최종-합격">최종 합격</h2>
<p>너무 간절해서였을까.. 
인터뷰 전형 <strong>합격 소식과 함께 입사 조건 검토 메일</strong>이 왔다!!🥳</p>
<img src="https://velog.velcdn.com/images/smj_716/post/60290f8c-3ec5-451c-bb3e-7b69eb6cf3cd/image.png" width="450"> 

<p>4학년때부터 준비한 취업 과정이 너무 길게 느껴질 만큼 힘들었는데,
묵묵히 최선을 다한 덕분에 졸업하자마자 이런 좋은 기회를 얻을 수 있어 너무 감사했다. </p>
<p>첫 회사가 무신사라는게 너무 설레서 잠도 못 자고 밥도 안 들어갔다.. ㅎㅎ
행복해서 다크서클이 생긴건 처음이다...</p>
<p><strong>6개월 동안 좋은 동료들과 함께 많은 경험을 쌓고, 많이 배우는 시간이 되었으면 좋겠다. 
파이팅💪🏻</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[쉽게 설명하는 개발자가 되기 위한 연습장 - 강사 활동 기록]]></title>
            <link>https://velog.io/@smj_716/%EC%89%BD%EA%B2%8C-%EC%84%A4%EB%AA%85%ED%95%98%EB%8A%94-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EB%90%98%EA%B8%B0-%EC%9C%84%ED%95%9C-%EC%97%B0%EC%8A%B5%EC%9E%A5-%EA%B0%95%EC%82%AC-%ED%99%9C%EB%8F%99-%EA%B8%B0%EB%A1%9D</link>
            <guid>https://velog.io/@smj_716/%EC%89%BD%EA%B2%8C-%EC%84%A4%EB%AA%85%ED%95%98%EB%8A%94-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EB%90%98%EA%B8%B0-%EC%9C%84%ED%95%9C-%EC%97%B0%EC%8A%B5%EC%9E%A5-%EA%B0%95%EC%82%AC-%ED%99%9C%EB%8F%99-%EA%B8%B0%EB%A1%9D</guid>
            <pubDate>Fri, 29 Aug 2025 12:58:32 GMT</pubDate>
            <description><![CDATA[<p>이번 글에서는 실제로 내가 교육 현장에 나가 아이들을 만나고 직접 수업을 기획하고 진행했던 실전 경험을 담아보고자 한다.</p>
<p>여러 수업에 참여했지만, 그중에서도 특히 기억에 남고 의미 있었던 대표 수업 세 가지를 언급하겠다.
🌟이 경험들이 단순한 ‘교육 활동’에 머무르지 않고 <strong>개발자로서의 커뮤니케이션 능력을 키우는 데</strong>에도 많은 도움이 되었기 때문이다.</p>
<hr>
<h3 id="📍성산중-디지털-윤리-수업">📍성산중 디지털 윤리 수업</h3>
<p align="center">
  <img src="https://velog.velcdn.com/images/smj_716/post/1598dbc8-01a7-41bb-8096-982274b5c8f0/image.png" width="200">
</p>

<p>성산중학교에서는 혼자 강사로 수업을 맡아 디지털 윤리 수업을 진행하게 되었다.
윤리라는 주제 자체는 학생들에게 익숙한 내용이 많았지만, <strong>디지털 환경에서의 개념(ex IP, 서버, 인터넷망)은 낯설어</strong>하는 학생들이 많다.</p>
<p>🧠 그래서 나는 최대한 추상적인 기술 개념을 <strong>아이들의 언어로 쉽게 풀어 설명하려고 노력했다.</strong></p>
<blockquote>
<ul>
<li>&quot;인터넷은 눈에 보이지 않지만 서로 연결되어 있는 거대한 거미줄이라고 생각해볼래?</li>
</ul>
</blockquote>
<ul>
<li>&quot;우리가 각자 사용하는 컴퓨터는 거미줄 끝의 ‘서버’라고 할 수 있어. 이 서버들이 줄을 타고 연결되어 있어야 서로 메시지를 주고받을 수 있겠지?&quot;</li>
<li>&quot;그런데 서버가 누구인지를 알아야 연결할 수 있으니 IP주소는 서버의 집 주소와 같은 역할을 해.&quot;</li>
<li>&quot;데이터를 주고받을 땐 한번에 가지 않고 &#39;택배처럼 잘게 나뉘어서&#39; 가는 ‘패킷’이라는 것도 있어.&quot;</li>
</ul>
<p>이런 식으로 시각 자료와 함께 설명했더니 학생들의 이해도가 훨씬 높아졌다.
PPT 자료는 단순한 텍스트보다 시연 영상과 퀴즈 요소를 넣어서 구성했고 수업 전에는 여러 번 리허설을 하며 시간 배분도 꼼꼼히 점검했다.</p>
<p>🎨 수업 후반에는 <strong>Canva</strong>라는 AI 기반 디자인 도구를 활용해 학생 각자가 ‘자기만의 명함’을 만드는 활동도 진행했다.
단순히 정보를 전달하는 수업이 아니라 디지털 도구를 실생활에서 어떻게 응용할 수 있는지 체험해보는 시간으로 구성하고 싶었다.</p>
<hr>
<h3 id="📍-2-김천여중-알티노-자율주행차-코딩-수업">📍 2. 김천여중 알티노 자율주행차 코딩 수업</h3>
<p align="center">
  <img src="https://velog.velcdn.com/images/smj_716/post/1fea3a2e-3414-42f2-a28d-d79fd522c871/image.png" width="300">
</p>

<p>김천여중에서는 1~4교시에 걸쳐 <strong>자율주행차(알티노)를 블록코딩으로 제어하는 수업</strong>을 진행했다.
🧠 이 수업은 특히 <strong>반복문</strong>(for, while)이나 조건문 같은 기초 코딩 개념을 처음 접하는 아이들에게 어떻게 쉽게 설명할 수 있을지 고민이 많았던 수업이다.</p>
<p>아이들은 대부분 코딩을 처음 접해보는 친구들이었기 때문에, 추상적인 용어보다는 친근한 일상 예시를 들어 설명하려고 했다.</p>
<p>예를 들어 <strong>for문</strong>은 이렇게 설명했다.</p>
<blockquote>
<p>&quot;라면을 끓일 때 물을 붓고 → 스프를 넣고 → 면을 넣고 → 3분간 끓이는 과정을
세 번 반복한다고 생각해봐. 그러면 이게 바로 for문이야!&quot;</p>
</blockquote>
<p><strong>while문</strong>은 조금 다르게 접근했다.</p>
<blockquote>
<p>&quot;while문은 ‘엄마가 그만하라고 할 때까지 게임하기’ 같은 거야.
언제 끝날지 모르고 계속 반복되니까 while은 조건이 참일 때 계속 도는 거고, for는 정해진 횟수만큼 반복되니까 달라!&quot;</p>
</blockquote>
<p>이렇게 설명하니 아이들이 금방 고개를 끄덕이며 이해하는 모습을 보여줬고, 직접 블록을 조립해서 자율주행차가 ‘실제로 움직이는’ 과정을 보면서 흥미를 느끼는 듯했다.</p>
<p>‘내가 입력한 코드가 진짜로 작동하고 있구나’라는 성취감을 아이들 스스로 느낄 수 있게 해주고 싶었고, 그 부분은 충분히 전달된 것 같아 뿌듯했다.</p>
<hr>
<h3 id="📍-3-경일여고-데이터분석-자격증-수업">📍 3. 경일여고 데이터분석 자격증 수업</h3>
<p align="center">
<img src="https://velog.velcdn.com/images/smj_716/post/c95b0522-f4a5-4104-be53-bb86475f5a1a/image.png" width="300"/>

<p>경일여고에서는 방학 특강으로 4일간의 AI 데이터 분석(AICE Junior) 자격증 수업 보조를 맡았다.
🧠 이 수업은 비교적 난이도가 높은 수업이었고 딱딱한 개념들이 많다 보니 어떻게 하면 ‘기계적인 암기’가 아닌 ‘이해 기반’의 수업이 될 수 있을까를 가장 많이 고민했다.</p>
<p>예를 들어, <strong>MAE와 MSE의 차이</strong>를 이렇게 설명했다.</p>
<blockquote>
<p>&quot;친구 생일을 1일 틀리는 것과, 10일 틀리는 건 느낌이 다르지?
MAE는 평균 오차고, MSE는 큰 오차에 더 민감하게 반응해.
MSE는 오차를 제곱하니까 실수를 더 크게 받아들이는 방식이야.&quot;</p>
</blockquote>
<p>또 <strong>머신러닝의 종류</strong>를 설명할 때는 이렇게 말했다.</p>
<blockquote>
<p>&quot;단순 회귀는 딱 하나의 조건만 가지고 예측하는 거야.
예를 들어 ‘공부 시간만 가지고 시험 점수를 예측하는 것’
다중 회귀는 여러 개를 고려하는 거지.
‘공부 시간, 수면 시간, 식사 여부까지 다 고려해서 점수를 예측하는 것’!&quot;</p>
</blockquote>
<p>이렇게 예시를 들어 설명했을 때, 아이들이 훨씬 자연스럽게 받아들이는 것을 보면서 ‘설명이 어렵고 복잡하다고 느껴지는 건 결국 예시가 없어서 그런 게 아닐까’라는 생각이 들었다.</p>
<hr>
<h3 id="💡이-활동이-나에게-어떤-의미였는가">💡이 활동이 나에게 어떤 의미였는가?</h3>
<p>나는 백엔드 개발자가 되기 위해 다양한 프로젝트를 진행하며 기술적인 역량을 키워왔다.
하지만 실제 프로젝트를 하는 과정에서 프론트엔드나 기획자 등 다른 직군과의 소통이 매우 중요하다는 것을 자주 느꼈다.</p>
<p>이 강사 활동을 시작한 것도, 단순히 교육이 하고 싶어서가 아니라
<strong>&quot;기술을 기술 언어로만 설명하지 않고, 상대방의 눈높이에 맞게 전달하는 연습&quot;</strong>을 하고 싶었기 때문이다.</p>
<p>실제로 이런 연습이 많이 되었고, 프로젝트를 진행하면서 큰 도움이 되었다.</p>
<p>예를 들어, 프론트엔드 팀원에게 Redis와 SSE 구조를 설명해야 할 일이 있었는데 이렇게 설명한 적이 있다.</p>
<blockquote>
<p>&quot;Redis의 pub/sub 구조를 통해 서버A가 서버B에게 &#39;지금 들어와도 돼!&#39;라는 메시지를 실시간으로 보내주고
이때 클라이언트는 SSE를 통해 서버B와 연결된 상태로 계속 대기하다가, 서버 B가 A에게 메시지를 받으면 클라이언트에게 신호를 보내요&quot;</p>
</blockquote>
<p>이렇게 설명했더니 프론트팀도 쉽게 이해하고 그에 맞춰 설계해줬다.
<strong>결국 기술은 사람을 위한 것이고, 그 기술을 잘 전달할 수 있어야 진짜 개발자라고 생각한다.</strong></p>
<p>앞으로도 나는 단순히 코드를 잘 짜는 개발자가 아니라,
기술을 이해시키고 함께 만들어갈 수 있는 개발자가 되고 싶다.
그 방법 중 하나가 기술을 모르는 사람에게 쉽게 설명하는 연습이라고 생각한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[실시간 좌석 응답 기능 도입]]></title>
            <link>https://velog.io/@smj_716/%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%A2%8C%EC%84%9D-%EC%9D%91%EB%8B%B5-%EA%B8%B0%EB%8A%A5-%EB%8F%84%EC%9E%85</link>
            <guid>https://velog.io/@smj_716/%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%A2%8C%EC%84%9D-%EC%9D%91%EB%8B%B5-%EA%B8%B0%EB%8A%A5-%EB%8F%84%EC%9E%85</guid>
            <pubDate>Sun, 17 Aug 2025 06:56:13 GMT</pubDate>
            <description><![CDATA[<p>먼저 결과부터 보자!!
아래 영상은 <strong>JMeter</strong> 도구를 활용해 500명의 사용자가 특정 courseId에 대해 <strong>수강신청 → 수강취소</strong>를 반복하도록 테스트한 장면이다.
<img src="https://velog.velcdn.com/images/smj_716/post/24707faf-f2aa-47fa-a3e4-4d001fb52dfc/image.gif" alt=""></p>
<p>사용자 화면에서도 좌석 수가 즉시 반영되는 것을 확인할 수 있다.
<strong>❓우리는 어떻게 이런 실시간 좌석 반영 기능을 만들었을까❓</strong></p>
<hr>
<h2 id="1-고민-과정">1. 고민 과정</h2>
<h3 id="💭-실시간-여석-수를-mysql에서-가져와야-할까">💭 실시간 여석 수를 MySQL에서 가져와야 할까?</h3>
<p>초기에 수강신청 여석은 MySQL 컬럼에서 관리했다.
하지만 실시간성 요구가 커질수록 디스크 기반 DB에서 잦은 조회는 부하가 크고 응답 속도도 한계가 있었다.
앞서 프로젝트에서 Redis를 사용했던 경험이 있었기 때문에 <strong>“좌석 수만큼은 Redis에서 관리하면 더 빠르게 실시간 처리가 되지 않을까?”</strong>라는 생각이 들었다.</p>
<p>🌟결국 MySQL은 정합성을 보장하는 저장소로, Redis는 실시간 여석 수를 관리하는 역할로 분리하게 되었다.</p>
<h3 id="💭-그렇다면-어떻게-실시간을-전송할까">💭 그렇다면 어떻게 실시간을 전송할까?</h3>
<p>✔️ <strong>Polling 기반 계획</strong>
처음에는 프론트에서 일정 주기로 잔여 좌석 조회 API를 호출하는 것을 계획했었다.
하지만 이 방식은 사용자 수가 늘어날수록 서버에 불필요한 요청이 폭주했고 실시간성도 “주기적 갱신”이라는 한계에 부딪혔다.</p>
<p>✔️ <strong>WebSocket 고려</strong>
양방향 통신이 가능해 강력했지만 이번 요구사항은 
<strong>단방향 알림(서버→클라이언트)</strong>만 필요했다. 
인프라 관리와 구현 난이도를 고려했을 때 과도한 선택이라고 판단했다.</p>
<p>✔️ <strong>SSE(Server-Sent Events) 선택</strong>
HTTP 기반 단방향 스트리밍이라 브라우저 기본 지원이 가능했고
프론트 적용도 단순했다.
서버는 필요한 시점에만 알림을 push하면 되었기 때문에 Polling 대비 서버 부하를 크게 줄일 수 있었다.
무엇보다 Redis와 함께 사용했을 때 빠른 조회 + 실시간 전송이라는 요구사항을 가장 깔끔하게 충족할 수 있었다.</p>
<hr>
<h2 id="2-설계와-구현">2. 설계와 구현</h2>
<p>실시간 기능을 적용하기 위해 아래와 같은 구조를 도입했다.</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/388395b5-eaa4-47ab-966f-74db31cdd899/image.png" width = "470">

<p><strong>👉 Redis</strong></p>
<ul>
<li>좌석 수는 <code>course:{id}:remaining</code> 키에 저장</li>
<li>수강신청 성공 시 <code>decrement()</code>, 취소 시 <code>increment()</code>로 즉시 반영</li>
<li>Redis는 메모리 기반이라 조회/갱신 속도가 매우 빠름</li>
</ul>
<p><strong>👉 SseEmitterManager</strong></p>
<pre><code class="language-java">private final Map&lt;Long, SseEmitter&gt; emitters = new ConcurrentHashMap&lt;&gt;();
private final Map&lt;Long, List&lt;Long&gt;&gt; subscribedCourses = new ConcurrentHashMap&lt;&gt;();</code></pre>
<ul>
<li>사용자당 Emitter는 단 하나만 유지</li>
<li>각 emitter에 사용자가 구독한 courseId 리스트를 매핑</li>
<li>불필요한 중복 연결을 막아 서버 자원 효율성을 확보</li>
</ul>
<p><strong>👉 SseSeatService</strong></p>
<ul>
<li>구독 시점에 Redis 값을 조회 → 프론트에 초기 여석 값 전달</li>
<li>이후 좌석이 변하면 <code>notifyRemainingChanged()</code>에서 해당 courseId를 구독한 사용자에게만 이벤트 push</li>
</ul>
<p><strong>👉 EnrollmentService</strong></p>
<ul>
<li>수강신청/취소가 성공하면 Redis 좌석 변경</li>
<li>최신 좌석 값을 조회 후 구독자들에게 즉시 전송</li>
</ul>
<p><strong>📌 페이징을 사용하지 않은 이유</strong></p>
<p>처음에는 강의 수가 많으니 페이징을 도입하는 것이 맞을까 고민했다. 
하지만 몇가지 이유로 최종적으로는 페이징을 적용하지 않기로 결정했다.</p>
<ul>
<li>Redis 조회 속도는 매우 빠르기 때문에 많은 강의 여석을 한 번에 내려주더라도 서버 부하가 크지 않았다.</li>
<li>페이징을 적용하면 한 사용자가 여러 페이지(course 리스트)를 구독해야 하므로 사용자 1명당 여러 개의 SSE 채널이 발생하게 된다.</li>
<li>이는 오히려 서버 부하를 증가시키고 클라이언트 측에서도 연결 관리가 복잡해진다.</li>
</ul>
<p>그래서 전체 강의 구독 + 사용자당 단일 SSE emitter 유지라는 구조로 선택했다.</p>
<hr>
<h2 id="3-느낀점">3. 느낀점</h2>
<p>이번 기능을 구현하면서 단순히 새로운 기술을 학습한 것이 아니라,
<strong>💡“이전에 배운 기술들을 실제 문제 상황에 맞게 도입하고 적용한 경험”</strong>을 얻을 수 있었다.</p>
<ul>
<li>처음에는 MySQL만으로 좌석 수를 관리했지만 실시간 조회라는 요구사항 앞에서 한계를 느꼈다.</li>
<li>그때 앞서 학습하고 이미 사용해본 Redis의 특성을 떠올려 여석 수를 <strong>Redis</strong>로 옮겼고</li>
<li>마찬가지로 Polling 방식의 비효율을 개선하기 위해 과거 학습했던 <strong>SSE(Server-Sent Events)</strong>를 실서비스에 도입했다.</li>
</ul>
<p>즉, 단순히 새로운 기술을 도입하는 것이 아니라
이전에 습득한 기술들을 문제 해결의 맥락에 맞게 해석하고 실제 기능에 녹여냈다는 점에서 의미가 컸다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis로 대기열 시스템 성능 개선 (pub/sub)]]></title>
            <link>https://velog.io/@smj_716/Redis%EB%A1%9C-%EB%8C%80%EA%B8%B0%EC%97%B4-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0-pubsub</link>
            <guid>https://velog.io/@smj_716/Redis%EB%A1%9C-%EB%8C%80%EA%B8%B0%EC%97%B4-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0-pubsub</guid>
            <pubDate>Thu, 07 Aug 2025 07:31:47 GMT</pubDate>
            <description><![CDATA[<p>수강신청 시스템에서 가장 까다로운 부분은 단순히 대기열을 유지하는 것이 아니라
<strong>&quot;입장이 허용된 사용자가 실제로 로그인까지 완료했는지&quot;</strong>를 추적하고 &quot;빈자리가 생겼을 때 <strong>누굴 입장시킬지&quot;</strong> 정확히 판단하는 것이다.</p>
<h2 id="1-기존-구조-map-기반-관리의-한계">1. 기존 구조: Map 기반 관리의 한계</h2>
<p>초기에는 서버 내부에서 아래와 같이 관리했다.</p>
<ul>
<li>대기열: <code>List&lt;QueueUser&gt;</code> 형태로 메모리에 저장</li>
<li>입장한 사용자: <code>Map&lt;Long studentId, Long timestamp&gt;</code> 으로 추적</li>
<li>빈자리 발생 시: 수강신청 서버가 대기열 서버에 HTTP 요청(<code>/notify</code>)을 보내 ALLOWED 처리를 요청</li>
</ul>
<p>이 구조는 간단하지만 다음과 같은 명확한 한계가 있었다:</p>
<table>
<thead>
<tr>
<th>문제점</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>❌ 정합성</td>
<td>사용자가 <code>ALLOWED</code> 메시지를 받은 후 실제 로그인하지 않는 경우, 해당 자리는 낭비되며 시스템은 이를 인지할 수 없었다.</td>
</tr>
<tr>
<td>❌ 수동 관리</td>
<td>미입장 사용자에 대한 회수를 위해 <code>BatchManager</code>나 스케줄러가 필요했고 관리가 번거로웠다.</td>
</tr>
<tr>
<td>❌ 강한 결합</td>
<td>수강신청 서버가 대기열 서버의 주소와 API 경로(<code>/notify</code>)를 알아야 했고 HTTP 연결 실패 시 장애가 전파될 위험이 있었다.</td>
</tr>
</tbody></table>
<hr>
<h2 id="2-redis-구조-전환">2. Redis 구조 전환</h2>
<p>이러한 문제를 해결하기 위해 시스템은 전체 구조를 Redis 중심으로 재설계했다.
단순한 캐시 용도가 아닌, <strong>실시간 데이터 처리, 자동 만료, 비동기 메시지 전송까지 담당하는 핵심 인프라</strong>로 동작하게 되었다.</p>
<h3 id="✅-redis-구조-요약">✅ Redis 구조 요약</h3>
<img src = "https://velog.velcdn.com/images/smj_716/post/6a571415-9d47-43cb-9bc5-4e41331694c7/image.png" width = "500">


<table>
<thead>
<tr>
<th>목적</th>
<th>Redis 구조</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>대기열 순서 관리</td>
<td>List</td>
<td>Redis의 List에 사용자 토큰을 RPUSH 하고, 입장 시에는 LPOP으로 제거</td>
</tr>
<tr>
<td>빈자리 알림</td>
<td>Pub/Sub 채널</td>
<td>수강신청 서버에서 PUBLISH로 알림을 보내고, 대기열 서버는 해당 채널을 SUBSCRIBE하여 실시간 메시지를 수신\</td>
</tr>
<tr>
<td>인증 사용자 세션</td>
<td>Key-Value + TTL</td>
<td>사용자가 로그인하면 <code>session:{studentId}</code> 키를 TTL과 함께 Redis에 저장하고, 일정 시간이 지나면 자동으로 삭제되도록 구성</td>
</tr>
<tr>
<td>인증 사용자 수 집계</td>
<td>Set</td>
<td>인증된 사용자 키의 개수를 기반으로 Redis에서 빠르게 입장 인원을 파악</td>
</tr>
</tbody></table>
<h3 id="💡-구조-개선-효과">💡 구조 개선 효과</h3>
<ul>
<li><p><strong>입장 후 로그인 여부를 시스템이 자동으로 판단할 수 있다.</strong><br><code>TTL</code>이 설정된 키를 통해 사용자가 로그인하지 않으면 자동으로 자리가 회수되고 별도의 관리 로직이 필요 없다.</p>
</li>
<li><p><strong>빈자리 발생 시 대기열 서버로 정확하게 알릴 수 있게 되었다.</strong><br>기존에는 HTTP 요청으로 수동 알림을 보냈지만, 이제는 Redis Pub/Sub을 사용하여 수강신청 서버가 메시지를 발행하면<br>대기열 서버는 이를 구독 중인 상태에서 자동으로 수신하여 실시간으로 반응할 수 있게 되었다.</p>
<p><img src="https://velog.velcdn.com/images/smj_716/post/363e2c79-cb95-4fb2-aba2-fd8a856794ba/image.png" alt=""></p>
<blockquote>
<p>👉 서버 간 강한 의존 없이 느슨하게 연결되는 구조(Low Coupling)로 바뀌었다.</p>
</blockquote>
</li>
</ul>
<ul>
<li><p><strong>순번 계산이 간단하다.</strong><br><code>LPOS</code>, <code>LRANGE</code> 등을 통해 Redis 내부에서 순번을 계산할 수 있어서 별도의 인덱스 관리가 필요 없기에 대기 순번도 자동으로 이동된다.</p>
</li>
<li><p><strong>BatchManager가 필요 없게 되었다.</strong><br>기존에는 미입장 사용자를 수동으로 관리하기 위해 별도의 로직이 존재하였지만, Redis TTL 구조로 자동 만료가 가능해지면서 이 기능을 제거할 수 있게 되었다.</p>
</li>
</ul>
<hr>
<h2 id="3-시스템-흐름">3. 시스템 흐름</h2>
<p> ➡️ 대기열 접속 시 해당 사용자의 토큰을 Redis List에 저장</p>
<pre><code class="language-java">redisTemplate.opsForList().rightPush(&quot;queue:waiting&quot;, token);</code></pre>
<p>➡️ 사용자에게 SSE를 통해 아래와 같은 메시지를 전송</p>
<pre><code class="language-java">{
  &quot;queueNumber&quot;: 125,  //대기순번
  &quot;status&quot;: &quot;WAITING&quot;
}</code></pre>
<p>➡️ 빈자리 발생 → 입장 로직
수강신청 서버에서 로그아웃, TTL 만료 등 변화 발생 시</p>
<pre><code class="language-java">stringRedisTemplate.convertAndSend(&quot;entrance-channel&quot;, String.valueOf(currentSessionCount));</code></pre>
<p>대기열 서버는 entrance-channel을 구독 중이며 메시지를 수신하면 다음 순서로 로직을 수행한다:</p>
<ul>
<li>Redis에서 LPOP으로 입장 대상 토큰들을 꺼냄</li>
<li>꺼낸 토큰에 대해 SSE로 status:ALLOWED 전송</li>
<li>Redis에 session:{studentId}로 TTL 기반 세션 저장</li>
</ul>
<pre><code class="language-java">stringRedisTemplate.opsForValue().set(
  &quot;session:&quot; + studentId,
  &quot;active&quot;,
  SESSION_TTL,
  TimeUnit.MILLISECONDS
);</code></pre>
<p>TTL이 지나면 자동 삭제되어 별도 정리 필요 없음 → 빈자리 발생으로 간주 가능
또한 수강신청 서버는 현재 인증된 사용자 수를 Redis에서 즉시 조회 가능 </p>
<hr>
<p>Redis 도입은 단순한 성능 향상이 아닌
정확하게 입장시킬 사람을 판단하고, 로그인하지 않은 사용자의 자리를 시스템이 자동으로 회수할 수 있도록 만든 구조 개편이었다.</p>
<p>또한 Pub/Sub 구조 도입을 통해
수강신청 서버와 대기열 서버 간의 의존성을 줄이고 실시간 메시지 전송을 안정적으로 처리할 수 있게 되었다.</p>
<p>✅ <strong>“입장”을 판단하고</strong>
✅ <strong>“알리는 방식”</strong>까지 모두 개선함으로써
수강신청 시스템의 실시간성, 정합성, 확장성이 모두 확보되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[대기열 시스템에 SSE 도입, 비동기 스레드풀 튜닝]]></title>
            <link>https://velog.io/@smj_716/%EB%8C%80%EA%B8%B0%EC%97%B4-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%97%90-SSE-%EB%8F%84%EC%9E%85</link>
            <guid>https://velog.io/@smj_716/%EB%8C%80%EA%B8%B0%EC%97%B4-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%97%90-SSE-%EB%8F%84%EC%9E%85</guid>
            <pubDate>Tue, 22 Jul 2025 11:42:53 GMT</pubDate>
            <description><![CDATA[<h2 id="1-도입-배경">1. 도입 배경</h2>
<p>기존 수강신청 시스템은 Polling 방식으로 동작하고 있었다.
사용자는 일정 간격으로 자신의 대기 상태를 확인하며,
<code>ALLOWED</code> 상태가 되었는지를 계속 서버에 요청해야 했다.</p>
<p>Polling 방식은 단순하고 안정적이라는 장점이 있었지만
대기 인원이 많아질수록 <strong>불필요한 요청이 반복적으로 발생</strong>했고,
서버는 매 요청마다 사용자 상태를 계산하고 응답해야 했다.</p>
<p><strong>&quot;사용자가 굳이 물어보지 않아도 되게 할 수 없을까❓&quot;</strong></p>
<p>그렇게 도입하게 된 구조가 바로 SSE(Server-Sent Events)이다.</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/7bb122fd-210f-4120-9c29-634319137495/image.png" width = "500">



<hr>
<h2 id="2-sse란">2. SSE란?</h2>
<blockquote>
<p>SSE(Server-Sent Events)는 서버가 클라이언트에 단방향으로 실시간 데이터를 푸시하는 기술이다.</p>
</blockquote>
<p>HTTP 기반으로 동작하며 브라우저가 서버와 연결을 유지하고 있다가
서버에서 이벤트가 발생할 때마다 <strong>event-stream</strong>을 통해 바로 데이터를 받을 수 있다.</p>
<p>📌 <strong>특징</strong></p>
<ul>
<li>연결 방식: 단방향 (서버 → 클라이언트)</li>
<li>전송 프로토콜: HTTP + text/event-stream</li>
<li>장점: WebSocket보다 가볍고 브라우저 호환성도 뛰어남</li>
<li>사용 시점: 실시간 알림, 상태 푸시, 실시간 대시보드 등</li>
</ul>
<p>👉 우리는 이 구조를 활용해 <strong>“입장 가능”</strong> 과 <strong>“순번 상태 변경”</strong>을 실시간으로 알려주는 방식으로 전환했다.</p>
<p>➡️ <strong>왜 Polling보다 효율적인가?</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>Polling 방식</th>
<th>SSE 방식</th>
</tr>
</thead>
<tbody><tr>
<td>연결 방식</td>
<td>요청마다 새로 연결</td>
<td>한 번 연결 후 지속 유지</td>
</tr>
<tr>
<td>응답 방식</td>
<td>정해진 주기로 서버 상태를 요청</td>
<td>서버가 이벤트 발생 시 실시간 푸시</td>
</tr>
<tr>
<td>네트워크 사용량</td>
<td>주기적으로 요청 → 트래픽 많음</td>
<td>연결 유지 상태 → 트래픽 적음</td>
</tr>
<tr>
<td>실시간성</td>
<td>일정 간격마다 확인 → 느림</td>
<td>이벤트 발생 즉시 전송 → 빠름</td>
</tr>
<tr>
<td>구현 난이도</td>
<td>간단함 (HTTP GET 반복)</td>
<td>약간 복잡 (Emitter 관리 필요)</td>
</tr>
</tbody></table>
<p>➡️ <strong>Spring에서 SSE 구현 방식</strong>
Spring에서는 <strong>SseEmitter</strong>라는 클래스를 제공한다.
이를 통해 클라이언트에 메시지를 비동기적으로 푸시할 수 있다.</p>
<pre><code class="language-java">@GetMapping(value = &quot;/sse/{uuid}&quot;, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribe(@PathVariable String uuid) {
    return sseQueueService.subscribe(uuid);
}</code></pre>
<ul>
<li><code>MediaType.TEXT_EVENT_STREAM_VALUE</code> → SSE 프로토콜을 사용한다는 의미</li>
<li>연결된 사용자 별로 <strong>SseEmitter 인스턴스를 발급</strong>해 연결 상태를 관리한다.</li>
</ul>
<p><strong>✅ 지속 연결 관리 – EmitterManager</strong>
EmitterManager는 사용자별로 연결된 SSE 통신 채널(SseEmitter)을 관리하는 역할을 한다.</p>
<pre><code class="language-java">private final Map&lt;String, SseEmitter&gt; emitters = new ConcurrentHashMap&lt;&gt;();</code></pre>
<ul>
<li><code>token(UUID)</code>를 키로 사용해 각 사용자의 SseEmitter를 저장</li>
<li><code>ConcurrentHashMap</code>을 사용해 <strong>멀티스레드 환경에서도 안전</strong>하게 동작</li>
</ul>
<p>에러, 타임아웃, 종료 이벤트는 자동으로 감지되며
해당 시점에 <code>emitter.complete()</code> 및 <code>removeEmitter(token)</code>을 호출해 <strong>메모리 누수를 방지</strong>했다.</p>
<hr>
<h2 id="3-구현-흐름">3. 구현 흐름</h2>
<p>전체 흐름은 다음과 같이 변경되었다.</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/31740f88-09b2-4989-a12e-360ee4849ec1/image.png" width = "450">

<p>1️⃣ 사용자가 <code>/join</code>을 통해 대기열에 진입하면
서버는 사용자 정보를 큐에 등록하고 상태 <code>WAITING</code>과 순번을 반환한다.</p>
<p>2️⃣ 1번 과정에서 클라이언트는 SSE에 연결된다.
이제부터 클라이언트가 주기적으로 상태를 요청하지 않아도 서버는 상태가 변경되면 실시간으로 메시지를 보낸다.</p>
<p>3️⃣ 빈자리가 발생하면 수강신청 서버에게 <code>/notify</code> API로 알림을 받고, 큐의 앞쪽에 있는 기준으로 입장 대상자를 선정한다.
해당 사용자들에게 <code>ALLOWED</code> 상태의 메시지를 SSE로 push한다.</p>
<p>4️⃣ ALLOWED 사용자는 로그인 시도
입장 완료 후 SSE 연결 종료!
대기 중인 사용자들 순번을 다시 계산하여 <code>WAITING</code> 상태와 순번을 반환한다.</p>
<hr>
<h2 id="4-비동기-전송-구조-도입">4. 비동기 전송 구조 도입</h2>
<p>SSE는 서버가 클라이언트에게 실시간으로 데이터를 push하는 구조지만
이 전송은 내부적으로 I/O 작업이다.
즉, 데이터를 보내는 순간까지 해당 스레드는 블로킹 상태가 될 수 있다.</p>
<p><strong>만약 여러 사용자에게 동시에 메시지를 보내야 하는 상황이라면❓</strong>
✔️ 각 사용자의 전송이 순차적으로 처리되면 전체 처리 시간이 늦어지고
✔️ 하나의 사용자 전송이 지연되면 나머지 사용자 알림도 지연되는 문제가 생긴다.</p>
<p>그래서 SSE 전송 로직을 별도 클래스로 분리하고 <strong><code>@Async</code>를 통해 비동기 전송이 되도록 리팩토링</strong>했다.</p>
<p>🖥️ <strong>SseAsyncSender.java</strong></p>
<pre><code class="language-java">@Slf4j
@Component
@RequiredArgsConstructor
public class SseAsyncSender {

    private final SseEmitterManager emitterManager;

    @Async(&quot;sseAsyncExecutor&quot;)
    public void send(SseEmitter emitter, String event, Object data, String token) {
        try {
            emitter.send(SseEmitter.event()
                .name(event)
                .data(data));
            log.info(&quot;✅ send 성공!!&quot;);

            if (&quot;allowed&quot;.equals(event)) {
                emitter.complete();
                emitterManager.removeEmitter(token);
                log.info(&quot;✅ emitter 종료 및 제거 완료: token={}&quot;, token);
            }
        } catch (IOException e) {
            log.warn(&quot;🚨 SSE 전송 실패: event={}, message={}&quot;, event, e.getMessage());
        }
    }
}</code></pre>
<ul>
<li><code>@Async(&quot;sseAsyncExecutor&quot;)</code>를 붙여 이 메서드는 별도 스레드풀에서 비동기로 실행</li>
<li>동시에 수십 명에게도 병렬로 빠르게 전송 가능</li>
<li><code>&quot;allowed&quot;</code> 이벤트일 경우 emitter를 종료하고 등록된 emitter도 정리</li>
<li>전송 성공/실패 로그를 통해 상태 추적 가능</li>
</ul>
<p>이 구조가 없었다면… 수천명의 대기자가 동시에 입장이 허용되는 상황에서 사용자 1명의 전송이 지연될 때 나머지 모두가 기다려야 했을지도 모른다..
<strong>🤣 비동기화는 성능을 위한 선택이 아니라 필수였다!</strong></p>
<hr>
<h2 id="5-비동기-스레드풀-조정">5. 비동기 스레드풀 조정</h2>
<p>기존에는 톰캣 기본 스레드풀을 조정해 요청 처리를 분산하고 있었지만
이번에는 SSE 전송 전용 비동기 스레드풀을 별도로 도입하고 튜닝을 진행했다.
왜냐하면 SSE 메시지 전송은 I/O 작업이기 때문에 수많은 사용자에게 메시지를 한꺼번에 보내야 하는 순간 서버의 처리 병목이 생길 수 있기 때문이다.</p>
<p><strong>✅ 비동기 스레드풀이란?</strong>
Spring에서는 <code>@Async</code>를 통해 특정 메서드를 <strong>별도 스레드풀에서 병렬 실행</strong>할 수 있다.
우리는 <code>SseEmitter.send()</code> 전송을 비동기로 처리하기 위해 다음처럼 스레드풀을 정의했다.</p>
<pre><code class="language-java">private static final int CORE_POOL_SIZE = 16;
private static final int MAX_POOL_SIZE = 64;
private static final int QUEUE_CAPACITY = 800;</code></pre>
<p>그리고 이 풀을 <code>sseAsyncExecutor</code>로 지정하여 SSE 전송 전용 비동기 처리 스레드풀로 사용했다.</p>
<ul>
<li><strong>CORE_POOL_SIZE</strong> : 요청이 없어도 유지되는 기본 스레드 수</li>
<li><strong>MAX_POOL_SIZE</strong> : 큐가 가득 찼을 때 생성 가능한 최대 스레드 수</li>
<li><strong>QUEUE_CAPACITY</strong> : 대기 큐 크기. Core를 초과한 요청이 일시적으로 저장되는 공간</li>
</ul>
<p>➡️ <strong>1차 설정 (과대 설정)</strong></p>
<pre><code class="language-java">CORE_POOL_SIZE = 64  
MAX_POOL_SIZE = 256  
QUEUE_CAPACITY = 2000  </code></pre>
<ul>
<li>오히려 오류율이 높았음</li>
<li>CPU 사용량만 높고 실제 전송은 제대로 병렬처리되지 않음</li>
<li>📌<code>t3.large</code> 인스턴스의 성능 한계로 과도한 스레드는 <strong>컨텍스트 스위칭</strong>만 유발함</li>
</ul>
<p>➡️ <strong>2차 → 3차 → 4차... 테스트</strong>
스레드풀의 크기를 단계적으로 줄여가며 오류율을 비교한 결과
오류율이 다시 높아지는 시점을 기준으로 최적점을 찾아낼 수 있었다.
그 결과 실제 배포 환경인 <strong>t3.large 인스턴스</strong>(vCPU 2, RAM 8GB)에 가장 적합한 설정은 다음과 같았다:</p>
<pre><code class="language-java">CORE_POOL_SIZE = 16
MAX_POOL_SIZE = 64
QUEUE_CAPACITY = 800</code></pre>
<p>이 설정은 테스트 과정에서 충분한 병렬 처리 성능을 제공하면서도
불필요한 컨텍스트 스위칭이나 CPU 과소비 없이 안정적인 전송 처리가 가능했다.</p>
<h3 id="🌟중요🌟"><strong>🌟중요🌟</strong></h3>
<p><strong>단순히 스레드를 많이 할당한다고 성능이 오르지 않는다는 점을 실험을 통해 직접 확인할 수 있었다.
서버 리소스와 작업 특성을 고려한 실험적 튜닝이 가장 중요하다!!</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[아파치 카프카의 미래]]></title>
            <link>https://velog.io/@smj_716/%EC%95%84%ED%8C%8C%EC%B9%98-%EC%B9%B4%ED%94%84%EC%B9%B4%EC%9D%98-%EB%AF%B8%EB%9E%98</link>
            <guid>https://velog.io/@smj_716/%EC%95%84%ED%8C%8C%EC%B9%98-%EC%B9%B4%ED%94%84%EC%B9%B4%EC%9D%98-%EB%AF%B8%EB%9E%98</guid>
            <pubDate>Sun, 20 Jul 2025 07:46:46 GMT</pubDate>
            <description><![CDATA[<h2 id="1-클라우드-기반-아파치-카프카-서비스">1. 클라우드 기반 아파치 카프카 서비스</h2>
<p>카프카는 기본적으로 자가 호스팅이 가능한 오픈소스 메시징 플랫폼이지만, 운영 및 유지보수에 대한 부담을 줄이기 위해 <strong>SaaS(Software as a Service)</strong> 형태로 제공되는 서비스들도 존재한다. 대표적으로는 AWS의 MSK(Amazon Managed Streaming for Apache Kafka)와 <strong>Confluent Cloud</strong>가 있다.</p>
<p>Confluent Cloud는 카프카의 핵심 개발자인 제이 크렙스가 창업한 기업에서 운영하는 공식 관리형 서비스이다. Kafka 클러스터의 설치, 구성, 운영, 확장, 보안 등을 모두 <strong>웹 기반</strong>으로 관리할 수 있도록 해준다.</p>
<p>🌟 <strong>장점</strong></p>
<ul>
<li>Zookeeper, Broker, Topic, Connect 등 인프라 구성을 직접 하지 않아도 됨</li>
<li>Kafka 클러스터의 생성을 클릭 몇 번으로 완료할 수 있고 토픽 생성, 삭제, Connect 설정, Schema Registry 관리 등도 웹 UI 기반으로 수행 가능</li>
<li>AWS, GCP, Azure 등 원하는 클라우드 환경에서 실행 가능</li>
<li>Kafka를 직접 운영할 때 흔히 발생하는 설정 이슈(토픽 리텐션, 파티션 조정, ACL 등)나 운영 노하우 부족에서 오는 시행착오를 줄일 수 있음</li>
</ul>
<p>REST API를 통해 Kafka Connect를 <strong>동적으로 설정</strong>할 수 있기 때문에, 지속적으로 커넥터 기반 파이프라인을 확장해야 하는 환경에서는 직접 개발 없이도 <strong>자동화된</strong> 구성이 가능하다.
✔️ Confluent Cloud와 같은 SaaS 기반 Kafka 서비스를 도입하면 스트리밍 플랫폼 운영에 대한 부담을 줄이고 실제 데이터 처리와 서비스 로직 구현에 집중할 수 있는 개발 환경을 구축할 수 있다‼️</p>
<hr>
<h2 id="2-빅데이터-플랫폼-아키텍처와-카프카">2. 빅데이터 플랫폼 아키텍처와 카프카</h2>
<p>➡️ <strong>초기 빅데이터 처리 방식</strong>
각 서비스 애플리케이션으로부터 데이터를 <strong>배치방식</strong>으로 수집해 <strong>일괄 처리</strong>하는 구조였다.
⚠️ 실시간 데이터 반영이 느리고, 파편화된 데이터로 인해 데이터의 흐름과 히스토리를 추적하기 어려운 단점이 있었다.
+개인정보 보호, 데이터 품질, 생명주기 등을 체계적으로 관리하는 <strong>데이터 거버넌스</strong> 측면에서도 유연성이 부족했다.</p>
<p>➡️ <strong>람다 아키텍처</strong></p>
<img src = "https://velog.velcdn.com/images/smj_716/post/cd9767ad-993d-4eb1-ab8b-1807ac4567c1/image.png" width = "350">

<p>람다 아키텍처는 다음 세 가지 계층으로 구성된다:</p>
<ul>
<li><strong>배치 레이어</strong>: 과거 데이터를 모아 일정 주기로 일괄 처리 
ex) Spark job</li>
<li><strong>스피드 레이어</strong>: 실시간 데이터 분석을 위한 계층 ex) Kafka</li>
<li><strong>서빙 레이어</strong>: 처리된 데이터를 외부 서비스가 접근할 수 있도록 저장</li>
</ul>
<p>실시간성과 정확도를 동시에 만족시키기 위한 목적이었지만
⚠️ 동일한 로직을 배치/실시간에 각각 구현해야 한다는 이중 관리 문제
디버깅, 배포, 모니터링 역시 두 시스템을 별도로 다뤄야 하는 번거로움이 있었다.</p>
<p>➡️ <strong>카파(Kappa) 아키텍처</strong></p>
<img src = "https://velog.velcdn.com/images/smj_716/post/afafb37c-d7c6-4808-86bc-c3b862b09885/image.png" width = "350">

<p>배치 레이어를 제거하고 <strong>모든 데이터를 스트리밍 기반의 스피드 레이어에서 처리하도록 구성</strong>한다.
즉 Kafka를 중심으로 운영하는 것이다. 스트리밍 처리 하나로 데이터 파이프라인을 단순화할 수 있다는 장점이 있다.</p>
<p>➡️ <strong>Streaming Data Lake 아키텍처</strong>
<img src = "https://velog.velcdn.com/images/smj_716/post/183b0f7d-e4f7-44d4-9383-003b00074986/image.png" width = "300"></p>
<p>서빙 레이어마저 제거! Kafka를 일시적인 메시지 큐가 아닌 <strong>장기 저장과 쿼리 분석이 가능한 데이터 레이크</strong>처럼 사용하자는 발상!</p>
<p>💡 이를 위해선 두가지를 갖추어야한다.</p>
<ol>
<li>스트리밍 데이터를 배치처럼 조회할 수 있어야 한다</li>
<li>자주 조회하지 않는 데이터는 저렴한 저장소로 자동 이전하는 기능이 필요하다</li>
</ol>
<p>이러한 개선이 아직 진행 중이긴 하지만 Kafka가 스트리밍부터 저장,분석까지 아우르는 중심 플랫폼으로 진화하고 있다는 흐름은 분명하다.</p>
<p>가까운 미래에는 Kafka가 단순한 메시지 브로커가 아니라 <strong>완전한 빅데이터 플랫폼의 핵심 엔진</strong>으로 자리 잡을 가능성이 높다고 한다.</p>
<hr>
<h2 id="3-아파치-카프카의-미래">3. 아파치 카프카의 미래</h2>
<p>카프카는 왜 이렇게까지 많이 쓰일까? 그리고 앞으로도 계속 쓰일까? 
결론은 🅾️
이미 너무 많은 기업들이 <strong>핵심 데이터 파이프라인</strong>에 카프카를 사용하고 있다.
원래 카프카는 링크드인 내부에서 여러 메시지 시스템들을 붙여 쓰다가 “너무 복잡하고 유지보수 힘들다”는 이유로 만들기 시작한 거였다.
그때의 문제는 지금도 여전히 많은 기업들이 겪고 있는 문제다. 
그래서 Kafka는 지금도 &quot;필요한 기술&quot;로 받아들여진다.</p>
<p> 🌟특히 좋은 점은</p>
<ul>
<li><strong>데이터를 엄청 빠르게 보낼 수 있다.</strong></li>
<li><strong>서버만 늘리면 확장도 쉽다.</strong></li>
<li><strong>컨슈머가 읽어가도 데이터가 사라지지 않아서 여러 팀에서 동시에 써도 괜찮다.</strong></li>
</ul>
<p>이런 특성 덕분에 카프카는 네이버, 카카오 같은 대기업은 물론 빠르게 성장하는 스타트업에게도 많은 관심을 받고 있다고 한다.
초기에는 작게 시작해서 쓰다가 나중에 서비스가 커지면 그대로 확장하면 되니까 부담도 없다.</p>
<p>📢 요즘은 Kafka 본체만 쓰는 게 아니라
-&gt; Kafka Streams, Connect, ksqlDB 같은 기능도 계속 늘어나고 있다.
심지어 웹에서 토픽을 만들고, 커넥터를 붙이고, 모니터링까지 할 수 있는 SaaS 서비스도 나와 있다.</p>
<p>즉, 카프카는 단순한 메시지 큐가 아니라** 실시간 데이터 플랫폼으로 계속 진화 중**이다.
앞으로도 실시간 로그, 트래픽 분석, 추천 시스템처럼 데이터를 빨리 다뤄야 하는 곳에서는 더 자주 쓰이게 될 거다.</p>
<p>✔️ <strong>결론?</strong>
개발자로 일한다면 언젠가는 꼭 마주하게 될 기술이니까 가볍게라도 알아두자!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[아파치 카프카 개발]]></title>
            <link>https://velog.io/@smj_716/%EC%95%84%ED%8C%8C%EC%B9%98-%EC%B9%B4%ED%94%84%EC%B9%B4-%EA%B0%9C%EB%B0%9C</link>
            <guid>https://velog.io/@smj_716/%EC%95%84%ED%8C%8C%EC%B9%98-%EC%B9%B4%ED%94%84%EC%B9%B4-%EA%B0%9C%EB%B0%9C</guid>
            <pubDate>Sat, 19 Jul 2025 13:00:19 GMT</pubDate>
            <description><![CDATA[<h2 id="1-aws에-카프카-클러스터-설치-실행">1. AWS에 카프카 클러스터 설치, 실행</h2>
<p><a href="https://blog.voidmainvoid.net/325">https://blog.voidmainvoid.net/325</a></p>
<hr>
<h2 id="2-카프카-프로듀서-애플리케이션">2. 카프카 프로듀서 애플리케이션</h2>
<p> 👉 <code>Producer</code> <strong>역할</strong></p>
<ul>
<li>Topic에 해당하는 메시지 생성</li>
<li>특정 Topic으로 데이터를 publish</li>
<li>처리 실패/재시도</li>
</ul>
<p>⚠️ 카프카는 브로커 버전과 클라이언드 버전의 <strong>호환성을 꼭 확인</strong>해야한다.</p>
<img src="https://velog.velcdn.com/images/smj_716/post/24a3915b-bcfe-4401-9d9a-144122d7716f/image.png" width = "200">

<p>🖥️ <strong>Producer.java</strong></p>
<pre><code class="language-java">Properties configs = new Properties();
configs.put(&quot;bootstrap.servers&quot;, &quot;localhost:9092&quot;);
configs.put(&quot;key.serializer&quot;, &quot;org.apache.kafka.common.serialization.StringSerializer&quot;);
configs.put(&quot;value.serializer&quot;, &quot;org.apache.kafka.common.serialization.StringSerializer&quot;);</code></pre>
<ul>
<li>Kafka 서버 주소와 직렬화 방식 설정</li>
<li>실제 서비스에서는 <strong>2개 이상의 브로커</strong> 주소를 넣는 것을 권장!</li>
</ul>
<pre><code class="language-java">KafkaProducer&lt;String, String&gt; producer = new KafkaProducer&lt;&gt;(configs);
ProducerRecord record = new ProducerRecord&lt;&gt;(&quot;click_log&quot;, &quot;login&quot;);
producer.send(record);
producer.close();</code></pre>
<ul>
<li>위 설정을 기반으로 <strong><code>KafkaProducer</code> 객체</strong> 생성</li>
<li>click_log 토픽에 &quot;login&quot; 메시지 전송</li>
<li><strong>키를 지정하지 않으면</strong> Kafka가 파티션을 자동으로 분배</li>
<li><code>.send()</code>로 메시지를 <strong>비동기</strong> 전송</li>
<li><code>.close()</code>는 리소스를 해제하는 필수 작업</li>
</ul>
<img src="https://velog.velcdn.com/images/smj_716/post/f8ef88ff-43c5-4f2a-a6ac-72ff5a104b30/image.png" width = "400">

<p>⚠️ send에 key값을 &quot;1&quot;,&quot;2&quot;와 같이 설정을 한다면 그림과 같이 데이터가 나뉘어지지만, 파티션이 하나 추가된다면 key와 파티션 매칭이 깨져서 <strong>key &lt;-&gt; 파티션 일관성이 보장되지 않는다.</strong></p>
<hr>
<h2 id="3-카프카-컨슈머-애플리케이션">3. 카프카 컨슈머 애플리케이션</h2>
<p>👉 <code>Consumer</code> <strong>역할</strong></p>
<ul>
<li>Topic의 partition에서 데이터 polling</li>
<li>Partition offset 위치 기록 (commit)</li>
<li>Consumer group을 통해 병렬처리</li>
</ul>
<p>🖥️ <strong>Consumer.java</strong></p>
<pre><code class="language-java">configs.put(&quot;bootstrap.servers&quot;, &quot;localhost:9092&quot;);
configs.put(&quot;group.id&quot;, &quot;click_log_group&quot;);
configs.put(&quot;key.deserializer&quot;, &quot;org.apache.kafka.common.serialization.StringDeserializer&quot;);
configs.put(&quot;value.deserializer&quot;, &quot;org.apache.kafka.common.serialization.StringDeserializer&quot;);</code></pre>
<ul>
<li>Kafka 브로커 주소 및 컨슈머 그룹 ID 설정</li>
<li>Key/Value 디시리얼화 -&gt; Kafka 메시지 읽기 준비 완료!</li>
</ul>
<pre><code class="language-java">KafkaConsumer&lt;String, String&gt; consumer = new KafkaConsumer&lt;&gt;(configs);
consumer.subscribe(Arrays.asList(&quot;click_log&quot;));</code></pre>
<ul>
<li>&quot;click_log&quot; 토픽을 구독</li>
<li><code>subscribe()</code>는 전체 파티션을 자동 분배받음</li>
<li>특정 파티션만 읽고 싶다면 <code>assign()</code>을 사용해야 함</li>
</ul>
<pre><code class="language-java">while (true) {
    ConsumerRecords&lt;String, String&gt; records = consumer.poll(Duration.ofMillis(500));
    for (ConsumerRecord&lt;String, String&gt; record : records) {
        System.out.println(record.value());
    }
}</code></pre>
<ul>
<li>0.5초 동안 메시지를 기다렸다가 처리</li>
<li>한 번에 여러 개의 메시지를 받아오고 <code>record.value()</code>로 개별 처리</li>
<li>실제 서비스에서는 DB, 로그 저장 등으로 확장 가능</li>
</ul>
<img src="https://velog.velcdn.com/images/smj_716/post/c48d303b-f971-4426-8750-242759059431/image.png" width = "400">

<p>⚠️ 컨슈머가 파티션이 2개인 click_log 토픽에서 데이터를 가져가는 중에 실행이 중지되어도, 어느 offset까지 읽었는지 <strong>consumer_offset</strong> 에 저장되어 있어 오류 시작위치부터 다시 복구할 수 있다.
= ✔️<strong>고가용성의 특징</strong></p>
<p>⚠️여러 파티션에 대해 병렬처리를 원한다면 <strong>컨슈머 수는 파티션 수보다 적거나 같아야한다.</strong></p>
<p>⚠️하나의 토픽에 들어온 데이터는 여러 컨슈머 그룹이 독립적으로 소비하여 <strong>서로 간섭 없이 각자의 역할에 따라 처리</strong>한다.</p>
<hr>
<h2 id="4-카프카-스트림즈-애플리케이션">4. 카프카 스트림즈 애플리케이션</h2>
<p>Kafka Streams는 카프카에서 공식으로 제공하는 Java 기반의 스트림 처리 라이브러리이다.
기존의 Kafka Consumer를 사용하는 방식보다 더 빠르고 안정적인 데이터 처리가 가능하고 실시간 데이터 파이프라인 구축에 적합하다.</p>
<p>✅ <strong>Kafka와 완벽하게 호환</strong>
Kafka Streams는 Kafka의 버전에 맞춰 함께 업데이트되므로 보안 기능이나 트랜잭션 처리 등 최신 기능과의 호환성이 뛰어나다.
-&gt; 즉 <strong>Kafka 클러스터와 항상 완벽하게 맞물려 돌아가는 구조!!</strong>
+<strong>Exactly Once Processing</strong>(정확히 한 번 처리) 기능을 기본 지원해 데이터 유실이나 중복 없이 안전하게 처리 가능</p>
<p>✅ <strong>별도의 클러스터 없이 운영 가능</strong>
컨슈머처럼 애플리케이션 <strong>단독으로 배포</strong>하면 된다.
-&gt; 즉, 클러스터나 스케줄러 없이도 운영이 가능하다는 점에서 구조가 단순하고 유연!!
데이터 양이 적다면 애플리케이션 몇 개만 띄워도 충분하고 처리량에 따라 자연스럽게 수평 확장할 수 있다.</p>
<p>✅ <strong>DSL과 Processor API 제공</strong>
스트림 처리에 필요한 대부분의 기능을 DSL로 제공한다.
ex) <code>filter</code>, <code>map</code>, <code>join</code>, <code>window</code> 등은 <strong>모두 간단한 메서드 호출로 구현</strong> 가능
-&gt; 복잡한 로직이 필요한 경우에는 Processor API를 통해 직접 로직을 정의해 더 세밀한 제어도 가능하다.</p>
<hr>
<h2 id="5-카프카-커넥트">5. 카프카 커넥트</h2>
<p>Kafka에서 공식적으로 제공하는 <strong>데이터 파이프라인 컴포넌트</strong>이다.
데이터 송수신을 반복적으로 처리해야 하는 환경에서는 도입을 적극 검토할 만한 플랫폼이다.</p>
<p>➡️ <strong>커넥트 vs 커넥터</strong></p>
<ul>
<li>Connect는 커넥터를 실행시키는 플랫폼 역할, 일종의 실행환경(Processor)</li>
<li>Connector는 실제 데이터를 송수신하는 <strong>로직이 담긴 코드 뭉치(JAR 패키지)</strong></li>
</ul>
<p>➡️ <strong>커넥터의 종류</strong></p>
<ul>
<li><strong>Source Connector</strong>: 외부 데이터 소스(DB 등) → Kafka 토픽으로 데이터를 보내는 역할 (프로듀서 역할)</li>
<li><strong>Sink Connector</strong>: Kafka 토픽 → 외부 저장소(DB, Elasticsearch 등)로 데이터를 저장하는 역할 (컨슈머 역할)</li>
<li>ex) 오라클 DB 데이터를 Kafka로 가져오고 싶다면 → Oracle Source Connector</li>
</ul>
<p>➡️ <strong>커넥트 실행 방식</strong></p>
<ul>
<li><strong>단일 모드</strong> (Standalone Mode): 개발/테스트용으로 간단히 실행 가능</li>
<li><strong>분산 모드</strong> (Distributed Mode): 프로덕션 환경에서 사용하는 방식
여러 커넥트 인스턴스를 클러스터로 구성하여 <strong>장애 발생 시 자동 복구(Failover)</strong>가 가능하다.</li>
</ul>
<p>➡️ <strong>실행 방법: REST API로 간단하게</strong>
커넥트는 REST API 기반으로 커넥터를 실행할 수 있다.
즉, 커넥터 코드를 별도로 배포하거나 다시 개발할 필요 없이 <strong>JSON 설정 파일</strong> 하나로 파이프라인을 반복적으로 생성한다.</p>
<p>ex) 동일한 Kafka 토픽 데이터를 오라클 테이블 A, 테이블 B에 각각 저장하고 싶다면? JSON 설정을 두 개 만들고 REST API로 두 번 요청만 하면 되는 것이다.</p>
<p>-&gt; 복잡한 배포, 컨슈머 개발 없이 템플릿 기반으로 빠르고 효율적인 데이터 흐름 구성이 가능해진다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[아파치 카프카 기초]]></title>
            <link>https://velog.io/@smj_716/%EC%95%84%ED%8C%8C%EC%B9%98-%EC%B9%B4%ED%94%84%EC%B9%B4-%EA%B8%B0%EC%B4%88</link>
            <guid>https://velog.io/@smj_716/%EC%95%84%ED%8C%8C%EC%B9%98-%EC%B9%B4%ED%94%84%EC%B9%B4-%EA%B8%B0%EC%B4%88</guid>
            <pubDate>Fri, 18 Jul 2025 13:48:27 GMT</pubDate>
            <description><![CDATA[<h2 id="1-아파치-카프카-개요">1. 아파치 카프카 개요</h2>
<p>서비스 초기에는 <strong><code>Source Application</code></strong>과 <strong><code>Target Application</code></strong>이 직접 연결된 <strong>단방향</strong> 구조를 사용한다. 
하지만 애플리케이션 수가 늘어날수록 각 애플리케이션 간 데이터 흐름이 복잡해지고 포맷 파편화나 배포 충돌, 장애 대응의 어려움 같은 문제가 발생한다.</p>
<p>이러한 문제를 해결하기 위해 등장한 것이 <strong>Apache Kafka</strong>이다‼️
<img src = "https://velog.velcdn.com/images/smj_716/post/0f18be6f-ef10-4132-ba1f-7e6161eb0b21/image.png" width = "400"></p>
<p>Kafka는 <strong>메시지 브로커(message broker)</strong>로서 Source와 Target 사이에 위치한다.
즉 데이터 생산자와 소비자 간의 결합도를 낮추는 역할을 수행하는 것이다. 
👉 <strong><code>Source Application</code></strong>은 데이터를 Kafka에 <strong>전송</strong>하고
👉 <strong><code>Target Application</code></strong>은 Kafka에서 원하는 데이터를 <strong>구독</strong>한다.</p>
<hr>
<h2 id="2-토픽이란">2. 토픽이란?</h2>
<p>Kafka에서 <strong>데이터를 구분하고 저장</strong>하기 위해 사용하는 논리적인 단위이다.</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/07d16a6b-89ea-410c-bb0c-d2b0f3220930/image.png" width = "420">

<p>데이터베이스의 테이블, 파일 시스템의 폴더처럼 데이터의 카테고리를 나누는 개념이다.</p>
<ul>
<li><p>즉 <code>Producer</code>는 특정 Topic에 데이터를 보내고 <code>Consumer</code>는 해당 Topic에서 데이터를 구독해 처리한다.</p>
</li>
<li><p>Topic은 목적에 따라 이름을 명확하게 짓는 것이 좋다 🔽</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/466a3ac1-3f51-4d23-9b81-68cff9ff835f/image.png" width = "400">

</li>
</ul>
<h4 id="partition이란❓">Partition이란❓</h4>
<p>Kafka에서 <strong>데이터의 저장과 처리 단위를 나누기 위한</strong> 물리적 단위이다. 
하나의 Topic은 내부적으로 여러 개의 Partition으로 나뉜다.</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/3cb98584-179d-4db7-8494-d94458910412/image.png" width = "500">

<p>각 Partition은 <strong>append-only</strong> 로그 구조로 데이터를 <strong>순차적으로 저장</strong>한다.
Consumer는 가장 <strong>오래된 데이터부터</strong> 순서대로 읽어간다.
데이터를 읽은 후에도 <strong>삭제되지 않고</strong> 새로운 Consumer가 붙으면 다시 읽을 수 있다.</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/e7ea918c-264c-4456-98d2-f2740a989d68/image.png" width = "500">

<p>🌟 Partition 수만큼 Consumer를 <strong>병렬</strong>로 붙일 수 있어 성능 확장성이 뛰어나다.</p>
<p>✅ <strong>메시지 분배 방식</strong>
producer가 특정 Partition에 분배할 때는</p>
<ul>
<li><strong>라운드 로빈</strong> : 키를 지정하지 않은 경우, 순차적으로 Partition에 분배</li>
<li><strong>키 기반</strong> : 해시 메시지에 키가 있는 경우, 해시값을 기준으로 특정 Partition에 분배</li>
</ul>
<p>메시지 <strong>삭제 방법</strong>은 아래와 같은 설정으로 처리한다.
<code>log.retention.ms</code>    메시지 보존 최대 시간 (기본값: 7일)
<code>log.retention.bytes</code> 최대 저장 크기</p>
<hr>
<h2 id="3-브로커-복제-isrin-sync-replication">3. 브로커, 복제, ISR(In-Sync-Replication)</h2>
<p>➡️ <strong>broker란?</strong>
Kafka가 설치되어 있는 서버 단위</p>
<p><strong>복제</strong>는 하나의 파티션 데이터를 여러 브로커에 복제 저장하는 방식이다. 이를 통해 고가용성(HA)을 확보할 수 있다.
예를 들어, 파티션 수가 1이고 replication이 3이면 다음과 같은 구조가 된다.</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/b41f3f8f-ab73-403e-ae8a-771494c6262c/image.png" width = "450">

<ul>
<li><code>partition</code>: 1 → 실제 저장할 파티션은 하나</li>
<li><code>replication</code>: 3 → 총 3개의 브로커에 해당 파티션이 복제됨</li>
</ul>
<p>⚠️ 즉 브로커 개수가 3이면 replication이 4가 될 수 없음 </p>
<ul>
<li><strong>Leader</strong>: 실제로 Producer로부터 데이터를 받아 저장하는 주체</li>
<li><strong>Follower</strong>: Leader의 데이터를 따라 복제만 수행함</li>
<li><strong>Producer</strong>는 오직 <strong>Leader</strong> 파티션에만 데이터 전송을 시도함</li>
</ul>
<p>➡️ <strong>ISR이란?</strong> 
Leader와 동기화된 상태의 Follower들 집합을 의미한다.
만약 Leader가 장애로 중단되면, ISR 내에서 가장 적합한 Follower가 새로운 Leader 역할을 승계한다. 이를 통해 무중단 운영 및 데이터 손실 없는 전환이 가능해진다.</p>
<p>➡️ <strong>ack 옵션</strong>
Producer는 데이터를 보낼 때 ack 옵션을 설정할 수 있고 이는 데이터 전송에 대한 신뢰 보장 수준을 정한다.</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/61481d83-0ca8-4682-92b8-624d9caa2ad6/image.png" width = "500">

<table>
<thead>
<tr>
<th>옵션 값 (<code>acks</code>)</th>
<th>동작 방식</th>
<th>데이터 신뢰성</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><code>0</code></td>
<td>Producer가 Leader에게 데이터 전송 후 <strong>응답 없이 종료</strong></td>
<td>매우 낮음</td>
<td>매우 빠르지만, 데이터 손실 위험 존재</td>
</tr>
<tr>
<td><code>1</code></td>
<td>Leader가 데이터를 받으면 <strong>즉시 응답</strong></td>
<td>중간</td>
<td>Leader까지만 확인 (Follower 복제 미보장)</td>
</tr>
<tr>
<td><code>all</code></td>
<td>모든 ISR(In-Sync Replica)에 <strong>복제 완료 후 응답</strong></td>
<td>매우 높음</td>
<td>가장 안전하지만, 가장 느린 방식</td>
</tr>
</tbody></table>
<p>❓<strong>그럼 복제 수는 많을수록 좋을까?</strong>
복제 수가 많을수록 안정성은 높아지지만 브로커 리소스를 더 많이 사용하게 된다.
Kafka 클러스터의 브로커 수, 데이터 처리량, 보관 시간 등을 고려하여 적절한 복제 수를 설정하는 것이 중요하다!!</p>
<hr>
<h2 id="4-파티셔너partitioner란">4. 파티셔너(Partitioner)란?</h2>
<p>레코드에 포함된 메시지 키 또는 값에 따라서 <strong>어떤 파티션에 넣을지 결정</strong>하는 역할을 한다.</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/fe0191f1-d790-4fee-88d4-c9375cde1452/image.png" width = "120">

<p>Producer를 사용할때 파티셔너를 따로 설정하지 않으면 <code>UniformStickyPartitioner</code>로 설정된다. 
이것은 메시지 키가 있을때와 없을때 다르게 동작한다. 
<strong>👉 메시지 키 있을 때</strong></p>
<ul>
<li>파티셔너에 의해 특정한 해시값 생성</li>
<li>동일한 키 -&gt; 동일한 해시값 -&gt; 동일한 파티션에 들어감 </li>
<li>즉, <strong>순서를 지켜 데이터 처리</strong>한다는 장점이 있음</li>
</ul>
<p><strong>👉 메시지 키 없을 때</strong></p>
<ul>
<li>프로듀서에서 배치로 모을 수 있는 최대한의 레코드를 모아 파티션으로 보냄</li>
<li><strong>라운드 로빈 방식</strong></li>
</ul>
<p>📢 카프카는 <strong>커스텀 파티셔너</strong>를 만들 수 있도록 파티션 인터페이스를 제공한다.</p>
<ul>
<li>메시지 키,값,토픽이름에 따라 어느 파티션에 데이터를 보낼지 결정 가능</li>
<li><strong>ex)</strong> vip 고객의 데이터를 더 빠르게 처리해야하는 상황 -&gt; 10개의 파티션 중 8개를 vip 고객 데이터를 처리하도록 함</li>
</ul>
<hr>
<h2 id="5-컨슈머-랙consumer-lag이란">5. 컨슈머 랙(Consumer Lag)이란?</h2>
<img src = "https://velog.velcdn.com/images/smj_716/post/6f6a88b3-bbaf-4fa6-b5b0-3600548c5159/image.png" width = "200">

<p>파티션에 데이터가 하나씩 들어갈 때 <strong>오프셋</strong>이라는 숫자가 붙게된다. Producer가 데이터를 넣는 속도가 Consumer가 가져가는 속도보다 빠르다면 👉 <strong><code>Producer</code>가 넣은 데이터의 오프셋과 <code>Concumer</code>가 가져간 데이터의 오프셋에</strong> <strong>차이가 발생</strong>한다. 이것이 <strong>Consumer lag</strong>이다. </p>
<ul>
<li>lag 숫자를 통해 해당 토픽에 파이프라인으로 연계되어 있는 Producer, Consumer 상태 유추가 가능<ul>
<li>주로 Consumer 상태에 대해 볼 때 사용</li>
</ul>
</li>
</ul>
<p><strong>만약 여러 파티션이 존재한다면❓</strong>
한개의 토픽에 여러 파티션이 존재한다면 여러 lag가 존재하며, 그 중 높은 숫자의 lag를 <strong>records-lag-max</strong>라고 부른다.</p>
<hr>
<h2 id="6-컨슈머-랙-모니터링-애플리케이션-카프카-버로우burrow">6. 컨슈머 랙 모니터링 애플리케이션, 카프카 버로우(Burrow)</h2>
<p><strong>Cusumer lag 모니터링</strong>을 도와주는 독립적인 애플리케이션</p>
<p>아래와 같은 3가지 큰 특징이 존재한다 :
➡️ <strong>멀티 카프카 클러스터 지원</strong>
   카프카 클러스터가 여러개라도 Burrow 한개만 실행해서 연동한다면 카프카 클러스터들에 붙은 컨슈머의 lag를 모두 모니터 가능</p>
<p>➡️ <strong>Sliding window를 통한 Consumer의 status 확인</strong>
status를 &#39;ERROR&#39;, &#39;WARNING&#39;, &#39;OK&#39;로 표현
<code>WARNING</code>: 데이터가 일시적으로 많아져 프로듀서 offset이 컨슈머 offset보다 더 빠르게 증가
<code>ERROR</code>: 데이터의 양이 많은데 컨슈머가 다 가져가지 못하는 경우</p>
<p>➡️ <strong>HTTP api 제공</strong>
response 받은 데이터를 시계열DB 같은 곳에 저장하는 application을 만들어서 사용할 수도 있음</p>
<hr>
<h2 id="7-카프카-레빗엠큐-레디스-큐의-차이점">7. 카프카, 레빗엠큐, 레디스 큐의 차이점</h2>
<p>메시징 플랫폼이라고 부르는 것들은 두가지로 나뉜다.
✔️ <strong>메시지 브로커</strong>
데이터를 보내고 처리하고 삭제한다.
ex) 레빗엠큐, 레디스 큐</p>
<p>✔️ <strong>이벤트 브로커</strong>
데이터 처리 후 삭제하지 않고 이벤트 브로커의 큐에 저장한다.
ex) 카프카, AWS의 키네시스</p>
<ul>
<li>단일 진실 공급원으로 사용 가능</li>
<li>장애가 발생했을 때 장애가 일어난 시점부터 처리 가능</li>
<li>많은 양의 실시간 데이터를 효과적으로 처리</li>
</ul>
<p>⚠️이벤트 브로커는 메시지 브로커로 역할을 할 수 있지만 반대는 불가하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[대기열 서버 도입: polling 사용 로직과 테스트 결과]]></title>
            <link>https://velog.io/@smj_716/2pb7cbkl</link>
            <guid>https://velog.io/@smj_716/2pb7cbkl</guid>
            <pubDate>Wed, 16 Jul 2025 09:05:52 GMT</pubDate>
            <description><![CDATA[<h2 id="1-문제-시작-서버가-터진다💥">1. 문제 시작: 서버가 터진다💥</h2>
<p>우리 팀은 일반적인 MVC 구조로 수강신청 서비스를 개발했다.</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/06784dc6-42c1-472a-95c9-83ba640093b5/image.png" width = "500">

<p>사용자는 로그인한 뒤, 바로 수강신청 API를 호출하는 방식이었다.
처음엔 잘 작동했지만 JMeter 테스트에서 수천명이 동시에 접속할때 CPU와 DB 커넥션 풀이 빠르게 고갈되었고 서버는 병목 상태에 빠져 오류율이 컸다.</p>
<p>💡가장 먼저 든 생각은 <strong>“일단 한 번에 들어오지 않도록 막자”</strong>였다.
단순하게 생각하면, 입장할 수 있는 인원을 정해두고 그 외의 사람은 기다리게 하면 된다.
그렇다면 <strong>누가 기다려야 하고 누가 들어올 수 있는지 판단하는 서버가 따로 필요</strong>해진다.</p>
<p>🔽 그래서 수강신청 서버와는 별도로 대기열 서버를 구성하기로 했다.</p>
<hr>
<h2 id="2-대기열-시스템-도입">2. 대기열 시스템 도입</h2>
<p>기능을 나누면 아래와 같다.</p>
<ul>
<li><strong>대기열 서버</strong> : 사용자 순서 관리 및 입장 여부 판단 담당</li>
<li><strong>수강신청 서버</strong> : 자리 발생 시 대기열 서버에게 알림, 실제 수강신청 DB 처리 담당</li>
</ul>
<p>💡그런데 한가지 의문점이 들었다.
<strong>그럼 입장 가능하다는 건 사용자에게 어떻게 알려주지❓</strong> 
클라이언트가 아래와 같이 <strong>대기열 시스템에 일정한 주기로 계속 물어보면 되지 않을까?</strong></p>
<blockquote>
<p>“지금 들어가도 되나요?”
&quot;지금은요?”</p>
</blockquote>
<p>그게 바로 <strong>Polling 방식</strong>이었다.</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/fbc5f3c7-d4d5-4cd1-a700-8348096bf29c/image.png" width = "500">

<p>polling 방식 적용 후 아키텍처는 위와 같이 바뀌었다.
자세한 로직 🔽</p>
<hr>
<h2 id="3-polling-방식-적용">3 Polling 방식 적용</h2>
<h3 id="🌟-내부-로직-흐름">🌟 내부 로직 흐름</h3>
<img src = "https://velog.velcdn.com/images/smj_716/post/a9c9bef0-62e7-450c-90e8-a9f4e05db755/image.png" width = "400">

<p><strong>1. 클라이언트 대기열 진입</strong>
클라이언트는 <code>POST /join</code> API를 통해 대기열에 진입한다.
서버는 토큰 기반으로 사용자 정보를 생성해 큐에 저장한다.
사용자에게 현재 대기 순번을 응답한다.</p>
<p><strong>2. 상태 확인 (Polling)</strong>
클라이언트는 <code>GET /{uuid}</code> API를 일정 주기로 호출해 자신의 상태를 확인한다.
서버는 <code>WAITING</code> 또는 <code>ALLOWED</code> 상태와 남은 순번을 반환한다.
클라이언트는 상태가 <code>ALLOWED</code>가 될 때까지 polling을 계속한다.</p>
<p><strong>3. 빈자리 발생 시 수강신청 서버 → 대기열 서버 알림</strong>
수강신청 서버는 빈자리를 감지하면 <code>POST /notify</code> API를 호출해서
앞 순서 n명의 사용자에게 입장 허용 요청을 보낸다.
대기열 서버는 해당 인원을 <code>ALLOWED</code> 상태로 바꾼다.</p>
<p><strong>4. ALLOWED 사용자 로그인</strong>
<code>ALLOWED</code> 상태가 된 사용자만 수강신청 서버에 로그인할 수 있다.
로그인 시 해당 사용자는 큐에서 제거된다.</p>
<h3 id="🔍-내부-구성-요약">🔍 내부 구성 요약</h3>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>QueueManager</code></td>
<td>대기열 큐 및 순번 계산 관리</td>
</tr>
<tr>
<td><code>queue</code></td>
<td>전체 대기열을 저장하는 큐 (<code>Queue&lt;QueueUser&gt;</code>)</td>
</tr>
<tr>
<td><code>userMap</code></td>
<td>사용자 토큰으로 빠르게 조회하기 위한 맵</td>
</tr>
<tr>
<td><code>globalIndex</code></td>
<td>전체 순번 계산을 위한 전역 카운터</td>
</tr>
<tr>
<td><code>BatchManager</code></td>
<td>ALLOWED 상태 사용자 그룹 및 타이머 관리</td>
</tr>
<tr>
<td><code>batchTokenMap</code></td>
<td>배치 단위 사용자 토큰 목록</td>
</tr>
<tr>
<td><code>batchFutures</code></td>
<td>로그인 완료 여부 추적용 <code>CompletableFuture</code> 객체</td>
</tr>
</tbody></table>
<h3 id="📘-api-요약">📘 API 요약</h3>
<table>
<thead>
<tr>
<th>API</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>POST /join</code></td>
<td>대기열 진입. 토큰 기반 사용자 생성 후 큐 등록</td>
</tr>
<tr>
<td><code>GET /{uuid}</code></td>
<td>사용자 상태 확인 (<code>WAITING</code> or <code>ALLOWED</code>) 및 현재 순번 확인</td>
</tr>
<tr>
<td><code>POST /notify</code></td>
<td>수강신청 서버가 빈자리 발생 시 앞 사용자 n명 ALLOWED 처리 요청</td>
</tr>
<tr>
<td><code>GET /clear</code></td>
<td>테스트용 전체 대기열 초기화</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-jmeter-테스트-결과">4. JMeter 테스트 결과</h2>
<p>우리는 100명부터 10,000명까지 다양한 시나리오를 구성해 총 4가지 상황에서 부하 테스트를 진행했다.
📌 각 시나리오에 대한 상세 설명은 아래 글에 정리되어 있다.
<a href="https://velog.io/@smj_716/zrtrqm2i">🔗 시나리오 설명 보러가기</a></p>
<p>대기열 시스템 도입과 Polling 방식을 적용한 현재 아키텍처는 이전 구조와 비교했을 때 오류율이 눈에 띄게 감소했다.</p>
<p>동시 <strong>5,000</strong>명 접속 시나리오에서의 변화를 예로 들어보겠다.</p>
<p><strong>📉 MVP 초기 구조 vs Polling 도입 후 오류율</strong></p>
<p>시나리오1</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/72692b76-5c5a-42ec-a816-8deb952c1435/image.png" width = "450">

<p>시나리오2</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/8be49112-bffa-4071-8735-49b2e97bfe32/image.png" width = "450">

<p>시나리오3</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/14de1f2e-2493-4b28-ab3a-dd3b0c21e88c/image.png" width = "450">

<p>시나리오4</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/294b89da-fa10-41e9-b9f7-3e3ea9650aff/image.png" width = "450">

<p>위 결과를 보면</p>
<ul>
<li><strong>Polling</strong>은 사용자가 <code>ALLOWED</code>가 될 때까지 계속 요청하기 때문에 표본 수가 많은것을 볼 수 있고 그 결과 정상적으로 많은 요청을 처리할 수 있는것으로 판단된다.</li>
<li>또한 <strong>대기열 도입</strong>으로 부하도 줄었다는것을 확인할 수 있다. </li>
</ul>
<p>😲 <strong>평균적으로 약 80% 이상 오류율이 감소했다.</strong> 
대기열 도입과 polling 적용 후 안정성 올라갔다는 걸 수치로 확인할 수 있었고 그 수치는 엄청난 차이였다.</p>
<hr>
<h3 id="🎯-마무리">🎯 마무리</h3>
<p>우리 팀은 처음부터 대기열 시스템 도입과 polling을 쓰려고 했던게 아니다.
수강신청 서버가 트래픽을 감당하지 못하고 터지는 걸 경험하면서</p>
<ul>
<li>👉 &quot;모두가 한 번에 들어오지 않게 하려면 어떻게 해야 하지?&quot;라는 고민에서 대기열을 도입하게 되었고</li>
<li>👉 &quot;입장 가능 여부를 사용자에게 어떻게 알릴까?&quot;라는 물음 끝에 Polling 방식이 자연스럽게 도출되었다.</li>
</ul>
<p>❌ 물론 완벽한 구조는 아니다.
서버가 반복적으로 요청을 받아야 하기에 리소스가 소모되고 실시간성도 다소 부족하다.
그래서 이 방법말고 회의를 통해 나온 다른 후보의 방법들도 사용해보려고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[수강신청 시스템, 플로우차트/ DB락/테스트]]></title>
            <link>https://velog.io/@smj_716/881zxilp</link>
            <guid>https://velog.io/@smj_716/881zxilp</guid>
            <pubDate>Wed, 02 Jul 2025 06:19:46 GMT</pubDate>
            <description><![CDATA[<h3 id="✍️-설계보다-코드가-먼저">✍️ 설계보다 코드가 먼저?</h3>
<p>이번에 수강신청 시스템을 구현하면서 가장 먼저 한 일은 코드를 짜는 것이 아니라 로직을 <strong>플로우차트</strong>로 그려보는 것이었다.
<img src="https://velog.velcdn.com/images/smj_716/post/83c48b35-74d7-4d1e-b763-85592a784f4a/image.png" width ="400"></p>
<p>이 차트를 먼저 만들고 나니 구현이 훨씬 쉬워졌다. 
✔️ 무엇을 언제 검사해야 하는지, 예외는 어디에서 발생할 수 있는지, 그리고 트랜잭션의 시작과 끝을 어디에 둘 것인지가 명확해졌다. 
✔️ 특히 락 타임아웃, DB 좌석 확인, 트랜잭션 경계처럼 헷갈릴 수 있는 부분도 미리 체크할 수 있어서 실수 없이 구현할 수 있었다.</p>
<hr>
<h3 id="🔐-db-비관적-락">🔐 DB 비관적 락</h3>
<h4 id="📌-수강신청의-본질-경쟁과-충돌">📌 수강신청의 본질: 경쟁과 충돌</h4>
<p>수강신청은 단순히 &quot;누가 먼저 클릭했느냐&quot;의 싸움이 아니다. 수십 명, 수백 명이 같은 강의를 동시에 신청하기 때문에 시스템 내부에서는 굉장한 충돌이 발생한다.</p>
<p>이런 상황에서 가장 중요한 건 바로 <strong>정합성(일관성)</strong>이다. 강의 정원이 40명인데 동시에 41명이 들어오면 누군가는 잘못 등록되는 것이다. 
이걸 막기 위해 DB 수준에서의 동시성 제어가 필요했다.</p>
<p><strong>그래서 선택한 방법이 바로 DB 비관적 락 (Pessimistic Lock)이다.</strong></p>
<p><strong>💡 왜 락이 필요한가?</strong>
예를 들어, A와 B가 동시에 같은 강의를 신청한다고 하자. 두 요청이 거의 같은 시점에 DB에서 &quot;현재 정원이 39명&quot;인 걸 확인하고, 동시에 1명을 추가해 41명이 되면 어떻게 될까? 시스템은 데이터 무결성 오류를 일으키게 된다.</p>
<p>이 문제를 방지하려면 <strong>동시에 같은 데이터를 읽는 순간부터 다른 트랜잭션이 해당 데이터를 수정하지 못하도록 막아야 한다.</strong></p>
<p><strong>📢 낙관적인 락(Optimistic Lock)을 쓰지 않은 이유</strong></p>
<ul>
<li>낙관적 락은 <strong>데이터를 읽을 땐 그냥 읽고, 쓰기 시점에 버전 정보를 비교해서 다른 트랜잭션이 변경했는지 확인</strong>한다. 충돌이 감지되면 예외를 던지거나, 재시도 로직이 필요하다.</li>
<li>수강신청에서 낙관적 락을 사용하면 충돌이 자주 발생하고, 그때마다 예외 처리나 재시도 로직을 추가로 구현해야 한다. </li>
<li><strong>즉, 락은 가볍지만 로직은 더 복잡해진다.</strong></li>
</ul>
<p>반면, <strong>비관적 락(Pessimistic Lock)</strong>은 <strong>충돌 자체를 원천 차단하는 방식</strong>이다. 한 사용자가 특정 강의 데이터를 조회하고 수정하려 할 때, 다른 사용자가 해당 데이터를 동시에 수정하지 못하도록 DB가 직접 막아준다.</p>
<p>수강신청처럼 정합성이 매우 중요한 작업에서는 충돌을 감지하고 복구하는 것보다 애초에 충돌이 발생하지 않도록 막는게 훨씬 안전하고 단순하다. 그래서 이번 구현에서는 비관적 락을 선택하게 됐다.</p>
<p>** 🔽 비관적 락 적용**
JPA에서 제공하는 <code>@Lock</code> 애너테이션을 통해 <code>PESSIMISTIC_WRITE</code> 락을 걸었다. 아래는 실제 사용한 코드다.</p>
<p>🔺CourseReader.java</p>
<pre><code class="language-java">public Optional&lt;Course&gt; readWithPessimisticLock(Long courseId) {
    return courseRepository.findWithPessimisticLock(courseId);
}</code></pre>
<p>🔺CourseRepository.java</p>
<pre><code class="language-java">@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query(&quot;SELECT c FROM Course c WHERE c.id = :courseId&quot;)
Optional&lt;Course&gt; findWithPessimisticLock(@Param(&quot;courseId&quot;) Long courseId);</code></pre>
<p>이 코드는 해당 강의에 대해 트랜잭션이 종료될 때까지 다른 트랜잭션이 이 강의 데이터를 수정하거나 읽지 못하게 막는 역할을 한다.</p>
<p><code>PESSIMISTIC_WRITE</code> 락은 쿼리 실행 시점에 DB 레벨에서 <strong>exclusive lock</strong>을 걸고 트랜잭션이 끝나기 전까지 다른 요청은 대기 상태로 들어간다.
만약 락을 기다리는 시간(timeout)이 초과되면 예외가 발생한다.</p>
<hr>
<h3 id="🧨-테스트-코드">🧨 테스트 코드</h3>
<p>이 락이 실제로 동시성 문제를 잘 막아주는지 확인하기 위해 100명이 동시에 40명 정원의 강의를 신청하는 시나리오를 테스트 코드로 작성했다.</p>
<pre><code class="language-java">assertEquals(40, successCount.get(), &quot;정원이 40명이므로 40명만 성공해야 함&quot;);
assertEquals(60, failCount.get(), &quot;100명 중 60명이 실패해야함&quot;);
assertEquals(40, course.getParticipant(), &quot;Course의 participant 필드도 40이어야 함&quot;);</code></pre>
<p>이 테스트를 통해 동시에 요청을 보내도 정확히 40명까지만 수강에 성공하고 나머지는 모두 실패하는 걸 확인할 수 있었다.</p>
<p>또한 <code>System.out.println(&quot;실패한 studentId: ...&quot;)</code> 로그를 통해 실제로 어떤 사용자들이 실패했는지도 체크할 수 있어 디버깅에 도움이 됐다.</p>
<hr>
<h3 id="➡️-추후에는-redis-락으로">➡️ 추후에는 Redis 락으로?</h3>
<p>현재는 DB 락만으로도 코드를 구현했지만, 우리 서비스는 만명까지 테스트하는 것을 목표로 두고 있기 때문에 수강 인원이 수천 명을 넘기 시작하면 또 다른 문제가 생길것이다.</p>
<p>DB는 락을 처리하면서 성능 저하가 올 수 있고 락 대기 시간이 길어지면 타임아웃이 잦아질 수 있다.</p>
<p>그래서 장기적으로는 <strong>Redis 기반의 분산 락</strong>을 도입하는 것을 고려하고 있다.</p>
<p>이 방법을 쓰면 <strong>DB가 아닌 메모리 기반에서 빠르게 락을 제어</strong>할 수 있고 분산 서버 환경에서도 동일한 락을 공유할 수 있다.</p>
<p>물론 도입 자체는 단순하지 않지만 장기적으로 봤을 때 확장성 면에서는 더 유리할것이다.</p>
<hr>
<h3 id="💡-느낀-점">💡 느낀 점</h3>
<ul>
<li>설계 없이는 절대 안정적인 시스템을 만들 수 없다.</li>
<li>무작정 코드를 짜기보다는 플로우차트를 먼저 그려보자.</li>
<li>DB 락은 강력한 도구다. 그러나 상황에 따라 다르게 선택해야 한다.</li>
<li>실전에서는 테스트 코드만으로는 부족할 수 있다. 시뮬레이션을 통한 예외 확인도 병행해야 한다. -&gt; JMeter 도입</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVP 대규모 동시접속 부하테스트 - JMeter]]></title>
            <link>https://velog.io/@smj_716/zrtrqm2i</link>
            <guid>https://velog.io/@smj_716/zrtrqm2i</guid>
            <pubDate>Sun, 29 Jun 2025 12:13:36 GMT</pubDate>
            <description><![CDATA[<h2 id="1-테스트-개요">1. 테스트 개요</h2>
<p>수천 명 이상이 동시에 몰리는 수강신청 상황에서 시스템이 다운되지 않고 얼마나 잘 버티는지, 특히 제한된 자원(t3.large) 환경에서 얼마만큼의 트래픽을 처리할 수 있는지를 보기 위해 이번 테스트를 진행햇다.</p>
<p><strong>👉이 시스템은 다음과 같은 특성이 있다:</strong></p>
<ul>
<li>짧은 시간 내 <strong>폭발적인 접속</strong></li>
<li>실시간 좌석 정보와 <strong>동기성</strong> 중요</li>
<li>백엔드 성능이 느려지면 사용자 불신이 직접적으로 이어짐</li>
</ul>
<p><strong>👉 테스트 아키텍처</strong></p>
<ul>
<li>백엔드: Spring Boot</li>
<li>웹 서버: Nginx</li>
<li>DB: Amazon RDS (MySQL)</li>
<li>캐시: Redis (후속 도입 예정)</li>
<li>메시지 브로커: Kafka (후속 도입 예정)</li>
<li>서버 인프라: EC2 t3.large (2vCPU, 8GiB RAM)</li>
<li><strong>테스트 도구</strong>: <strong>Apache JMeter</strong> + Puppeteer (브라우저 기반 시뮬레이션)</li>
</ul>
<p>이번 글은 MVP 구조에서의 테스트만 다루며 이후 보완된 단계는 별도 포스트로 기록할 예정이다.</p>
<p>📢 테스트의 성능 측정 결과는 JMeter에서 수집된 데이터를 기반으로 <strong>HTML 리포트로 시각화</strong>해 확인했다.
각 요청별 응답시간 분포, 실패율, TPS, 네트워크 전송량 등 지표를 정량적으로 비교할 수 있었다.
아래는 실제 리포트 시각화 예시 캡처 이미지이다.</p>
<img src ="https://velog.velcdn.com/images/smj_716/post/486786e4-a3cc-4fa8-b711-9c902058ffa7/image.png" width = "500">
<img src ="https://velog.velcdn.com/images/smj_716/post/b6b2d532-c42f-4cb3-9bd0-854dcf4e12a6/image.png" width = "500">

<hr>
<h2 id="2-시나리오-구성">2. 시나리오 구성</h2>
<p>테스트는 총 6가지 인원 케이스(100 / 1,000 / 3,000 / 5,000 / 7,000 / 10,000명)로 나누어 수행했고, 아래 5가지 핵심 시나리오를 중심으로 구성했다:
<img src ="https://velog.velcdn.com/images/smj_716/post/297ee466-9583-4a67-98ac-fa134d07aa1f/image.png" width = "360"></p>
<ul>
<li><strong>ENROLL_1</strong>: 로그인 유지 부하</li>
<li><strong>ENROLL_2</strong>: 특정 교양 강의 신청 집중</li>
<li><strong>ENROLL_3</strong>: 서로 다른 전공 그룹이 각각 하나의 전공 강의 신청</li>
<li><strong>ENROLL_4</strong>: 3조건에서 신청 -&gt; 취소 반복</li>
<li><strong>ENROLL_5</strong>: 다수 전공 그룹이 다수 강의에 대한 분산 신청</li>
</ul>
<hr>
<h2 id="3-실험별-상세-분석">3. 실험별 상세 분석</h2>
<img src ="https://velog.velcdn.com/images/smj_716/post/648ff1ad-a192-4347-80c2-0f17f8a9f1e4/image.png" width = "360">

<img src ="https://velog.velcdn.com/images/smj_716/post/0c5e08b4-0073-4c2c-b064-11da8853792a/image.png" width = "360">

<p>100명~1,000명까지는 비교적 안정적이었지만 3,000명 이상부터 502 Bad Gateway 오류가 급증했고, <strong>10,000명 접속 시 실패율은 83.54%</strong>에 달했으며 대부분 Timeout 또는 서버 내부 오류였다.</p>
<p>502는 Spring Boot 백엔드가 응답을 제때 주지 못해 Nginx가 대신 에러를 반환한 것으로 500 오류는 내부 Thread Pool 포화 혹은 HikariCP 커넥션 부족이 원인으로 보였다.</p>
<h3 id="➡️-nginx-worker_connections-변화"><strong>➡️ Nginx worker_connections 변화</strong></h3>
<p><strong>Nginx에서 worker_connections란?</strong>
클라이언트의 요청을 처리하는 동시 연결 수를 조절하는 값이다.
이 숫자가 작으면 많은 유저가 몰릴 경우 요청이 차단되거나 지연된다.
<img src ="https://velog.velcdn.com/images/smj_716/post/1a0e51ee-22df-40ed-a420-890b25e457fd/image.png" width = "500"></p>
<ul>
<li>worker_connections 값을 768 → 4096으로 증가시키며 테스트</li>
<li><strong>결과</strong>: 502 오류율은 점진적으로 감소했지만 전체 실패율은 91~93%로 유사함</li>
<li><strong>이유</strong>: Nginx 입구는 넓어졌지만 백엔드(Spring+DB) 병목은 여전했기 때문</li>
</ul>
<p>📌 정리하면, <strong>Nginx 튜닝은 효과가 있지만 입구만 뚫린 상태로 내부 문제는 여전했다.</strong></p>
<h3 id="➡️-spring-thread-pool-실험"><strong>➡️ Spring Thread Pool 실험</strong></h3>
<p><strong>Thread Pool이란?</strong>
애플리케이션에서 동시에 여러 요청을 처리할 수 있도록 해주는 작업 스레드 모음이다. 너무 작으면 처리 병목이 생기고, 너무 크면 시스템 자원과 충돌한다.
<img src ="https://velog.velcdn.com/images/smj_716/post/a75fe698-4a06-47ba-a290-3497e0df7203/image.png" width = "500"></p>
<ul>
<li>Thread Pool 크기를 200 → 300 → 400으로 늘려가며 평균 응답 시간, TPS(초당 처리 수), CPU 점유율, 실패율을 측정</li>
<li><strong>결과</strong>: Thread 수 증가에도 불구하고 평균 응답시간은 늘고 TPS는 감소함</li>
<li><strong>이유</strong>: Spring Boot 애플리케이션 내부 Thread가 동시 처리할 수 있는 요청을 늘어났지만, DB 커넥션 풀이 부족해 Thread는 대기만 하게 됨</li>
</ul>
<p>📌 결론: T<strong>hread만 늘리면 오히려 병목 심화됨 → 반드시 DB Connection Pool과 함께 조정</strong>해야 함</p>
<h3 id="➡️-db-커넥션-풀-hikaricp-실험"><strong>➡️ DB 커넥션 풀 (HikariCP) 실험</strong></h3>
<p><strong>DB 커넥션 풀(HikariCP)이란?</strong>
DB와 연결할 수 있는 커넥션들을 미리 만들어 풀(Pool) 형태로 관리하는 구조다. 필요할 때 커넥션을 즉시 꺼내 쓸 수 있어 빠르고 효율적이다.</p>
<ul>
<li><strong>active 커넥션</strong>: 현재 DB와 연결되어 사용 중인 커넥션 수</li>
<li><strong>idle 커넥션</strong>: 아직 사용되지 않고 대기 중인 커넥션 수</li>
<li><strong>waiting</strong>: 커넥션을 받기 위해 대기 중인 요청 수<img src ="https://velog.velcdn.com/images/smj_716/post/b49a26db-e12f-4a22-972d-95d1e61b1c95/image.png" width = "400"></li>
<li><strong>Pool size = 10일 때</strong><ul>
<li>Active 10 / Idle 0 / Waiting 190 (타임아웃 다수 발생)</li>
</ul>
</li>
<li><strong>Pool size = 100일 때</strong> <ul>
<li>Active 99 / Idle 1 / Waiting 6 (안정적 처리 가능)</li>
</ul>
</li>
</ul>
<p>커넥션 풀이 부족하면 Thread는 살아 있어도 DB 연결을 못 받아 대기 상태로 빠지고
이로 인해 TPS 하락, 응답 시간 증가, 502/500 오류 증가로 이어졌다.</p>
<p>📌 결론: <strong>적절한 커넥션 풀 확보만으로도 전체 시스템 안정성이 비약적으로 향상됨</strong></p>
<hr>
<h2 id="4-요약">4. 요약</h2>
<ul>
<li>Thread만 늘리는 방식은 오히려 지연을 초래했고 DB와 함께 튜닝해야 했다</li>
<li>Nginx 튜닝은 입구 처리량은 높였지만 백엔드 병목 해결에는 역부족이었다</li>
</ul>
<p>🌟 결론적으로 현재 구조(MVP)에서는 3,000명 이상의 동시 접속에서 시스템 안정성 확보가 어렵다는 것을 실험으로 확인했고 비동기 구조 도입 없이는 이 한계를 넘기 어렵다고 판단했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[반복로직/JPQL -> QueryDSL으로 리팩토링]]></title>
            <link>https://velog.io/@smj_716/1ndi35l9</link>
            <guid>https://velog.io/@smj_716/1ndi35l9</guid>
            <pubDate>Wed, 11 Jun 2025 04:46:01 GMT</pubDate>
            <description><![CDATA[<h2 id="1-jpql의-한계">1. JPQL의 한계</h2>
<p>Spring JPA를 처음 배우면 대부분 <code>@Query</code>를 쓰거나 <code>findByXXX()</code> 같은 메서드 네이밍 전략을 사용해서 데이터를 조회한다. 
이때 사용하는 쿼리 언어가 <strong>JPQL</strong>(Java Persistence Query Language)이다.</p>
<blockquote>
<p>JPQL은 SQL처럼 생겼지만, 테이블이 아닌 엔티티를 대상으로 한 객체 지향 쿼리이다.</p>
</blockquote>
<pre><code class="language-java">@Query(&quot;SELECT c FROM Course c WHERE c.department = :dept AND c.grade = :grade&quot;)
List&lt;Course&gt; findByDepartmentAndGrade(String dept, int grade);</code></pre>
<h4 id="⚠️-그런데-문제는">⚠️ 그런데 문제는</h4>
<ul>
<li><strong>문자열 기반</strong> : 쿼리를 <code>&quot; &quot;</code> 안에 문자열로 작성해야 해서 오타가 나도 컴파일 타임에 알 수 없음</li>
<li><strong>런타임 오류</strong> : 잘못된 필드명을 써도 에러는 실행 시점에 발생함</li>
<li><strong>조건문이 많을 때</strong> : if문을 여러번 사용해야하고 코드가 복잡해짐
특히, 동적 필터 기능이 많아질수록 JPQL은 구조적으로 한계에 부딪히게 된다. </li>
</ul>
<hr>
<h2 id="2-querydsl이란">2. QueryDSL이란?</h2>
<blockquote>
<p>Java 코드로 SQL처럼 쿼리를 작성할 수 있도록 도와주는 프레임워크</p>
</blockquote>
<p>즉! 우리가 직접 문자열로 SQL을 쓰는 것이 아니라,
타입 안정성이 보장된 Java 메서드 체이닝 방식으로 쿼리를 작성하는 도구이다.</p>
<ul>
<li><strong>타입 안정성</strong> : 컴파일 타임에 필드 오타를 체크할 수 있음</li>
<li><strong>IDE 자동완성</strong> : 엔티티 기준으로 쿼리 작성 시 자동완성 지원</li>
<li><strong>동적 쿼리</strong> : 조건을 메서드로 조립하여 동적으로 where절 생성 가능</li>
</ul>
<p><strong>❓그럼 QueryDSL은 어떻게 동작할까❓</strong>
QueryDSL은 내부적으로 <strong>Q타입 클래스</strong>를 자동으로 생성한다.
예를 들어 <code>Course</code>라는 엔티티가 있으면 <code>QCourse</code>라는 클래스를 만든다.
이 Q클래스는 빌드할때 Gradle의 annotationProcessor를 통해 생성되고 이 객체를 통해 타입 안전성 있는 필드를 사용할 수 있는 것이다.</p>
<pre><code class="language-java">public class QCourse extends EntityPathBase&lt;Course&gt; {
    public final StringPath name = createString(&quot;name&quot;);
    public final NumberPath&lt;Integer&gt; grade = createNumber(&quot;grade&quot;, Integer.class);
    ...
}</code></pre>
<p>위는 Q타입 클래스 예시이다.
<code>QCourse course = QCourse.course</code>라고 선언하면 이 클래스 안에 있는
필드들을 <code>course.name</code>, <code>course.grade</code>처럼 타입 안전하게 사용할 수 있다.</p>
<hr>
<h2 id="3-기존-프로젝트-코드-방식">3. 기존 프로젝트 코드 방식</h2>
<h3 id="➡️-모든-강의-조회">➡️ 모든 강의 조회</h3>
<p><strong>&lt; JPQL 없이 직접 조회 + 반복 로직&gt;</strong>
처음에는 아래와 같이 모든 강의를 조회하고, 각각 시간 정보를 별도로 불러오는 구조였다. </p>
<pre><code class="language-java">public List&lt;CourseListRes&gt; getCourseList() {
    List&lt;Course&gt; courses = courseReader.findAllCourses(); // 전체 강의 조회

    return courses.stream()
        .map(course -&gt; {
            List&lt;CourseTime&gt; times = courseReader.findCourseTimesByCourseId(course.getId()); // 강의별 시간 조회
            String time1 = CourseTimeFormatter.formatTime(times, 0);
            String time2 = CourseTimeFormatter.formatTime(times, 1);
            return CourseListRes.of(course, time1, time2);
        })
        .toList();
}</code></pre>
<p><strong>⚠️ 문제점</strong></p>
<ul>
<li>강의가 100개면 -&gt; 시간조회 쿼리 100번 발생 -&gt; <strong>N+1 문제 발생</strong></li>
<li>필터링 기능(전공, 학년, 과목코드 등)이 들어가면 <strong>조건문(if문) 난무</strong> <strong>-&gt; 유지보수 어려움</strong></li>
</ul>
<h3 id="➡️-강의를-신청한-인원-조회">➡️ 강의를 신청한 인원 조회</h3>
<p><strong>&lt; JPQL 사용 &gt;</strong></p>
<pre><code class="language-java">@Query(&quot;SELECT new com.allclearwas.domains.enrollment.dto.CourseEnrollmentCountDto(e.course.id, COUNT(e)) &quot;
        + &quot;FROM Enrollment e WHERE e.course.id IN :courseIds GROUP BY e.course.id&quot;)
    List&lt;CourseEnrollmentCountDto&gt; countByCourseIds(@Param(&quot;courseIds&quot;) List&lt;Long&gt; courseIds);</code></pre>
<p><strong>⚠️ 문제점</strong></p>
<ul>
<li>JPQL은 문자열 기반이라 오타나 필드명이 바뀌어도 <strong>컴파일 타임에 잡히지 않음</strong></li>
<li>DTO 생성자 기반이라 필드 순서 변경 시 오류 가능성 있음</li>
<li>추후 <strong>동적 조건</strong>이 생기면 쿼리 문자열을 다시 조립해야 하므로 유연하지 않음</li>
</ul>
<hr>
<h2 id="4-querydsl로-리팩토링한-코드">4. QueryDSL로 리팩토링한 코드</h2>
<h3 id="📌-설정">📌 설정</h3>
<pre><code class="language-java">@Configuration
public class QuerydslConfig {
    @PersistenceContext
    private EntityManager em;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(em);
    }
}</code></pre>
<p>이 설정은 QueryDSL에서 쿼리를 생성할 때 사용하는 핵심 도구인 <strong>JPAQueryFactory</strong>를 빈으로 등록한다.</p>
<ul>
<li><strong>EntityManager</strong>는 JPA의 모든 쿼리 동작을 관리하는 핵심 객체</li>
<li><strong>JPAQueryFactory</strong>는 EntityManager를 기반으로 QueryDSL 쿼리를 만들어주는 역할</li>
</ul>
<p>이 설정을 통해서 QueryDSL을 구현하는 클래스에서 <code>queryFactory</code>를 주입받아 쿼리를 작성할 수 있게된다. </p>
<h3 id="➡️-모든-강의-조회-1">➡️ 모든 강의 조회</h3>
<pre><code class="language-java">queryFactory
    .select(...)
    .from(course)
    .join(course.courseInfo, courseInfo)
    .where(
        equalsIfNotNull(courseInfo.category, request.category()),
        equalsIfNotNull(courseInfo.grade, request.grade()),
        equalsIfNotNull(courseInfo.department, request.department()),
        equalsIfNotBlank(course.courseCode, request.code())
    )
    .fetch();</code></pre>
<ul>
<li><code>select()</code>에는 <code>CourseListDao</code> 생성자에 필요한 정보만 골라 조회</li>
<li><code>selectTimeString()</code>으로 서브쿼리를 이용해 시간 정보를 2줄만에 깔끔히 처리</li>
<li>null 조건은 자동으로 제거되므로 if문 필요 없음</li>
</ul>
<pre><code class="language-java">private Expression&lt;String&gt; selectTimeString(QCourseTime time, QCourse course, int offset) {
    return JPAExpressions.select(
            Expressions.stringTemplate(
                &quot;concat({0}, &#39; &#39;, {1}, &#39;~&#39;, {2})&quot;,
                time.dayOfWeek.stringValue(),
                time.startTime.stringValue(),
                time.endTime.stringValue()
            )
        )
        .from(time)
        .where(time.course.eq(course))
        .orderBy(time.id.asc())
        .offset(offset)
        .limit(1);
}</code></pre>
<ul>
<li>강의 하나당 time1, time2만 뽑아서 시간 문자열을 생성하고,
메인 강의 조회 쿼리 안에 서브쿼리 형식으로 조립해서 한 번에 처리한다.</li>
<li>즉! 강의가 100개든 1000개든 추가적인 쿼리 실행은 발생하지 않고,</li>
<li><em>메인 쿼리 1개 + 시간 정보 서브쿼리 2개(각 time1, time2)*</em>만 QueryDSL 내부적으로 작성되어 쿼리는 실제로 한 번에 실행된다.</li>
</ul>
<p><strong>➕ JPAExpressions?</strong>
QueryDSL에서 서브쿼리(하위 쿼리)를 작성할 수 있도록 도와주는 클래스
🔺 기존 SQL에서 다음과 같은 서브쿼리를 작성한다고 하면:
<code>SELECT (SELECT name FROM student WHERE id = 1) AS studentName</code></p>
<p>🔺 아래와 같이 똑같이 표현할 수 있다:</p>
<pre><code class="language-java">JPAExpressions.select(student.name)
    .from(student)
    .where(student.id.eq(1))</code></pre>
<p><strong>❓ 그럼 애초에 JPQL로 조인하면 안되는가 ❓</strong>
조인 자체는 JPQL로도 가능하지만 JPQL로도 조인해서 해결할 수 있는 건 <strong>&#39;1:1&#39;</strong> 또는 <strong>&#39;다대일 관계&#39;</strong>의 단순 조회일 뿐이다.
우리 프로젝트와 같이 <code>Course</code>와 <code>CourseTime</code>이 1:N 관계이고, 시간 2개만 선택하고 싶은 경우에는 여러 문제가 발생한다.</p>
<h3 id="➡️-강의를-신청한-인원-조회-1">➡️ 강의를 신청한 인원 조회</h3>
<pre><code class="language-java">queryFactory
    .select(Projections.constructor(CourseEnrollmentCountDao.class,
        enrollment.course.id,
        enrollment.count()))
    .from(enrollment)
    .join(enrollment.course, course)
    .where(course.id.in(courseIds))
    .groupBy(course.id)
    .fetch();</code></pre>
<ul>
<li>특정 강의 목록(courseIds)에 대해 몇 명이 신청했는지 집계하는 쿼리</li>
<li><code>count()</code>와 <code>groupBy()</code>를 사용하여 쿼리 한 번에 수강인원 수를 조회</li>
<li>DTO를 바로 생성자 방식으로 반환하여 불필요한 변환 로직 생략</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 연관관계, 양방향을 사용하면 위험할까?]]></title>
            <link>https://velog.io/@smj_716/gdhx7npd</link>
            <guid>https://velog.io/@smj_716/gdhx7npd</guid>
            <pubDate>Thu, 29 May 2025 08:23:27 GMT</pubDate>
            <description><![CDATA[<p>이번 글에서는 최근 프로젝트를 하면서 실제로 겪은 연관관계 이슈와
공부하면서 정리하게 된 JPA 연관관계의 기초 개념과 주의할 점들을 간단하게 기록한다. </p>
<h3 id="📢-연관관계란">📢 연관관계란?</h3>
<p>DB 테이블은 서로 연결되어 있고, 이 연결을 <strong>관계(연관관계)</strong>라고 부른다.
예를 들어, 강의(course)와 학생(student)이 있을 때 &quot;한 명의 학생이 여러 강의를 수강할 수 있다&quot;면 1:N 관계(일대다 관계)인 것이다.
이런 관계를 <strong>코드 안에서도</strong> 표현해줘야 하는데 <strong>JPA</strong>에서는 <code>@OneToMany</code>, <code>@ManyToOne</code>, <code>@OneToOne</code>, <code>@ManyToMany</code> 같은 어노테이션을 사용하여 관계를 표현한다. </p>
<p><strong>🌟주의</strong>
 연관관계를 이해할 때 가장 헷갈리는 부분은 <strong>코드에서는 객체</strong>끼리 연결되고, <strong>DB에서는 테이블</strong>끼리 외래키(FK)로 연결된다는 점이다.
JPA는 이 둘을 자동으로 매핑해주지만 항상 <strong>“객체는 참조”</strong>, <strong>“DB는 외래키”</strong>라는 관점을 구분해서 바라보아야한다.</p>
<hr>
<h3 id="📢-단방향-vs-양방향-연관관계">📢 단방향 vs 양방향 연관관계</h3>
<h4 id="👉-단방향-연관관계">👉 단방향 연관관계</h4>
<p>한 쪽에서만 다른 객체를 참조하는 방식
ex) <code>CourseTime</code>은 <code>Course</code>만 알고 있고, <code>Course</code>는 <code>CourseTime</code>을 모름</p>
<pre><code class="language-java">//CourseTime
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = &quot;course_id&quot;)
private Course course;</code></pre>
<h4 id="👉-양방향-연관관계">👉 양방향 연관관계</h4>
<p>서로 참조하는 구조
ex) <code>CourseTime</code>도 <code>Course</code>를 알고, <code>Course</code>도 <code>CourseTime</code> 목록을 가지고 있음</p>
<pre><code class="language-java">//Course
@OneToMany(mappedBy = &quot;course&quot;)
private List&lt;CourseTime&gt; courseTimes = new ArrayList&lt;&gt;();</code></pre>
<p><code>Course</code>에서도 <code>CourseTime</code>을 탐색할 수 있어서 단방향보다 더 많은 정보를 가져올 수 있지만, 주의해야할 것들이 있음 (아래에 🔽)</p>
<h4 id="⚠️-양방향-연관관계는-주인-꼭-지정">⚠️ 양방향 연관관계는 주인 꼭 지정!!</h4>
<p>양방향 연관관계를 만들면 JPA가 헷갈릴 수 있기 때문에, 둘 중 한 쪽을 &#39;주인&#39;으로 정해야 한다.
연관관계의 주인은 실제로 <strong>외래키를 갖고 있는 객체</strong>이다.
<strong><code>mappedBy</code>는 주인이 아닌 쪽에서 사용하는 속성</strong>으로, &quot;나는 반대편 필드를 따라갈게&quot;라는 뜻이다. 
그래서 <code>mappedBy = &quot;course&quot;</code>는 <code>CourseTime</code> 엔티티 안의 <code>course</code> 필드를 기준으로 관계를 맺는다는 뜻이다.
이걸 정하지 않으면 JPA는 관계를 두 번 맺으려고 하고 쓸데없는 쿼리가 날아가게된다. </p>
<hr>
<h3 id="➕식별관계-vs-비식별관계">➕식별관계 vs 비식별관계</h3>
<p>추가로 DB 설계 쪽 얘기를 잠깐해보겠다.
우리가 JPA로 연관관계를 맺으면, 결국 DB에도 테이블 간 관계가 생기는데 이걸 식별관계와 비식별관계로 나눌 수 있다.</p>
<h4 id="📌-식별관계">📌 식별관계</h4>
<p>자식 테이블이 부모 테이블의 기본키(PK)를 포함해서 자기 PK를 만드는 경우
즉, 부모 없이는 자식이 존재할 수 없다. 잘 쓰면 정합성이 좋아지지만 JPA에서는 복잡해져서 실무에서는 많이 쓰지 않는다고 한다.</p>
<h4 id="📌-비식별관계">📌 비식별관계</h4>
<p>자식 테이블이 자기만의 PK를 갖고, <strong>부모의 PK는 외래키로만 사용</strong>
우리 프로젝트의 <code>CourseTime</code> → <code>Course</code> 관계도 여기에 해당한다.</p>
<pre><code class="language-java">📘 Course
 └─ 🔑 course_id (PK)

📗 CourseTime
 ├─ 🔑 course_time_id (PK)
 └─ 🔗 course_id (FK, 비식별관계)</code></pre>
<p>이렇게 설계하면 구조가 단순하고, 나중에 확장하거나 수정하기도 쉽다.
그래서 <strong>JPA에서는 대부분 비식별관계로 설계하는게 기본</strong>이라고 한다.</p>
<hr>
<h3 id="❗-양방향-연관관계-실제로-겪은-문제">❗ 양방향 연관관계, 실제로 겪은 문제</h3>
<ul>
<li>처음에는 <code>Course</code>와 <code>CourseTime</code> 사이를 양방향으로 설정했다.
수강신청한 강의의 시간을 가져오려면 <code>Enrollment</code> -&gt; <code>Course</code> -&gt; <code>CourseTime</code> 순으로 접근해야한다고 생각했기 때문이다. </li>
<li>이 구조는 보기에는 편하지만 문제가 생기기 시작한 건 조회 쿼리에서였다.<code>CourseTime</code>을 단순히 조회하려고 했을 뿐인데 예상치 않게 Course까지 함께 불러오는 쿼리가 나갔다. 
(즉시 로딩처럼 작동) 이로 인해 <strong>불필요한 join과 데이터 로딩</strong>이 발생했다.</li>
<li>게다가 <code>Course</code>에서 <code>course.getCourseTimes()</code>를 호출하는 경우,
강의마다 수업 시간이 여러 개 있을 때 반복적으로 지연 로딩이 일어나면서 <strong>N+1 문제</strong>로 확산될 수 있다는 점도 고려해야 했다.</li>
<li>결국 <code>Course</code> 쪽에 있던 <code>List&lt;CourseTime&gt;</code>을 제거하고, 필요한 경우에만 <strong>JPQL의 fetch join</strong>을 통해 명시적으로 조회하도록 구조를 변경했다.</li>
</ul>
<p><strong>💡 단방향으로도 충분할 때가 많다</strong>
JPA는 단방향만으로도 기능을 구현할 수 있는 경우가 많다.
화면에서 역방향으로 탐색할 일이 없다면 굳이 양방향으로 만들어 성능이 떨어지는 상항을 만들지 않아야겠다는 생각이 들었다.</p>
<hr>
<h3 id="🧭-lazy-vs-eager-fetch-join">🧭 LAZY vs EAGER/ Fetch Join</h3>
<p>연관된 객체를 언제 불러올지도 중요한 설정이다.</p>
<h4 id="👉-lazy-지연-로딩">👉 LAZY (지연 로딩)</h4>
<ul>
<li>객체를 처음 조회할 땐 연관 객체를 가져오지 않는다.</li>
<li>진짜 필요할 때(DB를 터치할 때) 불러온다. </li>
<li>기본값으로 많이 사용된다.<h4 id="👉-eager-즉시-로딩">👉 EAGER (즉시 로딩)</h4>
</li>
<li>처음 객체를 가져올 때 연관 객체도 바로 같이 불러온다.</li>
<li>객체가 여러 개 연결돼 있다면 성능이 급격히 떨어질 수 있다. 
ex) CourseTime이 여러개일 경우<h4 id="👉-fetch-join">👉 Fetch Join</h4>
</li>
<li>LAZY로 설정해도 JPQL에서 직접 <code>join fetch</code>를 써서 한 번에 가져올 수 있다.</li>
<li>원하는 상황에서만 불러올 수 있어서 성능 튜닝에 좋다.</li>
<li><code>SELECT ct FROM CourseTime ct JOIN FETCH ct.course</code></li>
</ul>
<hr>
<p>이번에 수강신청 현황 조회 기능을 리팩토링하면서 <strong>&#39;편하려고 만든 연관관계가 오히려 나중에 발목을 잡을 수 있다&#39;</strong>는 걸 확실히 느꼈다. 앞으로는 연관관계를 만들 때 무조건 편한 쪽보다 정확하고 단순한 쪽을 먼저 고려하는 습관을 들여야겠다.</p>
<p><a href="https://github.com/QUEUE-SW/AllClear-was/pull/12/commits/e40320e49ffb8aac14b2488533efd8f4a872ba22">Github 수강신청현황조회 PR</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 MVC 1편] 2]]></title>
            <link>https://velog.io/@smj_716/%EC%8A%A4%ED%94%84%EB%A7%81-MVC-1%ED%8E%B8-2</link>
            <guid>https://velog.io/@smj_716/%EC%8A%A4%ED%94%84%EB%A7%81-MVC-1%ED%8E%B8-2</guid>
            <pubDate>Fri, 23 May 2025 02:41:08 GMT</pubDate>
            <description><![CDATA[<p>서블릿은 서버에서 HTTP 요청 메시지를 파싱하고, 필요한 로직을 처리한 후 응답 메시지를 생성해 클라이언트에게 전달하는 핵심 웹 컴포넌트다. 또한 클라이언트와 서버를 잇는 HTTP 요청/응답의 중심이며 서블릿 컨테이너가 이를 실행해준다.</p>
<h2 id="1-hello-서블릿-등록-및-테스트">1. Hello 서블릿 등록 및 테스트</h2>
<p>서블릿은 톰캣 같은 웹 애플리케이션 서버를 직접 설치하고, 그 위에 서블릿 코드를 클래스 파일로 빌드해서 올린 다음, 톰캣 서버를 실행하면 된다. 
 👉 번거롭다! 
 👉 <strong>스프링 부트</strong>는 <strong>톰캣 서버를 내장</strong>하고 있어서 톰캣 서버 설치 없이 편리하게 서블릿 코드를 실행할 수 있다. </p>
<p> <code>@ServletComponentScan</code> : 스프링 부트가 서블릿을 직접 등록해서 사용할 수 있도록 지원</p>
<pre><code class="language-java">@WebServlet(name = &quot;helloServlet&quot;, urlPatterns = &quot;/hello&quot;)
public class HelloServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String username = request.getParameter(&quot;username&quot;);
        response.setContentType(&quot;text/plain&quot;);
        response.setCharacterEncoding(&quot;UTF-8&quot;);
        response.getWriter().write(&quot;hello &quot; + username);
    }
}</code></pre>
<p>브라우저에서 <a href="http://localhost:8080/hello?username=world%EB%A5%BC">http://localhost:8080/hello?username=world를</a> 호출하면 hello world가 출력된다.</p>
<hr>
<h2 id="2-httpservletrequest---요청-객체">2. HttpServletRequest - 요청 객체</h2>
<p>서블릿은 HTTP 요청메시지를 파싱하여 HttpServletRequest 객체에 담아 개발자에게 제공한다. 즉, 개발자는 이 객체를 사용하여 요청 메시지를 편리하게 조회할 수 있다. </p>
<h4 id="➡️-주요-기능">➡️ 주요 기능</h4>
<ul>
<li>요청 메서드(GET/POST), URI, 프로토콜, 쿼리스트링 등 조회</li>
<li>헤더, 파라미터, 쿠키, Body 등 다양한 정보 조회 가능</li>
<li>임시 저장소 기능 (setAttribute, getAttribute)</li>
<li>세션 관리 기능 (getSession())</li>
</ul>
<h4 id="➡️-http-요청-메시지를-통해-서버로-데이터-전달하는-3가지-방법">➡️ HTTP 요청 메시지를 통해 서버로 데이터 전달하는 3가지 방법</h4>
<p><strong>1. GET</strong> - 쿼리 파타리터
<strong>2. POST</strong> - HTML Form
<strong>3. HTTP message body</strong> </p>
<h3 id="📌-getpost"><strong>📌 GET/POST</strong></h3>
<p>GET은 URL 쿼리 파라미터를 통해, POST는 메시지 바디에 데이터를 전달한다. 그러나 서버 입장에서는 둘 다 key=value 형식으로 같기 때문에 <code>request.getParameter()</code>로 동일하게 조회할 수 있다.</p>
<pre><code class="language-java">//단일 파라미터 조회
String username = request.getParameter(&quot;username&quot;);
//복수 파라미터 조회
String[] values = request.getParameterValues(&quot;username&quot;);</code></pre>
<p>ex) <a href="http://localhost:8080/request-param?username=kim&amp;age=20">http://localhost:8080/request-param?username=kim&amp;age=20</a></p>
<ul>
<li>GET 요청: URL에 파라미터 포함</li>
<li>POST 요청 (HTML Form): application/x-www-form-urlencoded </li>
</ul>
<p>GET은 HTTP 메시지 바디를 사용하지 않아 content-type이 없지만, POST는 HTTP 메시지 바디에 해당 데이터를 포함해서 보내기 때문에 데이터가 어떤 형식인지 content-type을 지정해야한다. 이렇게 폼으로 데이터를 전송하는 형식을 <code>application/x-www-form-urlencoded</code> 라 한다. </p>
<p><small><em>참고로 request.getParameter()는 같은 이름의 파라미터가 여러 개 있을 경우 첫 번째 값만 반환한다. 모두 조회하려면 getParameterValues()를 사용해야 한다.</em></small></p>
<h3 id="📌-http-message-body">📌 <strong>HTTP message body</strong></h3>
<ul>
<li>HTTP API에서 주로 사용, JSON, XML, TEXT</li>
<li>데이터 형식은 주로 JSON</li>
<li>POST, PUT, PATCH </li>
</ul>
<p><strong>➕ 단순 텍스트 (text/plain)</strong> </p>
<pre><code class="language-java">ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);</code></pre>
<p>위와 같이 InputStream을 사용하여 Body에 담긴 순수 문자열을 직접 읽을 수 있다.</p>
<p>➕ <strong>JSON 파싱 (application/json)</strong></p>
<pre><code class="language-java">ObjectMapper objectMapper = new ObjectMapper();
HelloData data = objectMapper.readValue(messageBody, HelloData.class);</code></pre>
<p>JSON은 단순한 텍스트 형태로 전달되지만 이를 객체로 변환하려면 Jackson,Gson 같은 JSON 파싱 라이브러리가 필요하다. 스프링 부트로 Spring MVC를 선택하면 Jackson 라이브러리(<code>ObjectMapper</code>)를 제공한다.</p>
<p>이러한 방식은 모바일 앱이나 API 서버 통신처럼 메시지 바디에 JSON 데이터를 담아 전달할 때 주로 사용된다. 이때 주로 사용하는 HTTP 메서드는 POST, PUT, PATCH 등이다.</p>
<hr>
<h2 id="3-httpservletresponse---응답-객체">3. HttpServletResponse - 응답 객체</h2>
<h4 id="➡️-주요-기능-1">➡️ 주요 기능</h4>
<ul>
<li>HTTP 응답코드 지정</li>
<li>헤더/바디 생성</li>
<li>편의 기능 제공 (Content-Type, 쿠키, Redirect)</li>
</ul>
<p><strong>📌 응답 상태 코드</strong></p>
<pre><code class="language-java">response.setStatus(HttpServletResponse.SC_OK); // 200 OK</code></pre>
<p>서블릿은 요청을 처리한 결과를 상태 코드로 표현한다. 200 OK 외에도 400 Bad Request, 404 Not Found 등의 상태 코드를 설정할 수 있다.</p>
<p><strong>📌 헤더/바디 설정</strong></p>
<pre><code class="language-java">//헤더
response.setContentType(&quot;application/json&quot;);
response.setCharacterEncoding(&quot;utf-8&quot;);

//바디
PrintWriter writer = response.getWriter();
writer.println(&quot;ok&quot;);</code></pre>
<p>응답의 문자 인코딩을 명확히 지정하지 않으면 브라우저가 문자를 잘못 해석해 깨지는 현상이 발생할 수 있다. UTF-8 설정은 다국어 처리에서 특히 중요하다.</p>
<p>👉 위 예시처럼 단순 텍스트가 아닌 HTML or JSON 형태라면?</p>
<ul>
<li><strong>HTML 응답</strong><ul>
<li>contextType을 <code>text/html</code>로 지정하고 바디는 아래와 같이 설정하면 된다.</li>
<li><code>java
writer.println(&quot;&lt;html&gt;&quot;);
writer.println(&quot;&lt;body&gt;&quot;);
writer.println(&quot;  &lt;div&gt;안녕?&lt;/div&gt;&quot;);
writer.println(&quot;&lt;/body&gt;&quot;);
writer.println(&quot;&lt;/html&gt;&quot;);</code></li>
</ul>
</li>
<li><strong>JSON 응답</strong><ul>
<li>contextType을 <code>application/json</code>로 지정하고 바디는 아래와 같이 설정하면 된다.</li>
<li><code>java
HelloData data = new HelloData();
data.setUsername(&quot;kim&quot;);
data.setAge(20);
String result = objectMapper.writeValueAsString(data);
response.getWriter().write(result);</code></li>
<li>Jackson 라이브러리가 제공하는 <code>objectMapper.writeValueAsString()</code>을 사용하면 객체를 JSON 문자로 변경할 수 있다.</li>
</ul>
</li>
</ul>
<p><strong>📌 쿠키 / 리다이렉트</strong></p>
<pre><code class="language-java">Cookie cookie = new Cookie(&quot;myCookie&quot;, &quot;good&quot;);
cookie.setMaxAge(600);  //600초
response.addCookie(cookie);
response.sendRedirect(&quot;/home&quot;);</code></pre>
<p>쿠키는 클라이언트에 데이터를 저장할 때 사용하며 <code>setMaxAge()</code>를 통해 유효 시간을 설정할 수 있다. <code>sendRedirect()</code>는 클라이언트를 지정한 URL로 리다이렉트시키는 메서드로 브라우저가 새로운 요청을 보내도록 유도한다.</p>
<hr>
<p><small>더 자세한 코드는 깃허브 기록을 보자!</small>
<small><a href="https://github.com/MinjiSeo16/inflearnSpring/commits/main/mvc1">https://github.com/MinjiSeo16/inflearnSpring/commits/main/mvc1</a></small></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[한이음 드림업] 수강신청 기본 MVP 설계와 협업 기록]]></title>
            <link>https://velog.io/@smj_716/cx7xd6tv</link>
            <guid>https://velog.io/@smj_716/cx7xd6tv</guid>
            <pubDate>Thu, 22 May 2025 07:11:53 GMT</pubDate>
            <description><![CDATA[<h3 id="➡️-공모전-프로젝트의-목표">➡️ 공모전 프로젝트의 목표</h3>
<p>이번 글에서는 한이음 공모전에서 진행하고있는 수강신청 시스템 개발 시작 과정을 정리하려고 한다.
수강신청 서버는 특히 <strong>대용량 트래픽</strong>에 취약하기 때문에 <strong>&#39;터지지 않는 시스템&#39;</strong>을 만들자는 목표로 공모전 프로젝트를 시작하게되었다. 
<img src = "https://velog.velcdn.com/images/smj_716/post/d1967850-4cb3-42e4-a262-b4391fd36555/image.png" width = 200></p>
<hr>
<h3 id="➡️-기본-mvp">➡️ 기본 MVP</h3>
<p>이 프로젝트는 처음부터 복잡한 기술 스택을 도입하기보다는,
기본 기능을 먼저 구현하고 <strong>부하 테스트</strong>를 통해 문제를 확인하여 <strong>점진적으로 개선</strong>해나가는 방식으로 진행하고자 한다.
우선, 아래는 이번 프로젝트에서 설정한 기본 MVP 기능이다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>기능 요약</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>회원가입 / 로그인</td>
<td>사용자는 회원가입을 통해 계정을 생성하고, 로그인/로그아웃할 수 있음</td>
</tr>
<tr>
<td>2</td>
<td>사용자 정보 조회</td>
<td>사용자는 본인의 이름, 학번, 학점 등 기본 정보를 조회할 수 있음</td>
</tr>
<tr>
<td>3</td>
<td>강의 목록 조회</td>
<td>개설된 강의 목록을 조회하고, 각 강의의 수강 가능 인원을 확인할 수 있음</td>
</tr>
<tr>
<td>4</td>
<td>수강 신청 / 취소</td>
<td>원하는 강의를 신청하고, 이미 신청한 강의를 취소할 수 있음</td>
</tr>
</tbody></table>
<hr>
<h3 id="➡️-협업과-진행방식">➡️ 협업과 진행방식</h3>
<ul>
<li><strong>Slack</strong><ul>
<li>실시간 커뮤니케이션 및 데일리 스탠드업 공유</li>
<li>긴급 이슈나 장애 발생 시 신속한 대응</li>
<li>논의된 사항 빠르게 기록 및 알림</li>
</ul>
</li>
<li><strong>Confluence</strong><ul>
<li>기술 설계와 구조 도식화 문서 작성 
(아키텍처, API 명세, 회고 등)</li>
<li>테스트 결과 및 병목 지점 정리</li>
<li>스프린트 리뷰 및 피드백 기록</li>
</ul>
</li>
<li><strong>Jira</strong><ul>
<li>기능을 작은 단위 업무로 나눠 관리</li>
<li>마일스톤 기반 작업 계획 수립 및 진행률 시각화</li>
<li>To Do → In Progress → Done 상태로 진행 상황 추적</li>
</ul>
</li>
</ul>
<p>아래는 실제 <strong><code>Jira 보드</code></strong>에서 수강신청 기본 기능을 Task 단위로 나누어 정리한 모습입니다.</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/e6f6cd4b-d8cd-4027-b5c3-8eb365120ebb/image.png" width = 500>

<h4 id="🌟-목표-단위-중심의-애자일-전략">🌟 목표 단위 중심의 애자일 전략</h4>
<p>프로젝트를 진행할 때 ‘전체 기능을 한 번에 구현’하는 방식보다,
<strong>작은 단위로 쪼개어</strong> <strong>빠르게 실현</strong>하고 <strong>반복적으로 개선</strong>하는 방식을 선택했습니다.
프론트와 백엔드가 각자 담당할 Task를 나눈 뒤, 매 스프린트마다 우선순위를 정해 협업하고 테스트하며 점진적으로 기능을 완성합니다.</p>
<ul>
<li><strong>Epic</strong>(에픽)<ul>
<li>전체 목표 단위</li>
<li>위 MVP 기반으로 작성</li>
</ul>
</li>
<li><strong>User Story</strong>(사용자 스토리)<ul>
<li>사용자 중심 기능 정의</li>
<li>실제 사용자의 행동 기반으로 표현 (사용자는 ~할 수 있다)</li>
</ul>
</li>
<li><strong>Ticket</strong>(티켓)<ul>
<li>실제 개발 단위</li>
<li>GitHub 브랜치 네이밍도 티켓 키 기준으로 통일 
(ex <code>feature/QUEUE-52-login-api</code>)</li>
</ul>
</li>
</ul>
<p>즉, 작게 만들고, 빠르게 적용하고, 반복적으로 개선하는 <strong>애자일 개발 전략</strong>을 따릅니다.</p>
<hr>
<h3 id="➡️-erd">➡️ ERD</h3>
<p>기능 구현에 앞서, 핵심 데이터 흐름과 테이블 간 관계를 먼저 정의했고,
그에 따라 설계된 주요 테이블 구조는 아래와 같다.</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/3fe63545-7e12-45a4-a6c7-99e56f02323a/image.png" width = 500>

<table>
<thead>
<tr>
<th>테이블명</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>student</td>
<td>사용자 기본 정보 (학번, 이름, 단과대, 학과, 학년 등)</td>
</tr>
<tr>
<td>student_policy</td>
<td>각 학생에게 적용되는 수강 정책 정보 (최대 수강 가능 학점, 전공/교양 조건 등)</td>
</tr>
<tr>
<td>course_info</td>
<td>강의 개설 조건 (학기, 전공 여부 등 메타 정보 정의)</td>
</tr>
<tr>
<td>course</td>
<td>실제 개설된 강의 정보 (과목명, 교수명, 수업시간, 정원 등)</td>
</tr>
<tr>
<td>course_time</td>
<td>강의 시간 정보 (요일, 시작/종료 시간 등)</td>
</tr>
<tr>
<td>course_enrollment</td>
<td>학생의 수강 신청 내역. student와 course 간의 중간 테이블 역할</td>
</tr>
</tbody></table>
<p><strong>👉 테이블 간 관계 및 설계 의도</strong>
<strong>student ↔ student_policy (1:1 관계)</strong> 
→ 한 학생마다 고유의 수강 정책이 존재하기 때문에 명확히 분리</p>
<p><strong>student ↔ course_enrollment ↔ course (N:M 관계)</strong>
→ 수강 신청 로직에는 단순 연결 외에도
✔ 중복 신청 방지
✔ 시간 중복 체크
✔ 신청 마감 여부 검증 등 복잡한 로직이 존재
→ 이를 모두 처리하기 위해 중간 테이블을 중심으로 구조 설계</p>
<p><strong>course ↔ course_time (1:N 관계)</strong>
→ 하나의 강의가 여러 요일/시간대에 개설될 수 있는 구조
→ ex) 월/수, 화/목 각각 다른 시간대 강의</p>
<p><strong>course ↔ course_info (N:1 관계)</strong>
→ 과목 자체에 대한 분류 정보(전공/교양, 학기 등)를 course_info에 따로 분리하여 필터링 성능을 높이고, 재사용 가능한 메타 정보로 활용</p>
<hr>
<h3 id="➡️-서버-아키텍처">➡️ 서버 아키텍처</h3>
<img src = "blob:https://velog.io/de301ce1-8437-45f9-8051-dc94f3132d4d" width = 400>

<p><strong>① 사용자 (Client)</strong>
사용자가 수강신청 버튼을 클릭하면 요청은 Spring REST API 서버로 전송</p>
<p><strong>② Spring 서버 (Docker 컨테이너)</strong>
서버는 EC2 인스턴스 내 Docker 컨테이너에 배포되어 있음
수신된 요청을 처리하고 필요한 비즈니스 로직(중복 검사, 정원 제한 등)을 수행
이때 <code>@Transactional</code> 기반 <strong>트랜잭션 처리</strong>와 <strong>DB 레벨의 Lock</strong>을 통해 동시성 제어</p>
<p><strong>③ RDS (MySQL)</strong>
검증을 통과한 신청 정보는 AWS RDS에 위치한 MySQL DB에 저장</p>
<hr>
<h3 id="➡️-다음-단계">➡️ 다음 단계</h3>
<p>지금까지 수강신청 시스템의 기본 기능을 어떻게 설계하고 구현했는지 살펴보았습니다. 하지만 실제로 이 시스템이 수백 명, 수천 명의 동시 요청을 견딜 수 있을지는 아직 확인되지 않았습니다.</p>
<p>다음 단계에서는 <strong>JMeter</strong>를 활용해 부하 테스트를 진행하고,
<strong>어떤 시점에 병목이 발생</strong>하는지, 그 병목을 <strong>어떻게 개선</strong>할 수 있을지 구체적으로 분석해보려고 합니다.</p>
<p>단순히 잘 작동하는 것이 아니라 많은 사용자를 견디는 시스템으로 확장해나가는 과정을 기록할 예정입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 MVC 1편] 1]]></title>
            <link>https://velog.io/@smj_716/%EC%8A%A4%ED%94%84%EB%A7%81-MVC-1%ED%8E%B8-1</link>
            <guid>https://velog.io/@smj_716/%EC%8A%A4%ED%94%84%EB%A7%81-MVC-1%ED%8E%B8-1</guid>
            <pubDate>Mon, 12 May 2025 14:54:01 GMT</pubDate>
            <description><![CDATA[<h2 id="1-웹-애플리케이션의-기본-구성">1. 웹 애플리케이션의 기본 구성</h2>
<p><strong>➡️ 웹 서버(Web Server)</strong></p>
<ul>
<li>HTTP 기반으로 동작</li>
<li>정적인 리소스(HTML, CSS, JS, 이미지 등)를 클라이언트에게 제공</li>
<li>ex) Nginx, Apache</li>
</ul>
<p>단순히 요청받은 정적 파일을 그대로 응답해주는 역할이다.</p>
<p><strong>➡️ 웹 애플리케이션 서버(WAS: Web Application Server)</strong></p>
<ul>
<li>HTTP 기반으로 동작</li>
<li>동적인 데이터를 생성해 HTML 또는 JSON으로 응답</li>
<li>프로그램 코드를 실행해서 애플리케이션 로직 수행</li>
<li>ex) Tomcat, Jetty</li>
</ul>
<p>웹 서버 기능을 포함하고 애플리케이션 로직을 실행할 수 있는 서버이다.</p>
<p><strong>➡️ WEB + WAS + DB 구성</strong>
실제 대규모 시스템은 웹 서버, WAS, DB가 각각 역할을 나누어 수행한다.</p>
<img src = "https://velog.velcdn.com/images/smj_716/post/351fc821-0361-41f9-9679-cfa70cb17dbc/image.png" width = "500">

<p>WAS + DB만으로 시스템 구성은 가능하지만 <strong>웹 서버는 정적 리소스</strong>를 제공하고 <strong>복잡한 처리는 WAS</strong>로 위임한다면 
❗WAS 과부하 우려가 줄고
❗WAS, DB 장애시 WEB 서버가 오류 화면 제공 가능</p>
<hr>
<h2 id="2-서블릿">2. 서블릿</h2>
<p>서버는 HTTP 요청을 받으면 아래와 같이 처리해야할 업무가 매우 많다. </p>
<img src = "https://velog.velcdn.com/images/smj_716/post/67fd4dde-65ca-4775-a8aa-8647f519f822/image.png" width = "450">

<p>하지만 핵심은 <code>초록색 박스</code>가 의미있는 비즈니스 로직이라는 것이다. 
이 업무를 대신해주는 것이 바로 <strong>서블릿</strong>이다.</p>
<p><strong>👉 특징</strong></p>
<pre><code class="language-java">@WebServlet(name = &quot;helloServlet&quot;, urlPatterns = &quot;/hello&quot;)
public class HelloServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse res) {
        // 요청 처리
}</code></pre>
<ul>
<li><code>urlPatterns(/hello)</code>의 URL이 호출되면 서블릿 코드가 실행</li>
<li>HTTP 요청 정보를 편리하게 사용할 수 있는 <strong>HttpServletRequest</strong></li>
<li>HTTP 응답 정보를 편리하게 제공할 수 있는 <strong>HttpServletResponse</strong></li>
<li>개발자는 HTTP 스펙을 매우 편리하게 사용</li>
</ul>
<p><strong>👉 HTTP 요청, 응답 흐름</strong></p>
<img src = "https://velog.velcdn.com/images/smj_716/post/d0789663-6280-453a-bf54-b4d55f44361a/image.png" width = "450">

<h3 id="📌서블릿-컨테이너">📌서블릿 컨테이너</h3>
<ul>
<li>톰캣처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 한다.</li>
<li>서블릿 객체를 생성, 초기화, 호출, 종료하는 생명주기를 관리한다.</li>
<li>서블릿 객체는 <strong>싱글톤</strong>으로 관리된다.<ul>
<li>So, 공유 자원은 주의해야한다.</li>
<li>최초 요청 시 서블릿 객체를 생성하고 이후 모든 요청은 해당 객체를 재사용한다.</li>
</ul>
</li>
<li>동시 요청을 위한 <strong>멀티 쓰레드</strong> 처리를 지원한다.</li>
</ul>
<hr>
<h2 id="3-멀티-쓰레드">3. 멀티 쓰레드</h2>
<p><small>_* 쓰레드는 한번에 하나의 코드 라인만 수행하기 때문에 동시 처리가 필요하면 쓰레드를 추가로 생성해야한다. _</small></p>
<p><strong>그럼 다중 요청 시 요청당 쓰레드를 생성해야할까❓</strong>
❌ 아니다!</p>
<ul>
<li>쓰레드 생성/제거 비용과 컨텍스트 스위칭 비용이 발생한다.</li>
<li>응답 속도가 느려진다.</li>
<li>많은 요청이 오면 CPU, 메모리 임계점을 넘어서 서버가 죽을 수 있다.</li>
</ul>
<h3 id="➡️-쓰레드-풀thread-pool의-도입">➡️ 쓰레드 풀(Thread Pool)의 도입</h3>
<img src = "https://velog.velcdn.com/images/smj_716/post/82313c95-ffc6-4a03-8c26-2a34df0136de/image.png" width = "450">

<ul>
<li>필요한 쓰레드를 풀에 보관하여 필요할 때 꺼내어 사용하고 반납한다. </li>
<li>쓰레드 풀에 생성 가능한 쓰레드의 최대치를 관리한다. -&gt; 톰캣은 최대 200개 기본 설정(변경 가능)</li>
<li>쓰레드 수가 초과되면 요청을 대기시키거나 거절할 수 있다.</li>
<li>쓰레드 생성/종료 비용이 절약되고 응답 시간도 빠르다. </li>
<li>최대치가 있어서 너무 많은 요청이 들어와도 안전하게 처리할 수 있다. </li>
</ul>
<p><strong>🌟 실무 Tip</strong></p>
<ul>
<li>WAS의 주요 <strong>튜닝 포인트는 최대 쓰레드 (max thread) 수</strong>이다!<ul>
<li>이 값을 너무 낮게 설정하면 -&gt; 동시 요청 많을 시 응답이 지연되고</li>
<li>이 값을 너무 높게 설정하면 -&gt; 동시 요청 많을 시 CPU, 메모리 리소스 임계점 초과로 서버 다운</li>
</ul>
</li>
<li>*<em>장애 발생 시 *</em><ul>
<li>클라우드면 일단 서버부터 늘리고 이후에 튜닝</li>
<li>클라우드가 아니면 열심히 튜닝</li>
</ul>
</li>
</ul>
<p>*<em>‼️개발자가 멀티 쓰레드 관련 코드를 신경쓰지 않아도 되며 이 부분은 WAS가 처리한다는 것이 핵심이다. *</em></p>
<hr>
<h2 id="4-html-http-api-csr-ssr">4. HTML, HTTP API, CSR, SSR</h2>
<p><strong>👉 정적 리소스</strong></p>
<ul>
<li>고정된 HTML 파일, CSS, JS, 이미지, 영상</li>
</ul>
<p><strong>👉 HTML 페이지</strong></p>
<ul>
<li>동적 HTML, JSP, 타임리프 등 서버에서 HTML을 동적으로 생성</li>
</ul>
<p><strong>👉 HTTP API</strong></p>
<ul>
<li>HTML이 아니라 <code>JSON 데이터</code>를 주고받는 구조</li>
<li>다양한 시스템에서 호출 가능<ul>
<li>웹 브라우저에서 자바스크립트를 통한 HTTP API 호출</li>
<li>앱 클라이언트</li>
<li>React,Vue.js 같은 웹 클라이언트</li>
<li>서버 to 서버 (ex) 주문 서버 -&gt; 결제 서버)</li>
</ul>
</li>
</ul>
<p><strong>➡️ SSR (서버 사이드 렌더링)</strong></p>
<img src="https://velog.velcdn.com/images/smj_716/post/31720fc3-f518-4a1e-ab38-89b0477c00c7/image.png" width = "400">

<ul>
<li>HTML 최종 결과를 서버에서 만들어 웹 브라우저에 전달</li>
<li>주로 정적인 화면에 사용</li>
<li>관련 기술 : JSP, 타임리프 -&gt; 백엔드 개발자</li>
</ul>
<p><strong>➡️ CSR (클라이언트 사이드 렌더링)</strong></p>
<img src="https://velog.velcdn.com/images/smj_716/post/bc67e60d-8740-4018-a9ee-7430ff0ebffe/image.png" width = "450">

<ul>
<li>HTML 결과를 자바스크립트를 사용해 웹 브라우저에서 동적으로 생성해서 적용</li>
<li>주로 동적인 화면에 사용하고 웹 환경을 마치 앱 처럼 필요한 부분부분 변경 가능</li>
</ul>
<hr>
<h2 id="5-자바-웹-기술-역사">5. 자바 웹 기술 역사</h2>
<p><strong>➕ 과거 기술</strong></p>
<ul>
<li><code>1997</code> <strong>서블릿</strong> : HTML 생성이 불편</li>
<li><code>1999</code> <strong>JSP</strong> : HTML은 편리하나 비지니스 로직까지 너무 많은 역할 담당</li>
<li><strong>서블릿, JSP 조합</strong> MVC 패턴 사용 : 모델, 뷰 컨트롤러로 역할 나누어 개발</li>
<li><code>2000</code> <strong>MVC 프레임워크</strong> 춘추전국시대    : Spring MVC(과거버전), Struts 등</li>
</ul>
<p><strong>➕ 현재 기술</strong></p>
<ul>
<li>*<em>애노테이션 기반의 스프링 MVC 등장 *</em></li>
<li><strong>Spring Boot</strong> 등장 -&gt; 서버 내장, Jar로 빌드 배포 단순화</li>
</ul>
<p><strong>➕ 최근 기술</strong></p>
<ul>
<li>Web Servlet - <strong>Spring MVC</strong></li>
<li>Web Reactive - <strong>Spring WebFlux</strong><ul>
<li>비동기 넌 블러킹 처리/ 최소 쓰레드로 최대 성능/ 함수형 스타일 개발의 장점</li>
<li>But, 난의도 매우 높고 RDS 지원 부족해 실무에서 거의 사용 X</li>
</ul>
</li>
</ul>
<p><strong>➕ 뷰 템플릿 역사</strong></p>
<ul>
<li><strong>JSP</strong>: 느리고 기능 부족</li>
<li><strong>Freemarker, Velocity</strong>: 속도 문제 해결, 다양한 기능</li>
<li><strong>Thymeleaf</strong><ul>
<li>HTML 구조를 유지하며 뷰 템플릿 적용 가능 </li>
<li>스프링 MVC와 강력한 기능 통합</li>
<li>최선의 선택이지만 성능은 프리마커, 벨로시티가 더 빠름</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 핵심 원리] 8]]></title>
            <link>https://velog.io/@smj_716/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-8</link>
            <guid>https://velog.io/@smj_716/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-8</guid>
            <pubDate>Sun, 11 May 2025 07:30:43 GMT</pubDate>
            <description><![CDATA[<h2 id="1-빈-스코프란">1. 빈 스코프란?</h2>
<blockquote>
<p>빈이 존재하는 생명 주기 범위를 지정하는 것이다.</p>
</blockquote>
<p>스프링에서는 기본적으로 모든 빈이 <strong>싱글톤</strong>이다. 
즉, 애플리케이션 시작 시 한 번만 생성되고 모든 의존 주입 시 동일한 인스턴스를 공유하게 된다.
그런데 이렇게 항상 같은 객체를 공유하는 것이 불편한 상황도 있다.
예를 들어, 매번 새로운 객체가 필요한 경우나 요청마다 다른 객체가 필요할 경우이다. 이럴 때 사용하는 개념이 바로 <strong>빈 스코프</strong>이다.  </p>
<p>👉 스프링은 아래와 같은 스코프를 지원한다. </p>
<ul>
<li><strong><code>싱글톤</code></strong> : 기본값. 컨테이너 시작~종료까지 1개의 인스턴스 유지</li>
<li><strong><code>프로토타입</code></strong> : 요청할 때마다 매번 새로운 인스턴스 생성</li>
<li>웹 관련 스코프: <ul>
<li><strong><code>request</code></strong> : 웹 요청마다 새로운 빈 생성 </li>
<li><strong><code>session</code></strong> : HTTP 세션마다 하나의 인스턴스 유지</li>
<li><strong><code>application</code></strong> : 서블릿 컨텍스트 범위와 같은 스코프 </li>
<li><strong><code>websocket</code></strong> : 웹 소켓과 동일한 생명주기를 가지는 스코프 </li>
</ul>
</li>
</ul>
<p>👉 빈 스코프 지정하는 방법 </p>
<pre><code class="language-java">@Component
@Scope(&quot;prototype&quot;)
public class HelloBean {}</code></pre>
<p>또는 수동으로 </p>
<pre><code class="language-java">@Bean
@Scope(&quot;prototype&quot;)
public HelloBean helloBean() {
    return new HelloBean();
}</code></pre>
<hr>
<h2 id="2-프로토타입-스코프">2. 프로토타입 스코프</h2>
<p>싱글톤과 달리 프로토타입(prototype) 스코프는 스프링 컨테이너가 매번 새로운 인스턴스를 생성해서 반환한다. </p>
<img src="https://velog.velcdn.com/images/smj_716/post/ef356a03-7d7c-4fd6-9267-a78132060baa/image.png" width = "450">

<p><strong>💡 동작 흐름</strong></p>
<ol>
<li>스프링 컨테이너가 요청을 받으면 
(프로토타입 빈을 생성하고 필요한 의존 관계를 주입 + 초기화) </li>
<li>매번 새로운 객체를 생성하여 반환</li>
<li>이후 이 <strong>객체에 대한 생명주기 관리</strong>는 스프링이 아닌 <strong>클라이언트가 책임</strong>
<em><strong>-&gt;</strong></em> <strong>따라서 <code>@PreDestory</code> 메서드도 호출되지 않음!!</strong></li>
</ol>
<pre><code class="language-java">@Scope(&quot;prototype&quot;)
static class PrototypeBean {
    @PostConstruct
    public void init() {
        System.out.println(&quot;init&quot;);
    }

    @PreDestroy
    public void destroy() {
        System.out.println(&quot;destroy&quot;);
    }
}</code></pre>
<p>이렇게 작성하고 <code>ac.getBean(PrototypeBean.class)</code>을 2번 호출하면 서로 다른 인스턴스가 생성되어<code>init()</code>은 각각 호출되지만 <code>destroy()</code>는 호출되지 않는다. 
<strong>‼️ 즉, 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입 그리고 초기화까지만 관여한다.</strong></p>
<hr>
<h2 id="3-싱글톤-빈과-함께-사용시-문제점">3. 싱글톤 빈과 함께 사용시 문제점</h2>
<p>아래 그림은 싱글톤 빈(clientBean)이 프로토타입 빈(PrototypeBean)을 주입받아 사용하는 구조를 보여준다.</p>
<img src="https://velog.velcdn.com/images/smj_716/post/52f840ce-fcc2-42e4-898c-7d6ff17e5b98/image.png" width = "450">

<ol>
<li><code>clientBean</code>이 생성될 때 프로토타입 빈이 한 번 주입</li>
<li>클라이언트 A가 <code>logic()</code>을 호출하면 <code>count</code>는 1</li>
<li>클라이언트 B도 같은 <code>clientBean</code>을 사용하므로 다시 호출 시 <code>count</code>가 2로 증가</li>
</ol>
<p>⚠️ <strong>싱글톤 빈은 생성 시점에만 의존성이 주입</strong>되기 때문에 프로토타입 빈이 사용 시마다 새로 생성되지 않고 <strong>처음 주입된 인스턴스를 계속 사용</strong>하게 된다. 즉, <strong>프로토타입의 의미가 사라지는 것</strong>이다!</p>
<pre><code class="language-java">// 생략된 코드 동일하게 유지
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic(); // count = 1

ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic(); // count = 2</code></pre>
<p>프로토타입의 생명주기를 활용하고 싶다면 매번 새롭게 요청하는 방식으로 로직을 수정할 필요가 있다. </p>
<hr>
<h2 id="4-provider">4. Provider</h2>
<p>싱글톤 빈에서 프로토타입 빈을 사용할 때마다 새로 생성되게 하려면 가장 간단한 방법은 <strong>필요할 때 스프링 컨테이너에 직접 요청하는 것(DL)</strong>이다. </p>
<p>이는 의존관계를 외부에서 주입받는 것(DI)이 아니라 <strong>필요한 의존관계를 직접 찾는 것(Dependency Lookup)</strong>이다. </p>
<p>그런데 ApplicationContext 전체를 주입받는 방식은 스프링 컨테이너에 종속적인 코드가 되고 단위 테스트도 어려워진다. </p>
<h3 id="1️⃣-objectprovider-사용-스프링-제공"><strong>1️⃣ ObjectProvider 사용 (스프링 제공)</strong></h3>
<pre><code class="language-java">@Autowired
private ObjectProvider&lt;PrototypeBean&gt; provider;

public int logic() {
    // 매번 새 인스턴스 반환
    PrototypeBean prototypeBean = provider.getObject(); 
    prototypeBean.addCount();
    return prototypeBean.getCount();
}</code></pre>
<ul>
<li>스프링은 지금 필요한 프로토타입 빈을 컨테이너에서 대신 찾아주는 즉 <strong>DL 역할만 하는 <code>ObjectProvider</code>를 제공</strong>한다 </li>
<li><code>provider.getObject()</code>를 호출하는 시점에 스프링 컨테이너가 새로운 프로토타입 빈을 생성하여 반환한다.</li>
</ul>
<h3 id="2️⃣-jsr-330-provider-사용-자바-표준"><strong>2️⃣ JSR-330 Provider 사용 (자바 표준)</strong></h3>
<pre><code class="language-java">@Autowired
private javax.inject.Provider&lt;PrototypeBean&gt; provider;

public int logic() {
    PrototypeBean prototypeBean = provider.get();
    prototypeBean.addCount();
    return prototypeBean.getCount();
}</code></pre>
<ul>
<li>자바 표준 방식으로 다른 DI 컨테이너에서도 사용 가능하다.</li>
<li>단점은 별도 라이브러리 의존이 필요하다.<ul>
<li>ex) <code>javax.inject:javax.inject:1</code></li>
</ul>
</li>
</ul>
<p>_<small>자바 표준과 스프링 기능이 겹치는 경우, 특별히 다른 컨테이너를 사용할 일이 없다면 스프링이 제공하는 기능을 사용하자!</small>_</p>
<hr>
<h2 id="5-웹-스코프와-프록시-사용">5. 웹 스코프와 프록시 사용</h2>
<p>스프링에서는 웹 애플리케이션을 개발할 때 <strong>요청마다 새롭게 생성되어야 하는 객체</strong>가 필요할 때가 있다. 예를 들어, HTTP 요청마다 고유한 로깅 정보를 담고 싶은 경우이다. 
이럴 때 사용하는 것이 바로 <strong>웹 스코프(Web Scope)</strong>이다. </p>
<p>_<small>아래는 가장 많이 사용되는 request 스코프를 중심으로 설명한다.</small>_</p>
<h3 id="✅-예제--요청마다-고유한-로그를-남기고-싶다">✅ 예제 – 요청마다 고유한 로그를 남기고 싶다!</h3>
<p><code>MyLogger</code>라는 클래스를 만들어 요청마다 고유한 UUID를 생성하고 로그에 찍도록 설계한다.</p>
<pre><code class="language-java">@Component
@Scope(value = &quot;request&quot;)
public class MyLogger {

    private String uuid;
    private String requestURL;

    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String message) {
        System.out.println(&quot;[&quot; + uuid + &quot;][&quot; + requestURL + &quot;] &quot; + message);
    }

    @PostConstruct
    public void init() {
        uuid = UUID.randomUUID().toString();
        System.out.println(&quot;[&quot; + uuid + &quot;] request scope bean created: &quot; + this);
    }

    @PreDestroy
    public void destroy() {
        System.out.println(&quot;[&quot; + uuid + &quot;] request scope bean closed: &quot; + this);
    }
}</code></pre>
<h4 id="❌-그런데-문제-발생">❌ 그런데 문제 발생!</h4>
<p>다음처럼 <code>MyLogger</code>를 일반 싱글톤 컨트롤러에 그냥 주입하면 애플리케이션 실행 시점에 에러가 난다. </p>
<pre><code class="language-java">@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final MyLogger myLogger; // ❌ 에러 발생
    ...
}</code></pre>
<p><strong>💡 왜 에러가 날까?</strong></p>
<ul>
<li><code>@Scope(&quot;request&quot;)</code>는 HTTP 요청이 있을 때만 생성될 수 있다.</li>
<li>하지만 싱글톤 빈은 애플리케이션 실행 시점에 먼저 생성된다.</li>
<li>이 시점에서는 HTTP 요청이 없기 때문에 <code>MyLogger</code> 빈도 만들 수 없다. </li>
</ul>
<h3 id="✅-해결-1-objectprovider">✅ 해결 1: ObjectProvider</h3>
<pre><code class="language-java">@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final ObjectProvider&lt;MyLogger&gt; myLoggerProvider;

    @RequestMapping(&quot;/log-demo&quot;)
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        // 요청 시점에 빈 생성
        MyLogger myLogger = myLoggerProvider.getObject(); 
        myLogger.setRequestURL(request.getRequestURL().toString());
        myLogger.log(&quot;controller test&quot;);
        return &quot;OK&quot;;
    }
}</code></pre>
<p>이렇게 하면 <code>getObjcet()</code>를 호출하는 시점에 HTTP 요청이 진행중이므로<code>MyLogger</code> 빈이 생성된다.
즉, 빈 생성을 지연시켜서 문제를 해결하는 것이다. </p>
<h3 id="✅-해결-2-프록시proxy">✅ 해결 2: 프록시(proxy)</h3>
<p>더 간단히 처리할 수 있는 방법은 프록시 객체를 사용하는 것이다. </p>
<pre><code class="language-java">@Component
@Scope(value = &quot;request&quot;, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
    ...
}</code></pre>
<p>이 설정을 추가하면 스프링이 <code>MyLogger</code>를 상속받은 가짜 객체(프록시)를 먼저 만들어서 싱글톤 빈들에 미리 주입해놓는다.
그럼 컨트롤러는 아래와 같이 평범하게 사용할 수 있다. </p>
<pre><code class="language-java">@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final MyLogger myLogger; // ➡️ 프록시 객체가 들어옴

    @RequestMapping(&quot;/log-demo&quot;)
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        myLogger.setRequestURL(request.getRequestURL().toString());
        myLogger.log(&quot;controller test&quot;);
        return &quot;OK&quot;;
    }
}</code></pre>
<p>프록시 객체는 내부에서 실제 request scope 빈을 찾아서 <strong>필요할 때 위임</strong>한다. 
즉, 개발자는 마치 싱글톤처럼 쓰지만 내부에서는 요청마다 새 객체가 사용되는 것이다.</p>
<p>프록시가 실제로 동작하는지 확인하려면 
<code>System.out.println(&quot;myLogger = &quot; + myLogger.getClass());</code> 로 출력을 하면
<code>myLogger = ..MyLogger$$EnhancerBySpringCGLIB...</code> 라고 출력이된다. </p>
<ul>
<li><strong>CGLIB</strong> 라는 라이브러리로 <strong>가짜 객체가 생성</strong>된 것을 확인할 수 있다.</li>
<li>이 객체는 내부적으로 <strong>진짜 request scope빈을 찾아서 사용</strong>하는 기능만 가지고 있는 프록시이다. </li>
<li>가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지도 모르게 동일하게 사용할 수 있다. (다형성)</li>
</ul>
<p><small>_특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자. 무분별하게 사용하면 유지보수하기 어려워진다.
_</small></p>
<p>🌟 두 방식의 핵심 공통점은 모두 <strong>실제 빈 생성을 필요한 시점까지 미룬다</strong>는 것이다. 실제 객체는 지연 생성되고 로직은 그대로 유지할 수 있다는 점이 큰 장점이다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 핵심 원리] 7]]></title>
            <link>https://velog.io/@smj_716/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-7</link>
            <guid>https://velog.io/@smj_716/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-7</guid>
            <pubDate>Mon, 05 May 2025 17:14:12 GMT</pubDate>
            <description><![CDATA[<p>스프링을 사용하다 보면 객체의 초기화와 종료 시점에 작업을 해줘야 할 때가 있다. 예를 들어 네트워크 연결을 열거나 닫아야 할 때, 또는 파일이나 데이터베이스 리소스를 정리해야 할 때이다. </p>
<h3 id="💡-왜-생명주기-콜백이-필요할까">💡 왜 생명주기 콜백이 필요할까?</h3>
<p>어플리케이션을 실행할 때 꼭 필요한 작업들이 있다.</p>
<ul>
<li>서버 시작 시 외부 서버와 네트워크 연결</li>
<li>애플리케이션 종료 시 네트워크 연결 해제 </li>
</ul>
<p>이처럼 <strong>객체 생성 -&gt; 의존관계 주입 -&gt; 초기화 -&gt; 사용 -&gt; 종료 전 정리</strong> 과정이 필요하다. 스프링은 이 과정을 자동으로 관리할 수 있는 콜백 메커니즘을 제공한다. </p>
<h2 id="🔁-스프링-빈의-라이프사이클">🔁 스프링 빈의 라이프사이클</h2>
<p>스프링 빈은 아래와 같은 순서로 동작한다.</p>
<ol>
<li>스프링 컨테이너 생성</li>
<li>스프링 빈 생성</li>
<li>의존관계 주입</li>
<li>초기화 콜백</li>
<li>빈 사용</li>
<li>소멸 전 콜백 </li>
<li>스프링 종료
🌟 핵심은 <strong>초기화 작업</strong>은 <strong>의존관계 주입 이후에 실행</strong>돼야 한다는 것이다!!!</li>
</ol>
<p><em>생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다. 반면 초기화는 이렇게 생성된 값들을 활용해 외부 커넥션을 연결하는 등 무거운 동작을 수행한다.</em></p>
<hr>
<p>조금 더 이해하기 쉽도록 아래와 같은 예시를 보며 이해해보자!</p>
<p><strong>[NetworkClient 클래스]</strong></p>
<pre><code class="language-java">public NetworkClient() {
    System.out.println(&quot;생성자 호출, url = &quot; + url);
    connect(); // 문제: 아직 URL이 주입되지 않음
}</code></pre>
<p>객체 생성하는 단계에는 url이 없고, 객체를 생성한 다음에 외부에서 수정자 주입을 통해 <code>setUrl()</code>이 호출되어야 url이 존재하게 된다. 즉 url이 아직 주입되기 전이라 null 상태에서 <code>connect()</code>가 실행되는 것이다. 
<strong>따라서 초기화는 생성자가 아니라 따로 분리해서 처리해야한다.</strong> </p>
<img src = "https://velog.velcdn.com/images/smj_716/post/0457508f-5115-4d1f-b77b-0be05ccde627/image.png" width = "500">

<p>스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료된다. 즉 위에서 언급한대로 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 한다. <strong>그런데 개발자가 의존관계 주입이 모두 완료된 시점을 어떻게 알 수 있을까?</strong></p>
<p>✅ 스프링은 의존관계 주입이 완료되면 스프링 빈에게 <strong>콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공</strong>한다. 또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 따라서 <strong>안전하게 종료 작업을 진행</strong>할 수 있다.</p>
<hr>
<h2 id="️-생명주기-콜백-방법-3가지">‼️ 생명주기 콜백 방법 3가지</h2>
<h3 id="1-인터페이스-방식-initializingbean-disposablebean"><strong>1. 인터페이스 방식 (InitializingBean, DisposableBean)</strong></h3>
<p>스프링이 제공하는 인터페이스를 직접 구현해서 초기화/소멸 메서드를 정의하는 방법이다.</p>
<ul>
<li><p><code>afterPropertiesSet()</code>은 빈이 생성되고 의존관계 주입이 끝난 뒤 자동으로 호출된다.</p>
</li>
<li><p><code>destroy()</code>는 컨테이너가 종료되기 전에 호출된다.</p>
<pre><code class="language-java">public class NetworkClient implements InitializingBean, DisposableBean {
  private String url;

  public void setUrl(String url) {
      this.url = url;
  }

  @Override
  public void afterPropertiesSet() {
      connect();
      call(&quot;초기화 메시지&quot;);
  }

  @Override
  public void destroy() {
      disconnect();
  }
}</code></pre>
</li>
<li><p><em>👉 단점 *</em></p>
</li>
<li><p>스프링 전용 인터페이스이기 때문에 <strong>스프링에 종속</strong>된다. 즉 스프링이 없는 환경에선 재사용이 불가능하다. </p>
</li>
<li><p>메서드 이름을 바꿀 수 없다. </p>
</li>
<li><p>내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.</p>
</li>
</ul>
<p><em>요즘은 거의 사용하지 않는 방식이다!!</em></p>
<h3 id="2-bean-등록-시-메서드-지정"><strong>2. @Bean 등록 시 메서드 지정</strong></h3>
<p><code>@Bean</code> 어노테이션에서 초기화/종료 메서드의 이름을 지정해준다.
빈 클래스 내부에 <code>init()</code>, <code>close()</code> 등의 메서드를 자유롭게 만들고 설정에서 지정하는 방식이다.</p>
<pre><code class="language-java">public class NetworkClient {
    public void init() {
        connect();
        call(&quot;초기화 메시지&quot;);
    }

    public void close() {
        disconnect();
    }
}

@Configuration
static class Config {
    @Bean(initMethod = &quot;init&quot;, destroyMethod = &quot;close&quot;)
    public NetworkClient networkClient() {
        NetworkClient client = new NetworkClient();
        client.setUrl(&quot;http://hello.dev&quot;);
        return client;
    }
}</code></pre>
<p>*<em>👉 장점 *</em></p>
<ul>
<li>메서드 이름을 자유롭게 지을 수 있다. </li>
<li>스프링 코드에 의존하지 않는다.</li>
<li>외부 라이브러리에도 적용 가능하다.</li>
</ul>
<p>*<em>👉 단점 *</em></p>
<ul>
<li>설정에서 메서드 이름을 지정해줘야한다.</li>
</ul>
<p>_Tip : <code>destroyMethod</code>는 기본값이 <code>&quot;inferred&quot;</code>로 되어 있어서 메서드 이름이 close나 shutdown이면 자동으로 호출된다.
→ 굳이 destroyMethod = &quot;close&quot; 안 써도 된다.
_</p>
<h3 id="3-postconstruct--predestroy-애노테이션"><strong>3. @PostConstruct / @PreDestroy 애노테이션</strong></h3>
<p>최신 스프링에서 가장 권장하는 방법이다.
자바 표준 애노테이션 <code>@PostConstruct</code>, <code>@PreDestroy</code>를 사용하면 간단히 초기화/소멸을 구현할 수 있다.</p>
<pre><code class="language-java">public class NetworkClient {
    @PostConstruct
    public void init() {
        connect();
        call(&quot;초기화 메시지&quot;);
    }

    @PreDestroy
    public void close() {
        disconnect();
    }
}</code></pre>
<p>*<em>👉 장점 *</em></p>
<ul>
<li>코드에 애노테이션만 붙이면 되니까 매우 간편하다.</li>
<li>스프링 전용이 아니라 자바 표준이기 때문에 다른 컨테이너에서도 동작 가능하다.</li>
<li>컴포넌트 스캔과 잘 작동한다.</li>
</ul>
<p>*<em>👉 단점 *</em></p>
<ul>
<li>외부 라이브러리에는 애노테이션을 직접 붙일 수 없으니 적용이 불가하다.</li>
</ul>
<hr>
<h3 id="결론은-❓">결론은 ❓</h3>
<ul>
<li><strong>직접 만든 빈이면 → <code>@PostConstruct</code>, <code>@PreDestroy</code> 사용!</strong></li>
<li><strong>외부 라이브러리라면 → <code>@Bean(initMethod, destroyMethod)</code> 사용!</strong></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링 핵심 원리] 6]]></title>
            <link>https://velog.io/@smj_716/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-6</link>
            <guid>https://velog.io/@smj_716/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-6</guid>
            <pubDate>Sat, 03 May 2025 17:21:21 GMT</pubDate>
            <description><![CDATA[<h2 id="1-다양한-의존관계-주입-방법">1. 다양한 의존관계 주입 방법</h2>
<h3 id="➡️-1-생성자-주입">➡️ 1) 생성자 주입</h3>
<ul>
<li><p>생성자 호출시점에서 딱 1번만 호출되는 것이 보장된다.</p>
</li>
<li><p>불변, 필수 의존관계에 사용한다.</p>
<pre><code class="language-java">@Component
public class OrderServiceImpl implements OrderService {

  private final MemberRepository memberRepository;
  private final DiscountPolicy discountPolicy;

  @Autowired
  public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
       this.memberRepository = memberRepository;
       this.discountPolicy = discountPolicy;
  }
}</code></pre>
<p>⚠️ 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입이 된다.</p>
</li>
</ul>
<h3 id="➡️-2-수정자-주입">➡️ 2) 수정자 주입</h3>
<ul>
<li><p><code>setter</code>라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.</p>
</li>
<li><p>자바빈 프로퍼티의 수정자 메서드 방식이다. (=setter) </p>
</li>
<li><p>선택, 변경 가능성이 있는 의존관계에 사용한다.</p>
<pre><code class="language-java">...
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
   this.memberRepository = memberRepository;
}

@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
   this.discountPolicy = discountPolicy;
}
...</code></pre>
<p>⚠️ <code>@Autowired</code> 는 주입할 대상이 없으면 오류가 발생한다. 오류가 발생하지 않게 하려면 <code>@Autowired(required = false)</code>로 지정해야한다.</p>
</li>
</ul>
<h3 id="➡️-3-필드-주입">➡️ 3) 필드 주입</h3>
<ul>
<li>코드가 간결하고 쉽다.</li>
<li>But, 외부에서 값을 주입하거나 변경할 수 없어서(setter도 없고 생성자를 통한 주입도 아니니) 테스트에서 객체를 바꿔 넣기 어렵다는 단점이 있다.</li>
<li>DI 프레임워크(스프링) 없이는 객체를 만들 수 없다.<ul>
<li>⚠️ 스프링 컨테이너는 @Conponent 등이 붙은 클래스들을 자동으로 스프링 빈에 등록하여 관리해주는 통이다. 이 통 안에 들어간 객체만 스프링이 주입(<code>@Autowired</code>)도 관리도 해주기 때문에 new로 직접 객체를 생성하면 스프링은 몰라서 null이 될 수도 있다. <pre><code class="language-java">...
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
...</code></pre>
</li>
</ul>
</li>
<li>즉, 사용하지말자 ‼️ </li>
<li>아래의 경우는 고려해보자<ul>
<li>애플리케이션의 실제 코드와 관계 없는 테스트 코드</li>
<li>스프링 설정을 목적으로 하는 <code>@Configuration</code> 같은 곳</li>
</ul>
</li>
</ul>
<h3 id="➡️-4-일반-메서드-주입">➡️ 4) 일반 메서드 주입</h3>
<ul>
<li>한번에 여러 필드를 주입 받을 수 있다.</li>
<li>잘 사용하진 않는다.<pre><code class="language-java">...
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy 
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
  }</code></pre>
</li>
</ul>
<hr>
<h2 id="2-옵션-처리">2. 옵션 처리</h2>
<p>주입할 <strong>스프링 빈이 없어도 동작해야 할 때</strong>가 있다. 
그런데 <code>@Autowired</code>만 사용하면 <code>required</code> 옵션의 기본값이 true로 되어 있어서 자동 주입 대상이 없으면 오류가 발생한다. </p>
<p>해결방법은 다음 3가지이다.</p>
<ul>
<li><strong><code>@Autowired(required=false)</code></strong> : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨</li>
<li><strong><code>org.springframework.lang.@Nullable</code></strong> : 자동 주입할 대상이 없으면 null이 입력됨</li>
<li><strong><code>Optional&lt;&gt;</code></strong> : 자동 주입 대상이 없으면 <code>Optional.empty</code>가 입력됨</li>
</ul>
<p><em>(@Nullable, Optional은 스프링 전반에 걸쳐서 지원된다. 예를 들어서 생성자 자동 주입에서 특정 필드에만 사용해도 된다.)</em></p>
<pre><code class="language-java">//호출 안됨
@Autowired(required = false)
 public void setNoBean1(Member member) {
    System.out.println(&quot;setNoBean1 = &quot; + member);
 }
 //null 호출
@Autowired
 public void setNoBean2(@Nullable Member member) {
    System.out.println(&quot;setNoBean2 = &quot; + member);
 }
 //Optional.empty 호출
@Autowired(required = false)
 public void setNoBean3(Optional&lt;Member&gt; member) {
    System.out.println(&quot;setNoBean3 = &quot; + member);
 }</code></pre>
<p>Member는 스프링 빈이 아니기 때문에 아래와 같이 setNoBean1 자체는 호출이 되지 않는 결과가 나온다. </p>
<img src = "https://velog.velcdn.com/images/smj_716/post/2efecc79-a819-4622-9cf6-15b7e4508e3b/image.png" width = "550">

<hr>
<h2 id="3-왜-생성자-주입을-권장할까">3. 왜 생성자 주입을 권장할까?</h2>
<h4 id="💡-불변성-보장">💡 불변성 보장</h4>
<ul>
<li>대부분의 의존 객체는 한 번 주입되면 변경되면 안된다.</li>
<li>생성자 주입은 객체 생성 시점에만 호출되기 때문에, 객체가 생성된 후에는 절대 바뀌지 않는다.</li>
<li>final 키워드를 사용할 수 있어서 컴파일 타임에 안정성을 확보한다.<h4 id="💡-의존성-누락-방지-컴파일-오류">💡 의존성 누락 방지 (컴파일 오류)</h4>
</li>
<li><code>java: variable discountPolicy might not have been initialized</code></li>
<li>수정자 주입이나 필드 주입은 의존성 누락이 있어도 컴파일 오류 없이 실행되어 NPE 가능성이 있다.</li>
<li>생성자 주입은 생성자 파라미터가 필수이므로 누락되면 컴파일 에러로 알려줘서 안전하다.<h4 id="💡-순수-자바-코드로-테스트-가능">💡 순수 자바 코드로 테스트 가능</h4>
<pre><code class="language-java">// 생성자 주입은 테스트에서도 명확하게 어떤 의존 객체가 필요한지 알 수 있음
OrderServiceImpl service = new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());</code></pre>
</li>
<li>DI 프레임워크 없이도 new를 이용하여 객체를 만들 수 있어서 스프링 없이도 테스트가 가능하다.</li>
</ul>
<p>✅ <strong>항상 생성자 주입을 선택하자! 그리고 가끔 옵션이 필요하면 수정자 주입을 선택하도록 하자.</strong></p>
<h3 id="🛠-룸복으로-더-간단하게">🛠 룸복으로 더 간단하게!</h3>
<p>롬복 라이브러리가 제공하는<code>@RequiredArgsConstructor</code> 기능을 사용하면 <code>final</code>이 붙은 필드를 모아서 컴파일 시점에 생성자를 자동으로 만들어준다. (자바의 애노테이션 프로세서라는 기능을 이용) 
➕ 생성자가 하나일 경우 <code>@Autowired</code> 생략이 가능하기 때문에 아래와 같이 코드가 훨씬 간단해진다. </p>
<pre><code class="language-java">@RequiredArgsConstructor
@Component
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}</code></pre>
<hr>
<h2 id="4-의존성-주입-충돌-빈이-2개-이상일-때">4. 의존성 주입 충돌 (빈이 2개 이상일 때)</h2>
<p><code>@Autowired</code>는 <strong>타입 기준</strong>으로 주입을 시도하여 같은 타입에 등록된 빈이 2개 이상일 경우 <code>NoUniqueBeanDefinitionException</code> 오류가 발생한다. </p>
<pre><code class="language-java">@Autowired
private DiscountPolicy discountPolicy; 
// DiscountPolicy 빈이 2개라면?  (FixDiscountPolicy, RateDiscountPolicy)</code></pre>
<p>이 문제를 해결하는 방법 3가지는 아래와 같다. </p>
<h3 id="👉-필드명-매칭"><strong>👉 필드명 매칭</strong></h3>
<p><code>@Autowired</code>는 먼저 타입 매칭을 시도하고 여러 빈이 있을 때 추가로 동작하는 기능이다. 아래와 같이 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.</p>
<pre><code class="language-java">@Autowired
private DiscountPolicy rateDiscountPolicy; // 빈 이름과 일치</code></pre>
<h3 id="👉-qualifier-사용"><strong>👉 @Qualifier 사용</strong></h3>
<p>추가 구분자를 붙여주는 방법이다. 
주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것이 아니다! 주의하자!</p>
<pre><code class="language-java">@Component
@Qualifier(&quot;mainDiscountPolicy&quot;)
public class RateDiscountPolicy implements DiscountPolicy {}

@Autowired
public OrderServiceImpl(
    MemberRepository memberRepository,
    @Qualifier(&quot;mainDiscountPolicy&quot;) DiscountPolicy discountPolicy) {
    ...
}</code></pre>
<p>만약 <code>@Qualifier(&quot;mainDiscountPolicy&quot;)</code>을 찾지 못한다면 <code>mainDiscountPolicy</code>라는 이름의 스프링 빈을 추가로 찾는다. 
추가로 찾을 시에도 없다면 <code>NoUniqueBeanDefinitionException</code> 오류가 발생한다. </p>
<h3 id="👉-primary-사용"><strong>👉 @Primary 사용</strong></h3>
<p>우선 순위를 정하는 방법이다. @Autowired 시에 여러 빈이 매칭되면 <code>@Primary</code>가 우선권을 가진다. </p>
<pre><code class="language-java">@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}</code></pre>
<p><strong>✅ @Qualifier vs @Primary</strong></p>
<ul>
<li><code>@Primary</code>는 기본(default) 빈으로 지정하는 것이고 별다른 지정이 없을 때 주입된다.</li>
<li><code>@Qualifier</code>는 명시적으로 특정 빈을 지정하는 것이고 @Primary보다 우선순위가 높다.</li>
<li><strong>자주</strong> 사용하는 빈은 <code>@Primary</code>, *<em>가끔 *</em>사용하는 빈은 <code>@Qualifier</code>로 지정하면 깔끔하게 관리할 수 있다.</li>
</ul>
<h3 id="커스텀-애노테이션으로-간결하게">커스텀 애노테이션으로 간결하게!</h3>
<p><code>@Qualifier(&quot;mainDiscountPolicy&quot;)</code> 이렇게 문자를 적으면 컴파일시 타입 체크가 안된다.</p>
<pre><code class="language-java">@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, 
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier(&quot;mainDiscountPolicy&quot;)
public @interface MainDiscountPolicy {}</code></pre>
<p>자바 애노테이션에는 상속이 없고, 여러 애노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능이다.</p>
<pre><code class="language-java]">//빈 정의 시
 @Component
 @MainDiscountPolicy
 public class RateDiscountPolicy implements DiscountPolicy {}</code></pre>
<pre><code class="language-java">//생성자 자동 주입시
@Autowired
 public OrderServiceImpl(MemberRepository memberRepository,
                         @MainDiscountPolicy DiscountPolicy discountPolicy) {
      this.memberRepository = memberRepository;
      this.discountPolicy = discountPolicy;
}</code></pre>
<p>But, 명확한 목적 없이 무분별하게 사용하면 유지보수에 혼란이 생기므로 주의하자!</p>
<hr>
<h2 id="5-list-map으로-전체-빈-주입받기">5. List, Map으로 전체 빈 주입받기</h2>
<p>할인 서비스를 제공하는데 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다고 가정해보자! 
이럴 땐 모든 <code>DiscountPolicy</code> 구현체를 한번에 주입받고 클라이언트 선택에 따라 전략적으로 실행하는 방법이 있다. </p>
<pre><code class="language-java">@Component
public class DiscountService {

    private final Map&lt;String, DiscountPolicy&gt; policyMap;
    private final List&lt;DiscountPolicy&gt; policies;

    // 모든 DiscountPolicy 빈이 Map과 List로 자동 주입됨
    public DiscountService(Map&lt;String, DiscountPolicy&gt; policyMap, List&lt;DiscountPolicy&gt; policies) {
        this.policyMap = policyMap;
        this.policies = policies;
    }

    public int discount(Member member, int price, String discountCode) {
        DiscountPolicy policy = policyMap.get(discountCode);
        // 클라이언트가 고른 빈의 discount 메서드 실행 
        return policy.discount(member, price);
    }
}</code></pre>
<ul>
<li><strong>Map</strong> : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 <code>DiscountPolicy</code> 타입으로 조회한 모든 스프링 빈을 담아준다.</li>
<li><strong>List</strong> : <code>DiscountPolicy</code> 타입으로 조회한 모든 스프링 빈을 담아준다. </li>
</ul>
<hr>
<h2 id="6-자동-수동의-올바른-실무-운영-기준">6. 자동, 수동의 올바른 실무 운영 기준</h2>
<p><strong>1. 편리한 자동 기능을 기본으로 사용하자</strong></p>
<ul>
<li>컴포넌트 스캔과 자동 주입을 사용하는 선호하는 추세이다.</li>
<li>자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다. </li>
</ul>
<p><strong>2. 그럼 언제 빈 등록을 사용할까 ❓</strong></p>
<ul>
<li>애플리케이션은 크게 아래와 같이 나눌 수 있다. <ul>
<li><strong>업무 로직 빈</strong> : 보통 비지니스 요구사항 개발할 때 추가되거나 변경되며 컨트롤러, 서비스, 레포지토리 등이 해당된다.</li>
<li><strong>기술 지원 빈</strong> : 데이터 베이스 연결이나 공통 로그 처리처럼 업무 로직을 지원하기 위한 공통 기술들이다. (ex : AOP 처리 시 사용)</li>
</ul>
</li>
<li>숫자도 많고 한번 개발하면 어느정도 유사한 패턴이 있는 <strong>업무 로직</strong>은 <strong>자동</strong> 기능을 활용하고, 수가 적고 광범위하게 영향을 미치며 문제 발생 시 찾기 어려운 <strong>기술 지원</strong>은 <strong>수동</strong>으로 등록하는 것이 좋다. </li>
</ul>
<p><strong>3. 비지니스 로직 중 다형성을 적극 활용할 때는 ❓</strong></p>
<ul>
<li>자동으로 등록된다면, 다른 개발자가 구현한<code>Map&lt;String, DiscountPolicy&gt;</code> 에 어떤 빈들이 주입될지 파악하기 어렵다.</li>
<li>이런 경우 수동 빈으로 등록하거나 자동으로하면 특정 패키지에 같이 묶어두는게 좋다! </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AICE 자격증 1,2,3급]]></title>
            <link>https://velog.io/@smj_716/AICE-%EC%9E%90%EA%B2%A9%EC%A6%9D-123%EA%B8%89</link>
            <guid>https://velog.io/@smj_716/AICE-%EC%9E%90%EA%B2%A9%EC%A6%9D-123%EA%B8%89</guid>
            <pubDate>Tue, 15 Apr 2025 05:33:48 GMT</pubDate>
            <description><![CDATA[<h3 id="강의-수료-후">강의 수료 후</h3>
<h3 id="📅-2월">📅 2월</h3>
<ul>
<li>대성 지역 아동 센터 수업 참관</li>
<li>코디니 활용 블록코딩 교육 시연 관찰</li>
<li>수업 진행 시 강사의 태도와 아이들과의 소통 방식 학습</li>
</ul>
<h3 id="📅-3월">📅 3월</h3>
<ul>
<li>AICE Future 정기시험(2025년 제2회) 응시</li>
<li>1급, 2급, 3급 자격증 모두 취득</li>
</ul>
<h3 id="🏅-자격증-인증샷">🏅 자격증 인증샷</h3>
<img src="https://velog.velcdn.com/images/smj_716/post/eb5c0e54-110e-4668-8f53-d330b93d85b8/image.png" width="300"/>
<img src="https://velog.velcdn.com/images/smj_716/post/f86d9675-145b-400d-88d8-afa82a0f3ae3/image.png" width="300"/>
<img src="https://velog.velcdn.com/images/smj_716/post/14d808db-2438-4d27-8a62-197937f8bbd1/image.png" width="300"/>

<p>🌟 더 많은 교육 활동에 참여하며 아이들이 IT를 어떻게 접하고 배우는지 알아가고 싶다. 나만의 방식으로 아이들에게 IT를 전하는 것이 나의 다음 목표다!!</p>
]]></description>
        </item>
    </channel>
</rss>