<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>funda__mental_.log</title>
        <link>https://velog.io/</link>
        <description>열혈개발자~!!</description>
        <lastBuildDate>Thu, 08 May 2025 04:23:14 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>funda__mental_.log</title>
            <url>https://velog.velcdn.com/images/funda__mental_/profile/67268fc7-6111-4595-bbcd-2210c1dc5844/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. funda__mental_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/funda__mental_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[팀네이버 1차 면접 회고]]></title>
            <link>https://velog.io/@funda__mental_/%ED%8C%80%EB%84%A4%EC%9D%B4%EB%B2%84-1%EC%B0%A8-%EB%A9%B4%EC%A0%91-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@funda__mental_/%ED%8C%80%EB%84%A4%EC%9D%B4%EB%B2%84-1%EC%B0%A8-%EB%A9%B4%EC%A0%91-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Thu, 08 May 2025 04:23:14 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>정보보호 서약에 따라 자세한 내용이나 문제가 될 수 있는 사항은 언급할 수 없는 점 양해 부탁드립니다.</p>
</blockquote>
<p>내 인생 처음으로 회사에 지원했고, 생각지도 못하게 서류와 코딩테스트를 통과했다. 최선을 다해 2주 동안 1차 면접을 준비했고, 그 과정에서 어떤 성장이 있었는지를 담아보려고 한다.
나는 <strong>팀네이버</strong>의 <strong>Tech-Backend Developer</strong> 신입 직군에 지원했다. 아직 6학기가 남아 있었지만, 특이하게 해당 공고는 졸업을 하지 않은 경우에도 일정 조율을 통해 졸업 후 업무를 시작할 수 있어 지원할 수 있었다.</p>
<h2 id="💻-시작은-서류">💻 시작은, 서류</h2>
<p>처음 지원서를 보고 가장 놀랐던 점은 내 생각보다 써야 할 글자 수가 적었다는 것이다. 다양한 개발 동아리 지원서를 작성하면서 꽤 고생했던 나는(글자 수 제한이 없어 최대한 많이 써야 했던 곳도 있었다) 이번엔 제한된 글자 수 안에서 나를 최대한 표현해야 한다는 점이 가장 어려웠다.
먼저 팀네이버에는 어떤 계열사가 있는지, 그들이 원하는 인재상은 무엇인지 조사했다. 인터넷에서 찾을 수 있는 모든 팀네이버 Tech 면접 후기를 찾아보고, 각 회사의 공식 홈페이지, 유튜브 영상, 기술 블로그 등을 살펴보았다. 팀네이버는 네이버, 네이버 클라우드, 스노우, 네이버 파이낸셜을 포함하는 지칭이다.
그중 가장 인상 깊었던 신입 공채 영상 속 말은 다음과 같았다.</p>
<blockquote>
<p>팀네이버는 기술에 대한 애정을 가지고 매 순간 도전하는 곳. 기술에 대한 열정, 호기심, 배움의 즐거움을 가진 사람과 함께 성장하고 싶다.</p>
</blockquote>
<p>이 메시지를 보여주기 위해 노력했다. 또한, 지원서 질문 전반에 ‘도전’이라는 단어가 반복적으로 등장해, 내가 개발을 공부하며 겪었던 도전적인 경험을 중심으로 글을 작성했다.
구체적으로는 [정답이 아니라 최적의 답을 찾는 개발자]처럼 각 질문마다 소제목을 달아 두괄식으로 작성했다. 면접 스터디에서 다른 지원자들의 지원서를 보니 대부분 이런 식으로 두괄식으로 작성한 것을 확인할 수 있었다. 한 질문 안에 여러 소제목을 넣은 분도 있었고, 나처럼 질문 하나에 하나의 소제목을 넣은 분도 있었다.</p>
<p>서류에서는 자기소개서뿐만 아니라 포트폴리오도 제출해야 했는데, 이때 제출했던 포트폴리오를 면접 준비하면서 많이 후회했다. 나는 무작정 내가 많이 했다는 걸 보여주는 게 중요하다고 생각해 모든 프로젝트와 이력들을 다 넣었다. 그중에는 개발을 처음 시작할 때 했던 프로젝트나 깃허브도 있었고, 개발과는 무관한 수상 내역도 있었다. 이 부분은 ‘면접’ 파트에서 더 자세히 이야기하겠지만, 많은 이력이 서류 합격에는 도움이 됐을지 몰라도 면접 준비 과정에서는 오히려 힘들었다. 다른 분들의 포트폴리오를 보면, 내가 프로젝트를 가장 많이 넣은 축에 속했다. 이때 배운 점은, 자신 있는 프로젝트 2~3개만 넣어도 충분하고, 각각의 프로젝트에 대해 <strong>내용 요약, 서버 아키텍처, 본인이 맡은 부분, 어려웠던 점</strong>을 깔끔하게 정리하는 것이 중요하다는 것이다.</p>
<p>이런 포트폴리오의 장점은 크게 두 가지다:</p>
<ol>
<li>내가 의도한 방향으로 질문을 유도할 수 있다.</li>
<li>면접관이 프로젝트를 쉽게 이해할 수 있고, 내가 자신 있는 부분만 강조할 수 있다.</li>
</ol>
<p>따라서 꼭 포트폴리오는 깔끔하게 정리하길 추천한다.
또한, 자기소개서의 모든 문장을 의미 없는 형용사로 채우지 말고, <strong>모든 문장에 질문 거리를 남긴다는 마음으로 의미를 담아 퇴고</strong>하는 것이 중요하다.</p>
<h2 id="🧩-그리고-코딩테스트">🧩 그리고, 코딩테스트</h2>
<p>팀네이버는 특이하게 서류와 코딩테스트 결과를 함께 평가해 1차 합격 여부를 결정한다. 즉, 모두가 코테를 봐야 했다.
나는 코딩테스트 준비를 거의 해본 적 없었고, 졸업을 위한 약간의 알고리즘 수업이 전부였다. 그래서 서류 제출과 동시에 급히 코테 공부를 시작했다. 프로그래머스를 통해 시작했고, 처음에는 level 1 문제들을 풀며 문법에 익숙해지려고 했다. 이후 ‘알고리즘 고득점 Kit’를 풀었는데 이게 꽤 도움이 됐다. 출제 빈도 순으로 ‘해시’, ‘완전탐색’, ‘힙’, ‘스택/큐’를 집중적으로 공부했다. 특히 빡구현 위주의 문제가 많다는 이야기를 듣고, 끝까지 혼자 구현하는 데 집중하며 연습했다.</p>
<p>코테는 총 3문제였고, <strong>한 문제만 풀어도 합격할 수 있다</strong>는 말이 있어서 목표는 1문제 해결이었다. 1번을 풀다가 어려워서 2번부터 풀었고, 생각보다 수월하게 풀려서 결국 1번도 풀고 3번도 살짝 손댈 수 있었다. 여기서 팁은, <strong>부분 점수가 있기 때문에 문제를 완전히 해결하지 못하더라도 해당 조건까지는 구현하는 것이 좋다</strong>는 점이다. 체감상 2.5문제 정도를 해결했다고 느꼈다.
하지만 코테 준비한 지 일주일도 안 된 내가 붙을 리 없다고 생각해, 이후 네이버에 지원한 사실조차 잊고 있었다.</p>
<p>이후 조직 적합도 검사도 있었는데, 특별한 준비 없이 MBTI 검사와 유사한 느낌이었다. 솔직하게 답하면 괜찮을 것 같다!</p>
<h2 id="🎤-내가-1차-면접을">🎤 내가 1차 면접을?!</h2>
<p>그러다 정말로 1차 면접에 붙었다는 소식을 받게 되었다.
<img src="https://velog.velcdn.com/images/funda__mental_/post/02afa921-a4f1-4203-bbe1-4e92bd7f6ff5/image.png" alt="네이버서류합격"></p>
<p>생각지도 못한 선물을 받은 기분이라 정말 행복했다. 하지만 면접은 중간고사 기간 한가운데였고, 첫 면접 준비라 정말 준비할 게 산더미였다. 오픈채팅방과 학교 커뮤니티에서 면접 스터디를 구해 함께 준비했다.</p>
<p>기술역량 면접이기 때문에 CS, 수리/논리 문제, 손코딩, 자기소개서 기반 질문, 1분 자기소개 등 다양한 질문이 나올 수 있다고 해서 전부를 준비했다.
가장 도움이 되었던 책은</p>
<ul>
<li>『개발자 기술 면접 노트』</li>
<li>『이것이 취업을 위한 컴퓨터 과학이다 with CS 기술 면접』
두 권이었다. 직접적으로 면접 문제와 일치하진 않았지만, <strong>전반적인 CS 이해도</strong>와 <strong>답변 고도화</strong>에 큰 도움이 되었다.</li>
</ul>
<p>스터디를 통해 내 자소서를 객관적으로 검토하고, 예상치 못한 질문에 대응하는 연습을 했다. 노션에 예상 질문을 토글로 정리하고 답변 → 꼬리질문 → 다시 답변을 반복하며 준비했다. 중간에 PDF로 뽑아봤는데 무려 244페이지였다...
<img src="https://velog.velcdn.com/images/funda__mental_/post/022e4013-87f7-4c29-90c9-18eac017949d/image.png" alt="면접 준비 노션"></p>
<p>여기서 아쉬운 점은 마지막에 더 압축해서 여러 번 회독했어야 했다는 것이다. 내용이 너무 많아 머릿속 정리가 잘 안 됐고, 중간고사 2개와 졸업 프로젝트 발표까지 겹쳐 면접 직전 일주일은 거의 준비를 못해 공부한 내용을 많이 잊었다. 이 부분이 가장 아쉽다.</p>
<p>면접 준비를 하면서 가장 심적으로 힘들었던 부분은, 내가 잘 준비하고 있는지 모르겠고 공부를 할수록 내가 너무 부족해보인다는 것이었다. 면접을 처음 준비해보기도 하고, 면접에서 구체적으로 어떤 게 출제되는지에 대한 내용이 너무나 광범위하고 모호해서 하면서도 내가 공부하는 방향성에 대한 확신이 들지 않았다. 또한, CS 공부를 해가면서 내 프로젝트와 포폴을 다시 보면 너무나 부족해보였다... 그래서 이 부분이 가장 힘들었다.  그러나 지금 생각해보면, 그런 깨달음이야말로 성장의 증거였던 것 같다. 완벽하다고 생각했던 자소서와 포트폴리오가 부족해 보인다는 건, 내가 성장했고 보는 눈이 높아졌다는 뜻이니까.</p>
<p>면접은 정말 예상하지 못한 방향으로 흘러갔다. 내가 예상하지도 못한 것들을 형식에, 내가 준비하지 않은 것들을 잔뜩 물어보셔서 특히 두번째 세션을 진행하면서 &#39;아 떨어지겠다&#39;라는 생각이 머리에 가득찼다. 면접은 자신감이 있어야 되는데, 이 자신감이 있으려면 내가 뱉은 말들, 내가 한 것들에 대해 확실히 알고 있어야 한다는 걸 뼈저리게 느꼈다. 면접관분들은 너무나 친절하시고, 마지막에 내 포트폴리오에 대한 피드백을 들을 수 있어서 정말 유익했던 시간이었다. </p>
<p><img src="https://velog.velcdn.com/images/funda__mental_/post/3c5e9989-02a1-4457-ba15-ba7f0ba9b8e5/image.png" alt="면접 후 노트">
<em>면접 직후 바로 작성한 노트.</em></p>
<p>아쉽게도 1차 면접은 떨어졌지만, 그 어떤 2주보다 많은 것을 배웠고 앞으로 어떤 방향으로 개발 공부를 해야 할지 알 수 있었다.
무엇보다 소중했던 건, <strong>내가 백엔드 개발자가 되고 싶다는 확신</strong>을 갖게 된 것이다.
사실 이전까지는 대학원을 갈지, 백엔드 개발자가 되고 싶은 게 맞는지 고민이 많았다. 지원서 작성할 때까지만 해도 그랬다. 그런데 이번 면접 준비를 통해 알게 된 건, <strong>내가 개발을 꽤 좋아한다는 사실</strong>이었다.
공부 방법을 몰라 헤맸던 것이다. 리팩토링을 해서 성능 개선을 하면 재밌고, 내가 만든 서비스가 실제로 쓰일 걸 생각하면 설렌다.</p>
<p>앞으로는 기존 프로젝트를 리팩토링하고, 오픈소스에도 기여해보며 다음 면접을 준비하고 싶다.
그럼 팀네이버 지원 회고 끝!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ch10. 스프링 부트를 이용한 로그인 및 회원가입 구현]]></title>
            <link>https://velog.io/@funda__mental_/Ch10.-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%B0%8F-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@funda__mental_/Ch10.-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%B0%8F-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sun, 29 Dec 2024 07:51:04 GMT</pubDate>
            <description><![CDATA[<h2 id="📝-학습-목표">📝 학습 목표</h2>
<ol>
<li>스프링 시큐리티를 활용한 간단한 로그인 및 회원가입 기능 구현하기</li>
<li>카카오톡 소셜 로그인 구현하기</li>
</ol>
<hr>
<h2 id="📑-10주차-주제">📑 10주차 주제:</h2>
<p>이번 주차에서는 <strong>스프링 부트를 이용하여 로그인 및 회원가입 기능</strong>을 직접 구현합니다. 또한 카카오톡 소셜 로그인을 추가하여 OAuth2를 활용한 인증 기능도 학습합니다.</p>
<p>스프링 시큐리티(Spring Security)는 인증(Authentication)과 인가(Authorization)을 효과적으로 처리할 수 있는 프레임워크로, 간단한 설정으로 보안 기능을 제공하며 복잡한 보안 로직도 손쉽게 확장할 수 있습니다.</p>
<hr>
<h2 id="0-들어가기-전">0. 들어가기 전</h2>
<h3 id="spring-security-의존성-추가"><strong>Spring Security 의존성 추가</strong></h3>
<p>로그인 및 회원가입 기능 구현을 위해 기본적으로 Spring Security를 설정해야 합니다. 아래와 같이 <code>build.gradle</code>에 의존성을 추가합니다:</p>
<pre><code class="language-groovy">dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-security&#39;
    testImplementation &#39;org.springframework.security:spring-security-test&#39;
}</code></pre>
<p>의존성을 추가하면 Spring Security는 기본적으로 <strong>자동 설정(Auto-Configuration)</strong>을 통해 프로젝트에 기본 보안 기능을 적용합니다.</p>
<p>애플리케이션 실행 시 기본 계정과 비밀번호가 자동으로 생성되며, 이를 이용해 제공된 계정으로 로그인할 수 있습니다. 하지만 실제 애플리케이션에서는 사용자 정의 계정과 권한 설정이 필요합니다.</p>
<hr>
<h2 id="1-spring-security-개요-및-주요-흐름">1. Spring Security 개요 및 주요 흐름</h2>
<h3 id="1️⃣-spring-security란"><strong>1️⃣ Spring Security란?</strong></h3>
<p>Spring Security는 Spring 기반 애플리케이션에서 보안을 담당하는 프레임워크로, 다음과 같은 주요 기능을 제공합니다:</p>
<ol>
<li><strong>인증(Authentication)</strong>: 사용자의 신원을 확인하는 과정</li>
<li><strong>인가(Authorization)</strong>: 사용자의 리소스 접근 권한을 검증하는 과정</li>
<li><strong>보안 위협 방어</strong>: CSRF, XSS, 세션 고정 공격 등 다양한 보안 위협에 대한 방어</li>
</ol>
<hr>
<h3 id="2️⃣-spring-security의-주요-컴포넌트"><strong>2️⃣ Spring Security의 주요 컴포넌트</strong></h3>
<h4 id="authenticationmanager"><strong>AuthenticationManager</strong></h4>
<ul>
<li>사용자의 자격 증명을 관리하며, 인증 로직을 처리하는 중심 컴포넌트입니다.</li>
</ul>
<h4 id="authenticationprovider"><strong>AuthenticationProvider</strong></h4>
<ul>
<li>실제 인증 로직을 수행하며, 다양한 인증 방식을 지원합니다.</li>
</ul>
<h4 id="userdetailsservice"><strong>UserDetailsService</strong></h4>
<ul>
<li>사용자 정보를 데이터베이스나 저장소에서 불러와 인증 과정을 지원합니다.</li>
</ul>
<h4 id="securitycontext"><strong>SecurityContext</strong></h4>
<ul>
<li>인증된 사용자 정보를 저장하며, 애플리케이션 전반에서 공유됩니다.</li>
</ul>
<hr>
<h3 id="3️⃣-spring-security의-인증과-인가-흐름"><strong>3️⃣ Spring Security의 인증과 인가 흐름</strong></h3>
<h4 id="인증authentication-흐름"><strong>[인증(Authentication) 흐름]</strong></h4>
<p>인증은 사용자의 신원을 확인하는 과정입니다. Spring Security에서의 인증 흐름은 다음과 같습니다:</p>
<ol>
<li><p><strong>사용자 로그인 요청</strong></p>
<ul>
<li>사용자가 로그인 폼에 credentials(예: 이메일, 비밀번호)을 입력하고 제출합니다.</li>
</ul>
</li>
<li><p><strong>AuthenticationFilter</strong></p>
<ul>
<li><code>UsernamePasswordAuthenticationFilter</code>가 요청을 가로채고 <code>Authentication</code> 객체를 생성합니다.</li>
</ul>
</li>
<li><p><strong>AuthenticationManager</strong></p>
<ul>
<li><code>AuthenticationManager</code>는 적절한 <code>AuthenticationProvider</code>를 선택하여 인증을 위임합니다.</li>
</ul>
</li>
<li><p><strong>AuthenticationProvider</strong></p>
<ul>
<li>선택된 <code>AuthenticationProvider</code>는 <code>UserDetailsService</code>를 사용하여 사용자 정보를 로드하고 비밀번호를 검증합니다.</li>
</ul>
</li>
<li><p><strong>UserDetailsService</strong></p>
<ul>
<li>데이터베이스나 저장소에서 사용자 정보를 조회합니다.</li>
</ul>
</li>
<li><p><strong>SecurityContext</strong></p>
<ul>
<li>인증이 성공하면, <code>Authentication</code> 객체가 <code>SecurityContext</code>에 저장됩니다.</li>
</ul>
</li>
</ol>
<hr>
<h4 id="인가authorization-흐름"><strong>[인가(Authorization) 흐름]</strong></h4>
<p>인가는 인증된 사용자가 특정 리소스에 접근할 권한이 있는지 확인하는 과정입니다:</p>
<ol>
<li><p><strong>리소스 접근 요청</strong></p>
<ul>
<li>인증된 사용자가 보호된 리소스에 접근을 시도합니다.</li>
</ul>
</li>
<li><p><strong>FilterSecurityInterceptor</strong></p>
<ul>
<li><code>FilterSecurityInterceptor</code>가 요청을 가로채고 권한 검사를 시작합니다.</li>
</ul>
</li>
<li><p><strong>AccessDecisionManager</strong></p>
<ul>
<li><code>AccessDecisionManager</code>는 현재 사용자의 권한과 요청된 리소스의 필요 권한을 비교합니다.</li>
</ul>
</li>
<li><p><strong>권한 확인</strong></p>
<ul>
<li><code>SecurityContext</code>에서 현재 인증된 사용자의 권한 정보를 조회합니다.</li>
</ul>
</li>
<li><p><strong>접근 결정</strong></p>
<ul>
<li>사용자의 권한이 충분하면 리소스 접근이 허용됩니다.</li>
<li>권한이 부족하면 <code>AccessDeniedException</code>이 발생하고 접근이 거부됩니다.</li>
</ul>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/funda__mental_/post/37485941-2846-4898-a2eb-53b99e663e92/image.png" alt=""></p>
<hr>
<h2 id="2-로그인-및-회원가입-구현">2. 로그인 및 회원가입 구현</h2>
<h3 id="0️⃣-들어가기-전"><strong>0️⃣ 들어가기 전</strong></h3>
<p>이번 실습은 <strong>9주차까지 진행한 프로젝트</strong>를 그대로 이어서 진행합니다. 또한 <strong>Thymeleaf</strong>라는 템플릿 엔진을 이용해 결과를 화면에 출력할 예정입니다.</p>
<h4 id="thymeleaf란"><strong>Thymeleaf란?</strong></h4>
<blockquote>
<p><strong>Thymeleaf</strong>는 Spring Boot에서 HTML을 렌더링하기 위한 템플릿 엔진으로, 백엔드 개발자가 뷰(View)를 구성하는 데 유용한 도구입니다.</p>
</blockquote>
<h4 id="실제-협업-시-사용-방식"><strong>실제 협업 시 사용 방식</strong></h4>
<p>실제 프로젝트에서는 프론트엔드 개발자와 <strong>REST API</strong>를 통해 데이터를 주고받는 방식으로 구현합니다. 그러나 이번 실습에서는 백엔드 로직에 집중하기 위해 <strong>Thymeleaf로 간단한 HTML 페이지</strong>를 만들어 결과를 확인하는 방식을 사용합니다.</p>
<hr>
<h3 id="1️⃣-thymeleaf-의존성-추가하기"><strong>1️⃣ Thymeleaf 의존성 추가하기</strong></h3>
<p><code>build.gradle</code> 파일에 다음 내용을 추가하여 <strong>Thymeleaf</strong> 의존성을 추가합니다:</p>
<pre><code class="language-groovy">implementation &#39;org.springframework.boot:spring-boot-starter-thymeleaf&#39;</code></pre>
<hr>
<h3 id="2️⃣-html-파일-추가하기"><strong>2️⃣ HTML 파일 추가하기</strong></h3>
<p>실습에 필요한 HTML 파일을 추가합니다. <code>src/main/resources/templates</code> 폴더를 생성하고 필요한 HTML 파일을 추가해주세요.
=&gt; <code>admin.html</code>, <code>home.html</code>, <code>login.html</code>, <code>signup.html</code></p>
<hr>
<h3 id="3️⃣-view-controller-추가하기"><strong>3️⃣ View Controller 추가하기</strong></h3>
<p><code>src/main/java/web/controller</code> 패키지에 <code>MemberViewController</code> 클래스를 생성하고 다음 코드를 작성합니다:</p>
<pre><code class="language-java">@Controller
public class MemberViewController {

    @GetMapping(&quot;/login&quot;)
    public String loginPage() {
        return &quot;login&quot;;
    }

    @GetMapping(&quot;/signup&quot;)
    public String signupPage(Model model) {
        model.addAttribute(&quot;memberJoinDto&quot;, new MemberRequestDTO.JoinDto());
        return &quot;signup&quot;;
    }

    @GetMapping(&quot;/home&quot;)
    public String home() {
        return &quot;home&quot;;
    }

    @GetMapping(&quot;/admin&quot;)
    public String admin() {
        return &quot;admin&quot;;
    }
}</code></pre>
<h4 id="controller-역할"><strong>Controller 역할</strong></h4>
<ol>
<li><code>/login</code> → <code>login.html</code> 반환</li>
<li><code>/signup</code> → <code>signup.html</code> 반환, 회원가입 폼에 필요한 빈 DTO 객체 추가</li>
<li><code>/home</code> → <code>home.html</code> 반환</li>
<li><code>/admin</code> → <code>admin.html</code> 반환</li>
</ol>
<hr>
<h2 id="1-spring-security-설정-및-구현">1. Spring Security 설정 및 구현</h2>
<h3 id="1️⃣-security-의존성-추가하기"><strong>1️⃣ Security 의존성 추가하기</strong></h3>
<p><code>build.gradle</code> 파일에 다음 의존성을 추가하여 Spring Security를 활성화합니다:</p>
<pre><code class="language-groovy">implementation &#39;org.springframework.boot:spring-boot-starter-security&#39;
testImplementation &#39;org.springframework.security:spring-security-test&#39;
implementation &#39;org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE&#39;</code></pre>
<hr>
<h3 id="2️⃣-security-설정하기"><strong>2️⃣ Security 설정하기</strong></h3>
<p><code>src/main/java/umc/study/config/security</code> 디렉토리에 <code>SecurityConfig</code> 클래스를 생성하고 아래와 같이 작성합니다:</p>
<pre><code class="language-java">@EnableWebSecurity
@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((requests) -&gt; requests
                .requestMatchers(&quot;/&quot;, &quot;/home&quot;, &quot;/signup&quot;, &quot;/members/signup&quot;, &quot;/css/**&quot;).permitAll()
                .requestMatchers(&quot;/admin/**&quot;).hasRole(&quot;ADMIN&quot;)
                .anyRequest().authenticated()
            )
            .formLogin((form) -&gt; form
                .loginPage(&quot;/login&quot;)
                .defaultSuccessUrl(&quot;/home&quot;, true)
                .permitAll()
            )
            .logout((logout) -&gt; logout
                .logoutUrl(&quot;/logout&quot;)
                .logoutSuccessUrl(&quot;/login?logout&quot;)
                .permitAll()
            );

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}</code></pre>
<hr>
<h3 id="3️⃣-회원가입에-보안-추가하기"><strong>3️⃣ 회원가입에 보안 추가하기</strong></h3>
<h4 id="role-열거형-추가"><strong>Role 열거형 추가</strong></h4>
<p><code>Role</code> 열거형을 생성하여 사용자 역할을 정의합니다:</p>
<pre><code class="language-java">public enum Role {
    ADMIN, USER
}</code></pre>
<h4 id="member-엔티티-수정"><strong>Member 엔티티 수정</strong></h4>
<p><code>Member</code> 엔티티에 보안 관련 필드와 비밀번호 설정 메서드를 추가합니다:</p>
<pre><code class="language-java">@Entity
public class Member extends BaseEntity {
    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String password;

    @Enumerated(EnumType.STRING)
    private Role role;

    public void encodePassword(String password) {
        this.password = password;
    }
}</code></pre>
<hr>
<h4 id="dto와-converter-수정"><strong>DTO와 Converter 수정</strong></h4>
<ol>
<li><p><strong><code>MemberRequestDTO.JoinDto</code> 수정</strong>:</p>
<pre><code class="language-java">public class MemberRequestDTO {
    public static class JoinDto {
        @NotBlank
        private String name;
        @NotBlank
        @Email
        private String email;
        @NotBlank
        private String password;
        @NotNull
        private Role role;
    }
}</code></pre>
</li>
<li><p><strong><code>MemberConverter</code> 수정</strong>:</p>
<pre><code class="language-java">public class MemberConverter {
    public static Member toMember(MemberRequestDTO.JoinDto request) {
        return Member.builder()
            .name(request.getName())
            .email(request.getEmail())
            .password(request.getPassword())
            .role(request.getRole())
            .build();
    }
}</code></pre>
</li>
</ol>
<hr>
<h4 id="command-service-수정"><strong>Command Service 수정</strong></h4>
<p><code>MemberCommandServiceImpl</code>에 비밀번호 암호화 로직 추가:</p>
<pre><code class="language-java">@Service
public class MemberCommandServiceImpl implements MemberCommandService {
    private final PasswordEncoder passwordEncoder;

    @Override
    @Transactional
    public Member joinMember(MemberRequestDTO.JoinDto request) {
        Member newMember = MemberConverter.toMember(request);
        newMember.encodePassword(passwordEncoder.encode(request.getPassword()));
        return memberRepository.save(newMember);
    }
}</code></pre>
<hr>
<h3 id="-2회원가입-구현">** 2.회원가입 구현**</h3>
<h3 id="로그인-구현">로그인 구현</h3>
<p>이제 회원가입이 정상적으로 동작하므로, <strong>로그인 기능</strong>을 구현해보겠습니다. Spring Security는 기본적으로 로그인 로직을 처리해주지만, 이를 위해 우리는 사용자 정보를 처리하는 <code>CustomUserDetailsService</code>를 만들어야 합니다.</p>
<p>특정 이메일을 사용하는 유저가 DB에 존재하는지 확인하기 위한 함수를 추가해줍니다.</p>
<p><code>MemberRepository</code> 에 다음 함수를 추가합니다,</p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {

    // ... 기존 함수 ...

    Optional&lt;Member&gt; findByEmail(String email);
}</code></pre>
<p><strong><code>src/main/java/umc/study/config/security</code></strong> 위치에 <strong><code>CustomUserDetailsService</code></strong> 클래스를 생성한 후, 아래의 코드를 작성해주시면 됩니다!!</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Member member = memberRepository.findByEmail(username)
            .orElseThrow(() -&gt; new UsernameNotFoundException(&quot;해당 이메일을 가진 유저가 존재하지 않습니다: &quot; + username));

        return org.springframework.security.core.userdetails.User
            .withUsername(member.getEmail())
            .password(member.getPassword())
            .roles(member.getRole().name())
            .build();
    }
}</code></pre>
<p><strong>동작 과정:</strong></p>
<ol>
<li>사용자가 로그인 폼에서 이메일과 비밀번호를 입력하고 제출합니다.</li>
<li>Spring Security가 <code>CustomUserDetailsService</code>의 <code>loadUserByUsername</code> 메소드를 호출합니다.</li>
<li>이 메소드는 입력받은 이메일(username)으로 데이터베이스에서 사용자를 조회합니다.</li>
<li>사용자가 존재하면 Spring Security의 <code>User</code> 객체로 변환하여 반환합니다.</li>
<li>Spring Security는 반환된 <code>User</code> 객체의 정보를 사용하여 인증을 수행합니다.
<img src="https://velog.velcdn.com/images/funda__mental_/post/480564d1-9d00-4c9b-a9e9-db43bb2ea587/image.png" alt=""></li>
</ol>
<blockquote>
<p><strong>🔐 보안 강화 팁 🔐</strong>
실제 애플리케이션에서는 로그인 시도 횟수 제한, 2단계 인증 등의 추가적인 보안 기능을 구현하는 것이 좋습니다.</p>
</blockquote>
<h4 id="회원가입-로직"><strong>회원가입 로직</strong></h4>
<p><code>MemberCommandService</code>에서 비밀번호를 암호화하여 저장합니다:</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class MemberCommandService {
    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    public void joinMember(MemberRequestDto request) {
        Member member = new Member();
        member.setEmail(request.getEmail());
        member.encodePassword(passwordEncoder.encode(request.getPassword()));
        member.setRole(Role.USER);

        memberRepository.save(member);
    }
}</code></pre>
<hr>
<h2 id="3oauth2를-활용한-카카오-소셜-로그인-구현">3.OAuth2를 활용한 카카오 소셜 로그인 구현</h2>
<h3 id="oauth2란-무엇인가요"><strong>OAuth2란 무엇인가요?</strong></h3>
<p>OAuth2는 제3자 애플리케이션이 사용자 대신 안전하게 리소스에 접근할 수 있도록 허용하는 <strong>인증 프로토콜</strong>입니다. 이를 통해 사용자는 비밀번호를 공유하지 않고도 애플리케이션에 접근 권한을 부여할 수 있습니다.</p>
<p>이 프로토콜의 주요 장점은 다음과 같습니다!!!</p>
<ol>
<li>사용자는 비밀번호를 직접 입력하지 않아도 됩니다.</li>
<li>앱은 사용자의 소셜 계정 정보를 안전하게 받아올 수 있습니다.</li>
<li>사용자는 언제든 앱의 접근 권한을 취소할 수 있습니다.</li>
</ol>
<p>따라서 소셜 로그인을 구현할 때, OAuth2는 주로 <strong>사용자의 소셜 계정 정보</strong>를 애플리케이션이 안전하게 받아올 수 있게 해줍니다. 카카오톡, 구글, 페이스북 등의 서비스는 이 OAuth2 프로토콜을 통해 <strong>로그인</strong> 및 <strong>권한 관리</strong>를 처리합니다.</p>
<h3 id="1️⃣-oauth2-설정"><strong>1️⃣ OAuth2 설정</strong></h3>
<p><code>build.gradle</code>에 OAuth2 클라이언트 의존성을 추가합니다:</p>
<pre><code class="language-groovy">dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-oauth2-client&#39;
}</code></pre>
<h3 id="2️⃣-카카오톡-개발자-계정-생성-및-애플리케이션-등록">2️⃣ 카카오톡 개발자 계정 생성 및 애플리케이션 등록</h3>
<p>카카오 로그인을 구현하려면 먼저 카카오 개발자 계정을 만들고 애플리케이션을 등록해야 합니다. </p>
<p>따라서 다음 단계를 따라해 주세요</p>
<ol>
<li><a href="https://developers.kakao.com/">카카오 디벨로퍼스</a>에 접속해 <strong>카카오 계정</strong>으로 로그인합니다.</li>
<li>상단의 [내 애플리케이션] 탭에서 [애플리케이션 추가하기]를 클릭합니다.</li>
<li>앱 이름을 입력하고 사업자 정보를 기입합니다. (개인 프로젝트라면 개인으로 설정하세요)    </li>
<li>애플리케이션을 생성한 후, [플랫폼] 설정으로 이동합니다.</li>
<li>[Web 플랫폼 등록] 섹션에서 우리 앱의 도메인을 등록합니다. <ul>
<li>이번 실습에서는 로컬에서 테스트하기 때문에 <code>http://localhost:8080</code>을 입력하세요.</li>
</ul>
</li>
<li>[카카오 로그인] 설정으로 이동해 카카오 로그인을 활성화하고, Redirect URI를 등록합니다.<ul>
<li>Redirect URI는 <code>http://localhost:8080/login/oauth2/code/kakao</code>로 설정하세요.</li>
</ul>
</li>
</ol>
<pre><code>&gt;**Redirect URI란?**
OAuth2 인증이 성공한 후 사용자가 돌아올 주소입니다. 로컬 테스트 시에는 localhost를 사용하지만, 실제 서비스 배포 시에는 여러분의 서비스 도메인으로 변경해야 합니다!</code></pre><ol start="7">
<li>[동의항목] 설정에서 필요한 정보 항목을 선택합니다. 이번 실습에서는 <strong>닉네임을</strong> 필수로 설정해주세요.</li>
</ol>
<p><code>application.yml</code> 파일에 카카오 API 정보를 추가합니다:</p>
<pre><code class="language-yaml">spring:
  security:
    oauth2:
      client:
        registration:
          kakao:
            client-id: {YOUR_CLIENT_ID}
            client-secret: {YOUR_CLIENT_SECRET}
            redirect-uri: http://localhost:8080/login/oauth2/code/kakao
            authorization-grant-type: authorization_code
            scope: profile_nickname</code></pre>
<h3 id="2️⃣-customoauth2userservice-구현"><strong>2️⃣ CustomOAuth2UserService 구현</strong></h3>
<p>카카오 로그인 후 사용자 정보를 처리하는 <code>CustomOAuth2UserService</code>를 작성합니다:</p>
<pre><code class="language-java">@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);

        String email = (String) oAuth2User.getAttributes().get(&quot;email&quot;);
        String nickname = (String) oAuth2User.getAttributes().get(&quot;nickname&quot;);

        // 사용자 정보 저장 로직 추가

        return oAuth2User;
    }
}</code></pre>
<h2 id="2-사용자-정보-저장을-위한-customoauth2userservice-구현"><strong>2. 사용자 정보 저장을 위한 CustomOAuth2UserService 구현</strong></h2>
<p>이제 카카오 로그인 후 받은 사용자 정보를 우리 데이터베이스에 저장하는 <code>CustomOAuth2UserService</code>를 만들어볼게요.</p>
<p><code>src/main/java/umc/study/config/security</code> 폴더에 <code>CustomOAuth2UserService</code> 클래스를 새로 만들고 다음 코드를 작성해주세요:</p>
<pre><code class="language-groovy">@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);

        Map&lt;String, Object&gt; attributes = oAuth2User.getAttributes();
        Map&lt;String, Object&gt; properties = (Map&lt;String, Object&gt;) attributes.get(&quot;properties&quot;);

        String nickname = (String) properties.get(&quot;nickname&quot;);
        String email = nickname + &quot;@kakao.com&quot;; // 임시 이메일 생성

        // 사용자 정보 저장 또는 업데이트
        Member member = saveOrUpdateUser(email, nickname);

            // 이메일을 Principal로 사용하기 위해 attributes 수정
        Map&lt;String, Object&gt; modifiedAttributes = new HashMap&lt;&gt;(attributes);
        modifiedAttributes.put(&quot;email&quot;, email);

        return new DefaultOAuth2User(
            oAuth2User.getAuthorities(),
            modifiedAttributes,
            &quot;email&quot;  // email Principal로 설정
        );
    }

    private Member saveOrUpdateUser(String email, String nickname) {
        Member member = memberRepository.findByEmail(email)
            .orElse(Member.builder()
                .email(email)
                .name(nickname)
                .password(passwordEncoder.encode(&quot;OAUTH_USER_&quot; + UUID.randomUUID()))
                .gender(Gender.NONE)  // 기본값 설정
                .address(&quot;소셜로그인&quot;)  // 기본값 설정
                .specAddress(&quot;소셜로그인&quot;)  // 기본값 설정
                .role(Role.USER)
                .build());

        return memberRepository.save(member);
    }
}</code></pre>
<p><code>CustomOAuth2UserService</code> 클래스는 카카오 로그인 후 받은 사용자 정보를 처리하고 데이터베이스에 저장하는 역할을 합니다. 주요 기능을 다음과 같이 설명할 수 있습니다:</p>
<ol>
<li><strong>OAuth2User 정보 로드</strong><ul>
<li><code>super.loadUser(userRequest)</code>를 통해 카카오에서 제공하는 사용자 정보를 OAuth2User 객체로 받아옵니다.</li>
<li>이 객체의 attributes에는 사용자의 닉네임 등 기본 정보가 포함되어 있습니다.</li>
</ul>
</li>
<li><strong>사용자 정보 추출</strong><ul>
<li>카카오 API에서 제공하는 사용자 정보 중 nickname을 추출합니다.</li>
<li>이메일 정보를 받기 위한 따로 설정하지 않았기 때문에, 닉네임을 이용해 임시 이메일 주소(예: <a href="mailto:nickname@kakao.com">nickname@kakao.com</a>)를 생성합니다.</li>
</ul>
</li>
<li><strong>사용자 정보 저장 및 업데이트</strong><ul>
<li><code>saveOrUpdateUser</code> 메소드를 통해 사용자 정보를 데이터베이스에 저장하거나 업데이트합니다.</li>
<li>임시 이메일을 기준으로 기존 사용자를 찾거나 새 사용자를 생성합니다.</li>
<li>카카오에서 제공하지 않는 정보(성별, 주소 등)는 기본값으로 설정합니다.</li>
</ul>
</li>
<li><strong>Spring Security용 OAuth2User 반환</strong><ul>
<li><code>DefaultOAuth2User</code> 객체를 생성하여 반환합니다.</li>
<li>사용자의 권한, 속성, 그리고 주요 식별자(email)를 설정합니다.</li>
<li>이를 통해 Spring Security가 인증된 사용자를 올바르게 식별하고 처리할 수 있습니다.</li>
</ul>
</li>
</ol>
<blockquote>
<p><strong>주의사항</strong></p>
</blockquote>
<ul>
<li>실제 서비스에서는 닉네임 대신 고유한 식별자(예: 이메일, 카카오 고유 ID)를 사용하는 것이 더 안정적입니다.</li>
<li>소셜 로그인 후 추가 정보 입력 페이지를 통해 필요한 사용자 정보를 수집하는 것이 좋습니다.</li>
</ul>
<hr>
<h2 id="3-spring-security에-oauth2-설정-추가"><strong>3. Spring Security에 OAuth2 설정 추가</strong></h2>
<p>이제 <code>SecurityConfig</code>에 OAuth2 관련 설정을 추가해볼게요. </p>
<p><code>SecurityConfig</code> 클래스를 열고 다음과 같이 수정해주세요:</p>
<pre><code class="language-groovy">@EnableWebSecurity
@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                        // ... 기존 설정 ...
            .oauth2Login(oauth2 -&gt; oauth2
                .loginPage(&quot;/login&quot;)
                .defaultSuccessUrl(&quot;/home&quot;, true)
                .permitAll()
            );

        return http.build();
    }

    // 기존 빈 설정...
}</code></pre>
<p>다음으로, 로그인 페이지에서 카카오 소셜 로그인 버튼을 추가하겠습니다.</p>
<p><code>src/main/resources/templates</code> 폴더에 잇는 기존 <code>login.html</code> 파일을 다음과 같이 변경해주세요.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;head&gt;
    &lt;title&gt;Login&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;!-- 기존 코드 --&gt;

&lt;a th:href=&quot;@{/oauth2/authorization/kakao}&quot;&gt;카카오로 로그인&lt;/a&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>
<hr>
<h3 id="jwt란"><strong>JWT란?</strong></h3>
<p>JWT(Json Web Token)는 <strong>JSON 포맷</strong>을 사용하여 정보를 안전하게 주고받기 위한 <strong>토큰 기반 인증 방식</strong>입니다. JWT는 세션을 유지할 필요 없이 인증과 권한 부여를 수행할 수 있는 <strong>세션리스(stateless)</strong> 인증 메커니즘으로, 클라이언트와 서버 간의 <strong>데이터 무결성</strong>을 보장합니다.</p>
<hr>
<h3 id="jwt의-구조"><strong>JWT의 구조</strong></h3>
<p>JWT는 세 부분으로 구성된 문자열이며, 각각 <code>.</code>으로 구분됩니다.</p>
<ol>
<li><strong>Header</strong>: 토큰 타입과 해싱 알고리즘 정보를 포함합니다.</li>
<li><strong>Payload</strong>: 실제 데이터(예: 사용자 ID, 권한 등)를 포함합니다.</li>
<li><strong>Signature</strong>: 토큰의 무결성을 검증하기 위한 서명으로, 비밀 키를 사용해 생성됩니다.</li>
</ol>
<blockquote>
<p>예시:<br><code>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMSIsInJvbGUiOiJVU0VSIiwiaWF0IjoxNjk2MTM2MDAwfQ.X4Jz5P8EExkEMHiNn9TlF6Wx51QBOUM7LzCW0mEDlqw</code></p>
</blockquote>
<hr>
<h3 id="jwt가-왜-필요한가"><strong>JWT가 왜 필요한가?</strong></h3>
<ol>
<li><p><strong>세션리스 인증</strong></p>
<ul>
<li>JWT는 서버에 상태 정보를 저장하지 않아도 됩니다. 서버 확장성이 좋아지고, 분산 시스템에서 유용합니다.</li>
</ul>
</li>
<li><p><strong>확장성</strong></p>
<ul>
<li>서버 간의 상태 동기화가 필요 없으므로, 마이크로서비스 아키텍처에서 유리합니다.</li>
</ul>
</li>
<li><p><strong>유연한 데이터 포함</strong></p>
<ul>
<li>토큰에 사용자 정보, 권한, 만료 시간 등의 정보를 포함할 수 있어 추가 요청 없이 인증 및 권한 부여를 처리할 수 있습니다.</li>
</ul>
</li>
<li><p><strong>보안</strong></p>
<ul>
<li>서버는 JWT의 서명을 통해 토큰이 변조되지 않았음을 확인할 수 있습니다.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="jwt의-동작-흐름"><strong>JWT의 동작 흐름</strong></h3>
<ol>
<li><strong>클라이언트 인증 요청</strong>:<ul>
<li>클라이언트가 서버에 사용자 자격 증명(예: ID, 비밀번호)을 보냅니다.</li>
</ul>
</li>
<li><strong>서버에서 토큰 발급</strong>:<ul>
<li>서버가 사용자 인증에 성공하면, JWT를 생성하여 클라이언트에 반환합니다.</li>
</ul>
</li>
<li><strong>클라이언트가 JWT 포함 요청</strong>:<ul>
<li>클라이언트는 이후 요청의 <code>Authorization</code> 헤더에 JWT를 포함하여 서버로 보냅니다.</li>
</ul>
</li>
<li><strong>서버에서 토큰 검증</strong>:<ul>
<li>서버는 토큰의 서명을 검증하고, 토큰에 포함된 정보를 기반으로 요청을 처리합니다.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="jwt-간단-구현-예제"><strong>JWT 간단 구현 예제</strong></h3>
<h4 id="1-의존성-추가">1. <strong>의존성 추가</strong></h4>
<p><code>build.gradle</code>에 JWT 관련 의존성을 추가합니다.</p>
<pre><code class="language-groovy">implementation &#39;io.jsonwebtoken:jjwt-api:0.11.5&#39;
runtimeOnly &#39;io.jsonwebtoken:jjwt-impl:0.11.5&#39;
runtimeOnly &#39;io.jsonwebtoken:jjwt-jackson:0.11.5&#39;</code></pre>
<h4 id="2-jwt-생성-및-검증-유틸리티-작성">2. <strong>JWT 생성 및 검증 유틸리티 작성</strong></h4>
<pre><code class="language-java">import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;

@Component
public class JwtUtil {

    private final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    private final long validity = 3600000; // 1시간

    // JWT 생성
    public String createToken(String username, String role) {
        return Jwts.builder()
                .setSubject(username)
                .claim(&quot;role&quot;, role)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + validity))
                .signWith(key)
                .compact();
    }

    // JWT 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

    // JWT에서 사용자 이름 추출
    public String getUsername(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }
}</code></pre>
<h4 id="3-로그인-api에서-jwt-발급">3. <strong>로그인 API에서 JWT 발급</strong></h4>
<p>사용자 인증 후 JWT를 발급합니다.</p>
<pre><code class="language-java">import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(&quot;/auth&quot;)
public class AuthController {

    private final JwtUtil jwtUtil;

    public AuthController(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @PostMapping(&quot;/login&quot;)
    public String login(@RequestBody LoginRequest request) {
        // 실제 인증 로직은 생략
        return jwtUtil.createToken(request.getUsername(), &quot;ROLE_USER&quot;);
    }
}

class LoginRequest {
    private String username;
    private String password;

    // Getters and Setters
}</code></pre>
<h4 id="4-jwt-인증-필터-추가">4. <strong>JWT 인증 필터 추가</strong></h4>
<p>Spring Security에서 JWT를 검증하도록 필터를 설정합니다.</p>
<pre><code class="language-java">import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;

public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final JwtUtil jwtUtil;

    public JwtAuthenticationFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String token = request.getHeader(&quot;Authorization&quot;);
        if (token != null &amp;&amp; jwtUtil.validateToken(token)) {
            String username = jwtUtil.getUsername(token);
            SecurityContextHolder.getContext().setAuthentication(new JwtAuthentication(username));
        }
        chain.doFilter(request, response);
    }
}</code></pre>
<h4 id="5-securityconfig에-필터-등록">5. <strong>SecurityConfig에 필터 등록</strong></h4>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtUtil jwtUtil;

    public SecurityConfig(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .addFilterBefore(new JwtAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class)
            .authorizeHttpRequests()
            .requestMatchers(&quot;/auth/login&quot;).permitAll()
            .anyRequest().authenticated();
        return http.build();
    }
}</code></pre>
<hr>
<h3 id="jwt-사용-시-주의점"><strong>JWT 사용 시 주의점</strong></h3>
<ol>
<li><p><strong>비밀 키 관리</strong></p>
<ul>
<li>비밀 키는 안전하게 보호해야 하며, 외부에 노출되지 않도록 주의합니다.</li>
</ul>
</li>
<li><p><strong>토큰 만료 시간</strong></p>
<ul>
<li>Access Token의 만료 시간을 짧게 설정하고, Refresh Token을 추가로 구현해 보안을 강화합니다.</li>
</ul>
</li>
<li><p><strong>HTTPS 사용</strong></p>
<ul>
<li>토큰이 네트워크를 통해 전달되므로 HTTPS를 사용하여 암호화해야 합니다.</li>
</ul>
</li>
</ol>
<p>이 과정을 통해 JWT를 사용한 인증 시스템을 구현할 수 있습니다. JWT는 세션리스 인증 방식으로 확장성과 보안성을 동시에 제공합니다.</p>
<hr>
<h2 id="🎯-핵심-키워드">🎯 핵심 키워드</h2>
<ul>
<li><strong>Spring Security</strong>: 인증과 인가를 통합 관리하는 프레임워크</li>
<li><strong>Authentication</strong>: 사용자 신원 확인</li>
<li><strong>Authorization</strong>: 리소스 접근 권한 부여</li>
<li><strong>OAuth2</strong>: 제3자 애플리케이션 인증 프로토콜</li>
</ul>
<hr>
<p>이제 로그인까지 모두 배웠내요!! 길었던 10주차의 스터디가 드디어 끝이 났습니다..... 저도 많은 스프링 내용을 한번에 배우느라 진짜 힘들었던 거 같아요. 그래도 정말 많은 걸 배웠던 시간인 거 같습니다. 남은 2주차에는 CI/CD와 AWS에 대해서 살펴보겠습니다!! 여기까지 같이 따라오셨으면 정말 수고 많으셨어요😊</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ch9. Spring Data JPA로 효율적인 목록 조회 API 설계와 구현]]></title>
            <link>https://velog.io/@funda__mental_/Ch9.-Spring-Data-JPA%EB%A1%9C-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-%EB%AA%A9%EB%A1%9D-%EC%A1%B0%ED%9A%8C-API-%EC%84%A4%EA%B3%84%EC%99%80-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@funda__mental_/Ch9.-Spring-Data-JPA%EB%A1%9C-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-%EB%AA%A9%EB%A1%9D-%EC%A1%B0%ED%9A%8C-API-%EC%84%A4%EA%B3%84%EC%99%80-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Sun, 29 Dec 2024 06:19:02 GMT</pubDate>
            <description><![CDATA[<h4 id="학습-목표"><strong>학습 목표</strong></h4>
<ol>
<li><strong>효율적인 목록 조회 API 설계</strong> 방법을 탐구하고, 페이징 처리의 원리를 이해한다.  </li>
<li><strong>Spring Data JPA를 활용</strong>하여 데이터 양이 많은 환경에서 성능 최적화 방법을 학습한다.</li>
</ol>
<hr>
<h3 id="1-왜-목록-조회-api가-중요할까"><strong>1. 왜 목록 조회 API가 중요할까?</strong></h3>
<h4 id="🌟-목록-조회-api의-중요성">🌟 <strong>목록 조회 API의 중요성</strong></h4>
<ol>
<li><strong>성능 최적화의 핵심:</strong>  <ul>
<li>조회 API는 시스템 부하의 주요 원인 중 하나입니다. 데이터 양이 많을수록 효율적인 처리가 중요합니다.</li>
</ul>
</li>
<li><strong>사용자 경험 개선:</strong>  <ul>
<li>클라이언트가 한 번에 모든 데이터를 로드할 필요 없이 필요한 만큼의 데이터를 요청할 수 있어야 합니다.</li>
</ul>
</li>
<li><strong>데이터 전송 비용 감소:</strong>  <ul>
<li>불필요한 데이터를 줄이고 네트워크 전송량을 최소화합니다.</li>
</ul>
</li>
</ol>
<hr>
<h4 id="⚠️-문제-데이터가-많아질수록-발생하는-성능-문제">⚠️ <strong>문제: 데이터가 많아질수록 발생하는 성능 문제</strong></h4>
<ul>
<li><strong>예시 상황:</strong><br>리뷰 데이터가 10만 건일 때, 클라이언트가 한 번의 요청으로 모든 데이터를 요청한다면:<ol>
<li><strong>네트워크 부하:</strong> 전송량이 기하급수적으로 증가.</li>
<li><strong>메모리 문제:</strong> 클라이언트와 서버에서 데이터 처리 용량 초과.</li>
<li><strong>응답 지연:</strong> 사용자 경험 저하.</li>
</ol>
</li>
</ul>
<p>이를 해결하기 위해 <strong>페이징(Paging)</strong>과 같은 기법이 필요합니다.</p>
<hr>
<h3 id="2-설계-원리와-구현-프로세스"><strong>2. 설계 원리와 구현 프로세스</strong></h3>
<p>자 이제 차근차근 API를 만들어 봅시다.<br>보통 API를 만들 때 아래의 순서를 따르면 우왕좌왕 하지 않고 만들 수 있습니다.</p>
<blockquote>
<p>🧭 <strong>전제 조건</strong>: API URL은 설계가 되었다는 전제입니다!</p>
</blockquote>
<ol>
<li><strong>API 시그니처를 만든다!</strong><ul>
<li>응답과 요청 DTO를 만든다.</li>
<li>컨트롤러에서 어떤 형태를 리턴하는지, 어떤 파라미터가 필요한지, URI는 무엇인지 HTTP Method는 무엇인지만 정해둔다.</li>
<li>컨버터 정의만 해둔다.</li>
</ul>
</li>
<li><strong>API 시그니처를 바탕으로 Swagger에 명세를 해준다.</strong></li>
<li><strong>데이터베이스와 연결하는 부분을 만든다.</strong></li>
<li><strong>비즈니스 로직을 만든다.</strong></li>
<li><strong>컨트롤러를 완성한다.</strong></li>
<li><strong>Validation 처리를 한다.</strong></li>
</ol>
<hr>
<h3 id="21-api-시그니처-설계"><strong>2.1. API 시그니처 설계</strong></h3>
<h4 id="api-시그니처란"><strong>API 시그니처란?</strong></h4>
<p><strong>API 시그니처</strong>는 API의 <strong>기본 설계 정보</strong>로, <strong>클라이언트와 서버 간에 어떤 데이터를 주고받을지 정의한 약속</strong>입니다. API가 어떤 역할을 하고, 어떤 데이터를 입력받고 출력하는지 명확히 알려주는 가이드라고 볼 수 있습니다.</p>
<p><strong>주요 구성 요소</strong></p>
<ol>
<li><strong>URI</strong>: API의 주소 (예: <code>/stores/{storeId}/reviews</code>)</li>
<li><strong>HTTP Method</strong>: 작업 유형 (예: <code>GET</code>, <code>POST</code>)</li>
<li><strong>요청 데이터</strong>:<ul>
<li><strong>Path Variable</strong>: URL 경로에 포함된 데이터 (예: <code>{storeId}</code>)</li>
<li><strong>Query Parameter</strong>: URL 쿼리 스트링 데이터 (예: <code>?page=1</code>)</li>
<li><strong>Body</strong>: 요청 본문 데이터 (예: JSON 형식)</li>
</ul>
</li>
<li><strong>응답 데이터</strong>: 서버가 반환하는 데이터의 구조 (예: JSON)</li>
<li><strong>상태 코드</strong>: 요청 결과를 나타내는 코드 (예: <code>200 OK</code>, <code>400 Bad Request</code>)</li>
</ol>
<hr>
<h4 id="간단한-예-특정-가게의-리뷰-목록을-조회하는-api"><strong>간단한 예</strong>: 특정 가게의 리뷰 목록을 조회하는 API</h4>
<ul>
<li><strong>URI</strong>: <code>/stores/{storeId}/reviews</code></li>
<li><strong>HTTP Method</strong>: <code>GET</code></li>
<li><strong>요청 데이터</strong>:<ul>
<li>Path Variable: <code>storeId</code> (가게 ID)</li>
<li>Query Parameter: <code>page</code> (페이지 번호)</li>
</ul>
</li>
<li><strong>응답 데이터</strong>:<pre><code class="language-json">{
    &quot;reviewList&quot;: [...],
    &quot;totalElements&quot;: 100,
    &quot;isLast&quot;: false
}</code></pre>
</li>
<li><strong>상태 코드</strong>: <code>200 OK</code>, <code>401 Unauthorized</code></li>
</ul>
<hr>
<p>해당 글에서 API 시그니처를 만든다는 말은 아래와 같습니다. </p>
<ol>
<li>응답과 요청 DTO를 만든다.</li>
<li>컨트롤러에서 어떤 형태를 리턴하는지, 어떤 파라미터가 필요한지, URI는 무엇인지 HTTP Method는 무엇인지만 정해둔다.</li>
<li>컨버터 정의만 해둔다.</li>
</ol>
<h4 id="📘-dto-구현-코드">📘 <strong>DTO 구현 코드</strong></h4>
<pre><code class="language-java">public class StoreResponseDTO {

    @Builder
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class ReviewPreViewListDTO {
        List&lt;ReviewPreViewDTO&gt; reviewList;
        Integer listSize;
        Integer totalPage;
        Long totalElements;
        Boolean isFirst;
        Boolean isLast;
    }

    @Builder
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class ReviewPreViewDTO {
        String ownerNickname;
        Float score;
        String body;
        LocalDate createdAt;
    }
}</code></pre>
<h4 id="✅-설계-원칙">✅ <strong>설계 원칙</strong></h4>
<ul>
<li><code>ReviewPreViewDTO</code>는 단일 리뷰 정보를 담고, 이를 리스트 형태로 묶은 <code>ReviewPreViewListDTO</code>를 반환.</li>
<li><strong>Builder 패턴</strong>을 사용해 불변성을 보장하고 코드 가독성을 향상.</li>
</ul>
<hr>
<h3 id="22-convertor-겉-모양정의만-설계"><strong>2.2. Convertor 겉 모양(정의만) 설계</strong></h3>
<pre><code class="language-java">public class StoreConverter {

    public static StoreResponseDTO.ReviewPreViewDTO reviewPreViewDTO(Review review) {
        return null;
    }

    public static StoreResponseDTO.ReviewPreViewListDTO reviewPreViewListDTO(List&lt;Review&gt; reviewList) {
        return null;
    }
}</code></pre>
<hr>
<h3 id="23-swagger를-이용한-api-명세-작성"><strong>2.3. Swagger를 이용한 API 명세 작성</strong></h3>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
@Validated
@RequestMapping(&quot;/stores&quot;)
public class StoreRestController {

    private final StoreQueryService storeQueryService;

    @GetMapping(&quot;/{storeId}/reviews&quot;)
    @Operation(summary = &quot;특정 가게의 리뷰 목록 조회 API&quot;, description = &quot;특정 가게의 리뷰들의 목록을 조회하는 API이며, 페이징을 포함합니다. Query String으로 페이지 번호를 제공합니다.&quot;)
    @ApiResponses({
        @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = &quot;COMMON200&quot;, description = &quot;OK, 성공&quot;),
        @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = &quot;AUTH003&quot;, description = &quot;access 토큰을 제공해 주세요!&quot;, content = @Content(schema = @Schema(implementation = ApiResponse.class))),
        @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = &quot;AUTH004&quot;, description = &quot;access 토큰 만료&quot;, content = @Content(schema = @Schema(implementation = ApiResponse.class))),
        @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = &quot;AUTH006&quot;, description = &quot;access 토큰 형식 오류&quot;, content = @Content(schema = @Schema(implementation = ApiResponse.class))),
    })
    @Parameters({
        @Parameter(name = &quot;storeId&quot;, description = &quot;가게의 ID입니다 (Path Variable)&quot;),
        @Parameter(name = &quot;page&quot;, description = &quot;페이지 번호입니다 (Query Parameter)&quot;)
    })
    public ApiResponse&lt;StoreResponseDTO.ReviewPreViewListDTO&gt; getReviewList(
            @ExistStore @PathVariable(name = &quot;storeId&quot;) Long storeId,
            @RequestParam(name = &quot;page&quot;) Integer page) {
        storeQueryService.getReviewList(storeId, page);
        return null;
    }
}</code></pre>
<ul>
<li><strong>@Operation</strong>: API 설명을 정의(summary와 description 사용).  </li>
<li><strong>@ApiResponses</strong>: API 응답에 대한 설명 포함.  <ul>
<li>에러 상황에는 <code>content</code>를 통해 오류 응답의 스키마를 정의.  </li>
<li>성공 응답은 스키마 없이 <code>ApiResponse&lt;T&gt;</code>를 보여줌.  </li>
</ul>
</li>
<li><strong>@Parameters</strong>: 프론트엔드에서 전달해야 하는 파라미터를 명시.</li>
</ul>
<hr>
<h3 id="24-서비스-메서드-로직-작성"><strong>2.4. 서비스 메서드 로직 작성</strong></h3>
<h4 id="📘-서비스-인터페이스">📘 <strong>서비스 인터페이스</strong></h4>
<pre><code class="language-java">public interface StoreQueryService {

    Page&lt;Review&gt; getReviewList(Long storeId, Integer page);
}</code></pre>
<h4 id="📘-리포지토리-인터페이스">📘 <strong>리포지토리 인터페이스</strong></h4>
<pre><code class="language-java">public interface ReviewRepository extends JpaRepository&lt;Review, Long&gt; {

    Page&lt;Review&gt; findAllByStore(Store store, PageRequest pageRequest);
}</code></pre>
<hr>
<h4 id="📘-서비스-구현">📘 <strong>서비스 구현</strong></h4>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class StoreQueryServiceImpl implements StoreQueryService {

    private final StoreRepository storeRepository;
    private final ReviewRepository reviewRepository;

    @Override
    public Page&lt;Review&gt; getReviewList(Long storeId, Integer page) {
        Store store = storeRepository.findById(storeId).orElseThrow();
        return reviewRepository.findAllByStore(store, PageRequest.of(page, 10));
    }
}</code></pre>
<p><strong>serviceImpl</strong></p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class StoreQueryServiceImpl implements StoreQueryService{

    private final StoreRepository storeRepository;

    private final ReviewRepository reviewRepository;

    // ... 다른 코드들

    @Override
    public Page&lt;Review&gt; getReviewList(Long StoreId, Integer page) {

        Store store = storeRepository.findById(StoreId).get();

        Page&lt;Review&gt; StorePage = reviewRepository.findAllByStore(store, PageRequest.of(page, 10));
        return StorePage;
    }
}</code></pre>
<p><strong>controller</strong></p>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
@Validated
@RequestMapping(&quot;/stores&quot;)
public class StoreRestController {

    private final StoreQueryService storeQueryService;

    @GetMapping(&quot;/{storeId}/reviews&quot;)
    @Operation(summary = &quot;특정 가게의 리뷰 목록 조회 API&quot;,description = &quot;특정 가게의 리뷰들의 목록을 조회하는 API이며, 페이징을 포함합니다. query String 으로 page 번호를 주세요&quot;)
    @ApiResponses({
            @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = &quot;COMMON200&quot;,description = &quot;OK, 성공&quot;),
            @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = &quot;AUTH003&quot;, description = &quot;access 토큰을 주세요!&quot;,content = @Content(schema = @Schema(implementation = ApiResponse.class))),
            @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = &quot;AUTH004&quot;, description = &quot;acess 토큰 만료&quot;,content = @Content(schema = @Schema(implementation = ApiResponse.class))),
            @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = &quot;AUTH006&quot;, description = &quot;acess 토큰 모양이 이상함&quot;,content = @Content(schema = @Schema(implementation = ApiResponse.class))),
    })
    @Parameters({
            @Parameter(name = &quot;storeId&quot;, description = &quot;가게의 아이디, path variable 입니다!&quot;)
    })
    public ApiResponse&lt;StoreResponseDTO.ReviewPreViewListDTO&gt; getReviewList(@ExistStore @PathVariable(name = &quot;storeId&quot;) Long storeId,@RequestParam(name = &quot;page&quot;) Integer page){
        storeQueryService.getReviewList(storeId,page);
        return null;
    }
}</code></pre>
<p><strong>converter</strong></p>
<p>여기서 converter를 보면,
ListDTO를 위해 리스트에 들어갈 DTO를 다른 Converter에서 제작해서 이를 Java stream을 통해 DTO의 List를 만드는 것을 알 수 있습니다.</p>
<pre><code class="language-java">public class StoreConverter {

    public static StoreResponseDTO.ReviewPreViewDTO reviewPreViewDTO(Review review){
        return StoreResponseDTO.ReviewPreViewDTO.builder()
                **.ownerNickname(review.getMember().getName()) // 객체그래프 탐색**
                .score(review.getScore())
                .createdAt(review.getCreatedAt().toLocalDate())
                .body(review.getBody())
                .build();
    }
    public static StoreResponseDTO.ReviewPreViewListDTO reviewPreViewListDTO(**Page**&lt;Review&gt; reviewList){

        List&lt;StoreResponseDTO.ReviewPreViewDTO&gt; reviewPreViewDTOList = reviewList.stream()
                .map(StoreConverter::reviewPreViewDTO).collect(Collectors.toList());

        return StoreResponseDTO.ReviewPreViewListDTO.builder()
                .isLast(reviewList.isLast())
                .isFirst(reviewList.isFirst())
                .totalPage(reviewList.getTotalPages())
                .totalElements(reviewList.getTotalElements())
                .listSize(reviewPreViewDTOList.size())
                .reviewList(reviewPreViewDTOList)
                .build();
    }
}</code></pre>
<p>그리고 .ownerNickname(review.getMember().getName())</p>
<p>이 코드를 통해 review에 @MantyToOne으로 지정해둔 Member를 통해 아주 편하게 데이터를 가져오는 것을 확인 할 수 있습니다.</p>
<p>이는 <strong>객체 그래프 탐색 이라는 Spring Data JPA에서 사용 가능한 아주 강력한 기능입니다.</strong></p>
<p>이제 컨트롤러를 controller를 converter에 맞게 바꾸고 </p>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
@Validated
@RequestMapping(&quot;/stores&quot;)
public class StoreRestController {

    private final StoreQueryService storeQueryService;

    @GetMapping(&quot;/{storeId}/reviews&quot;)
    @Operation(summary = &quot;특정 가게의 리뷰 목록 조회 API&quot;,description = &quot;특정 가게의 리뷰들의 목록을 조회하는 API이며, 페이징을 포함합니다. query String 으로 page 번호를 주세요&quot;)
    @ApiResponses({
            @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = &quot;COMMON200&quot;,description = &quot;OK, 성공&quot;),
            @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = &quot;AUTH003&quot;, description = &quot;access 토큰을 주세요!&quot;,content = @Content(schema = @Schema(implementation = ApiResponse.class))),
            @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = &quot;AUTH004&quot;, description = &quot;acess 토큰 만료&quot;,content = @Content(schema = @Schema(implementation = ApiResponse.class))),
            @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = &quot;AUTH006&quot;, description = &quot;acess 토큰 모양이 이상함&quot;,content = @Content(schema = @Schema(implementation = ApiResponse.class))),
    })
    @Parameters({
            @Parameter(name = &quot;storeId&quot;, description = &quot;가게의 아이디, path variable 입니다!&quot;)
    })
    public ApiResponse&lt;StoreResponseDTO.ReviewPreViewListDTO&gt; getReviewList(@ExistStore @PathVariable(name = &quot;storeId&quot;) Long storeId,@RequestParam(name = &quot;page&quot;) Integer page){
        storeQueryService.getReviewList(storeId,page);
        return ApiResponse.onSuccess(StoreConverter.reviewPreViewListDTO(reviewList));
    }
}</code></pre>
<hr>
<h3 id="3-cqrs-설계-원칙"><strong>3. CQRS 설계 원칙</strong></h3>
<h4 id="cqrs-command-query-responsibility-segregation란"><strong>CQRS (Command Query Responsibility Segregation)란?</strong></h4>
<p>CQRS는 <strong>데이터 변경 작업 (Command)</strong>과 <strong>조회 작업 (Query)</strong>을 분리하여 각 작업의 책임을 명확히 하는 설계 원칙입니다.</p>
<hr>
<h3 id="4-객체-그래프-탐색과-spring-data-jpa"><strong>4. 객체 그래프 탐색과 Spring Data JPA</strong></h3>
<h4 id="🌟-객체-그래프-탐색">🌟 <strong>객체 그래프 탐색</strong></h4>
<p>Spring Data JPA에서 객체 그래프 탐색은 연관 관계에 있는 엔티티를 함께 조회하는 방법을 의미합니다. 보통 연관 관계 매핑(@OneToMany, @ManyToOne 등)된 엔티티를 처리할 때 사용됩니다.</p>
<h4 id="주요-기법">주요 기법</h4>
<ul>
<li><p><strong>JPQL 및 Fetch Join 사용</strong></p>
<ul>
<li><p><strong>특징</strong></p>
<ul>
<li>JPQL에서 <code>JOIN FETCH</code> 키워드를 사용하여 연관 데이터를 한 번에 로드합니다.</li>
<li><strong>EAGER</strong>와 유사하지만, 개발자가 명시적으로 특정 상황에서만 데이터를 로드하도록 설계할 수 있습니다.</li>
<li><code>Fetch Join</code>을 사용하면 즉시 로딩(<code>EAGER</code>)이 이루어지며, N+1 문제를 방지할 수 있습니다.</li>
</ul>
</li>
<li><p><strong>장점</strong></p>
<ul>
<li>한 번의 쿼리로 연관 데이터를 로드하므로 성능이 최적화됩니다.</li>
<li>특정 조건이나 필터를 JPQL로 지정할 수 있어 유연합니다.</li>
</ul>
</li>
<li><p><strong>단점</strong></p>
<ul>
<li>복잡한 JPQL 작성이 필요하며, 쿼리가 길어질 수 있습니다.</li>
<li>데이터가 많을 경우, 한 번의 쿼리로 많은 데이터를 로드해 메모리 문제를 일으킬 수 있습니다.</li>
<li>JPQL을 직접 작성해야 하므로 코드 유지보수가 어려울 수 있습니다.</li>
</ul>
</li>
<li><p><strong>사용 예</strong></p>
<pre><code class="language-java">  @Query(&quot;SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :id&quot;)
  User findUserWithOrders(@Param(&quot;id&quot;) Long id);</code></pre>
</li>
</ul>
</li>
<li><p><strong>EntityGraph 사용</strong></p>
<ul>
<li><p>JPQL을 사용하지 않고, <code>@EntityGraph</code>를 통해 연관 데이터를 로드할 수 있습니다.</p>
</li>
<li><p><strong>특징</strong></p>
<ul>
<li><code>@EntityGraph</code>를 사용해 JPQL 없이 객체 그래프 탐색을 정의할 수 있습니다.</li>
<li>선언적으로 연관 엔티티를 함께 로드할 경로(<code>attributePaths</code>)를 지정합니다.</li>
<li><code>EntityGraph</code>는 JPA Repository에서 바로 활용 가능하며, <strong>Lazy Loading</strong> 엔티티를 명시적으로 Fetch Join 형태로 로드합니다.</li>
</ul>
</li>
<li><p><strong>장점</strong></p>
<ul>
<li>JPQL 작성 없이 선언적으로 연관 데이터를 로드할 수 있어 유지보수가 용이합니다.</li>
<li>비즈니스 로직과 탐색 경로 설정이 분리되어 코드 가독성이 높아집니다.</li>
<li>특정 메서드에만 적용 가능하므로 성능을 세밀하게 조정할 수 있습니다.</li>
</ul>
</li>
<li><p><strong>단점</strong></p>
<ul>
<li>설정된 경로(<code>attributePaths</code>) 이외의 데이터는 로드되지 않으므로 추가 데이터가 필요할 경우 다시 쿼리가 발생할 수 있습니다.</li>
<li>복잡한 객체 그래프 탐색이 필요한 경우에는 사용이 제한적입니다.</li>
</ul>
</li>
<li><p><strong>예제</strong></p>
<pre><code class="language-java">  @EntityGraph(attributePaths = {&quot;orders&quot;})
  @Query(&quot;SELECT u FROM User u WHERE u.id = :id&quot;)
  User findUserWithOrders(@Param(&quot;id&quot;) Long id);</code></pre>
</li>
</ul>
</li>
<li><p><strong>Hibernate 초기화 강제화</strong></p>
<ul>
<li><p>Lazy 로딩된 데이터를 강제로 초기화합니다.</p>
</li>
<li><p><strong>특징</strong></p>
<ul>
<li><code>Hibernate.initialize()</code>를 사용해 지연 로딩된 데이터를 강제로 초기화합니다.</li>
<li>특정 비즈니스 로직에 필요한 경우, Lazy 로딩된 연관 엔티티를 조회 시점에 로드합니다.</li>
</ul>
</li>
<li><p><strong>장점</strong></p>
<ul>
<li>매우 유연하며, Lazy 로딩된 데이터를 필요할 때만 초기화할 수 있습니다.</li>
<li>로딩 시점과 방법을 완전히 제어할 수 있습니다.</li>
</ul>
</li>
<li><p><strong>단점</strong></p>
<ul>
<li>데이터를 강제로 초기화하기 때문에 추가적인 쿼리가 발생합니다.</li>
<li>잘못 사용하면 N+1 문제가 심화될 수 있습니다.</li>
<li>코드에서 명시적으로 초기화를 작성해야 하므로 코드가 복잡해질 수 있습니다.</li>
</ul>
</li>
<li><p><strong>예제</strong></p>
<pre><code class="language-java">  User user = userRepository.findById(1L).get();
  Hibernate.initialize(user.getOrders()); // Lazy 로딩된 orders를 강제 초기화</code></pre>
</li>
</ul>
</li>
<li><p><strong>Batch Fetching</strong></p>
<ul>
<li><p>N+1 문제를 해결하기 위해 여러 연관 데이터를 한 번에 가져옵니다.</p>
</li>
<li><p><strong>특징</strong></p>
<ul>
<li>Hibernate의 <code>@BatchSize</code> 또는 <code>hibernate.default_batch_fetch_size</code> 설정을 통해 연관된 엔티티를 한 번에 로드합니다.</li>
<li>Lazy 로딩된 데이터를 <strong>배치(batch)</strong> 방식으로 가져와, 여러 엔티티에 대해 한 번의 쿼리를 실행합니다.</li>
<li>기본적으로 <code>SELECT</code> 쿼리를 최적화하여 N+1 문제를 완화합니다.</li>
</ul>
</li>
<li><p><strong>장점</strong></p>
<ul>
<li>N+1 문제를 방지하면서도 데이터의 필요 여부에 따라 Lazy 로딩을 유지합니다.</li>
<li>데이터 양이 많아도 메모리를 과도하게 사용하지 않고, 일정 크기로 나누어 로드합니다.</li>
</ul>
</li>
<li><p><strong>단점</strong></p>
<ul>
<li>잘못된 Batch 크기 설정은 오히려 성능에 부정적인 영향을 미칠 수 있습니다.</li>
<li>모든 경우에 최적화되지 않으며, 데이터 로드 크기를 세밀하게 조정해야 합니다.</li>
</ul>
</li>
<li><p><strong>사용 예</strong></p>
<pre><code class="language-java">  @BatchSize(size = 10)
  @OneToMany(mappedBy = &quot;user&quot;, fetch = FetchType.LAZY)
  private List&lt;Order&gt; orders;</code></pre>
</li>
<li><p><strong>전역 설정</strong></p>
<pre><code>  properties

  spring.jpa.properties.hibernate.default_batch_fetch_size=10</code></pre></li>
</ul>
</li>
</ul>
<p><strong>주요 기법 비교</strong></p>
<table>
<thead>
<tr>
<th><strong>기법</strong></th>
<th><strong>장점</strong></th>
<th><strong>단점</strong></th>
<th><strong>적합한 상황</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>JPQL 및 Fetch Join</strong></td>
<td>한 번의 쿼리로 객체 그래프 탐색 가능.조건부 로딩 가능.</td>
<td>JPQL 작성 필요.메모리 문제 발생 가능.</td>
<td>복잡한 조건부 연관 데이터 탐색 시 적합.</td>
</tr>
<tr>
<td><strong>EntityGraph</strong></td>
<td>선언적 탐색으로 코드 간결화.Lazy 로딩 문제 해결 가능.</td>
<td>복잡한 그래프 탐색에 한계.추가 데이터 필요 시 다시 쿼리 발생.</td>
<td>간단한 객체 그래프 탐색 시.유지보수 용이성이 중요할 때.</td>
</tr>
<tr>
<td><strong>Hibernate 초기화 강제화</strong></td>
<td>로딩 시점과 방법을 완전히 제어 가능.필요할 때만 초기화.</td>
<td>코드 복잡도 증가.추가 쿼리로 인해 성능 저하 가능.N+1 문제 발생 가능.</td>
<td>Lazy 로딩이 기본이지만 일부 데이터만 로드해야 할 때.</td>
</tr>
<tr>
<td><strong>Batch Fetching</strong></td>
<td>N+1 문제 완화.대량 데이터도 메모리 사용을 최적화하며 로드.</td>
<td>Batch 크기 조정 필요.잘못 설정 시 성능 저하.</td>
<td>다수의 Lazy 로딩 엔티티를 효율적으로 로드해야 할 때.</td>
</tr>
</tbody></table>
<hr>
<h3 id="마무리"><strong>마무리</strong></h3>
<p>이번 글을 통해 성능 최적화와 유지보수성을 고려한 목록 조회 API의 핵심 원칙을 학습했습니다. 거의 다 이 시리즈에 끝에 와갑니다!😁</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ch8. 목록 조회 API & Swagger]]></title>
            <link>https://velog.io/@funda__mental_/Ch8.-%EB%AA%A9%EB%A1%9D-%EC%A1%B0%ED%9A%8C-API-Swagger</link>
            <guid>https://velog.io/@funda__mental_/Ch8.-%EB%AA%A9%EB%A1%9D-%EC%A1%B0%ED%9A%8C-API-Swagger</guid>
            <pubDate>Sun, 29 Dec 2024 06:01:56 GMT</pubDate>
            <description><![CDATA[<h3 id="목표"><strong>목표</strong></h3>
<ol>
<li>Database와 상호작용하는 API의 전체 과정을 익힌다.</li>
<li>Swagger 세팅을 학습한다.</li>
<li>어노테이션을 활용한 데이터 검증 방법을 익힌다.</li>
</ol>
<hr>
<h3 id="8주차-주제-및-진행"><strong>8주차 주제 및 진행</strong></h3>
<p>주요 내용은 <strong>Database와 상호작용하는 API</strong>의 흐름을 이해하는 것입니다.<br>지난 포스팅의 내용을 기반으로 구체적인 예제를 다룹니다.  </p>
<p>또한, Service와 Controller 단에서 데이터 검증 처리를 코드가 아닌 <strong>어노테이션</strong>으로 수행하는 방법을 통해 단일 책임 원칙을 준수하고, <strong>더 깔끔한 코드 작성 방법</strong>을 배웁니다.</p>
<hr>
<h3 id="1-회원-가입-api-구현"><strong>1. 회원 가입 API 구현</strong></h3>
<blockquote>
<p><strong>GitHub 이슈 및 브랜치 만들기</strong></p>
</blockquote>
<ol>
<li>GitHub에서 <strong>이슈</strong>를 생성합니다.<br>라벨은 깃모지(gitmoji)를 활용해 꾸며보세요.<br>예시:  <ul>
<li>🐛 <code>:bug:</code>  </li>
<li>📝 <code>:memo:</code>  </li>
<li>✨ <code>:sparkles:</code>  </li>
</ul>
</li>
<li><strong>브랜치 생성</strong>  <ul>
<li>브랜치 이름 예시: <code>feature/#이슈번호</code>  </li>
<li>예를 들어, <code>feature/#3</code>  </li>
</ul>
</li>
<li><strong>브랜치 작업 시 주의사항</strong>  <ul>
<li>항상 작업할 브랜치가 <strong>최신 상태인지 확인</strong> 후 작업을 시작하세요.  </li>
</ul>
</li>
</ol>
<h4 id="1-dto-생성"><strong>1) DTO 생성</strong></h4>
<p>회원 가입에 필요한 Request와 Response DTO를 생성합니다.</p>
<pre><code class="language-java">// Request DTO
public class MemberRequestDTO {
    @Getter
    public static class JoinDto {
        String name;
        Integer gender;
        Integer birthYear;
        Integer birthMonth;
        Integer birthDay;
        String address;
        String specAddress;
        List&lt;Long&gt; preferCategory;
    }
}

// Response DTO
public class MemberResponseDTO {
    @Builder
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class JoinResultDTO {
        Long memberId;
        LocalDateTime createdAt;
    }
}</code></pre>
<hr>
<h4 id="2-converter-생성"><strong>2) Converter 생성</strong></h4>
<p>엔티티와 DTO 간 변환을 위한 Converter를 생성합니다.</p>
<pre><code class="language-java">public class MemberConverter {
    public static MemberResponseDTO.JoinResultDTO toJoinResultDTO(Member member) {
        return MemberResponseDTO.JoinResultDTO.builder()
            .memberId(member.getId())
            .createdAt(LocalDateTime.now())
            .build();
    }

    public static Member toMember(MemberRequestDTO.JoinDto request) {
        Gender gender = switch (request.getGender()) {
            case 1 -&gt; Gender.MALE;
            case 2 -&gt; Gender.FEMALE;
            default -&gt; Gender.NONE;
        };

        return Member.builder()
            .name(request.getName())
            .gender(gender)
            .address(request.getAddress())
            .specAddress(request.getSpecAddress())
            .build();
    }
}</code></pre>
<hr>
<h4 id="3-controller-생성"><strong>3) Controller 생성</strong></h4>
<p>클라이언트 요청을 처리하는 Controller를 작성합니다.</p>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/members&quot;)
public class MemberRestController {

    private final MemberCommandService memberCommandService;

    @PostMapping(&quot;/&quot;)
    public ApiResponse&lt;MemberResponseDTO.JoinResultDTO&gt; join(@RequestBody @Valid MemberRequestDTO.JoinDto request) {
        Member member = memberCommandService.joinMember(request);
        return ApiResponse.onSuccess(MemberConverter.toJoinResultDTO(member));
    }
}</code></pre>
<hr>
<h4 id="4-service-생성"><strong>4) Service 생성</strong></h4>
<p>비즈니스 로직을 처리하는 Service를 작성합니다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class MemberCommandServiceImpl implements MemberCommandService {
    private final MemberRepository memberRepository;
    private final FoodCategoryRepository foodCategoryRepository;

    @Transactional
    @Override
    public Member joinMember(MemberRequestDTO.JoinDto request) {
        Member newMember = MemberConverter.toMember(request);

        // 선호 카테고리 처리
        List&lt;FoodCategory&gt; foodCategoryList = request.getPreferCategory().stream()
            .map(categoryId -&gt; foodCategoryRepository.findById(categoryId)
                .orElseThrow(() -&gt; new IllegalArgumentException(&quot;카테고리 없음&quot;)))
            .toList();

        foodCategoryList.forEach(category -&gt; {
            MemberPrefer prefer = MemberPrefer.builder()
                .member(newMember)
                .foodCategory(category)
                .build();
            newMember.getMemberPreferList().add(prefer);
        });

        return memberRepository.save(newMember);
    }
}</code></pre>
<hr>
<h4 id="5-repository-생성"><strong>5) Repository 생성</strong></h4>
<p>Spring Data JPA를 사용해 Repository를 작성합니다.</p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {}
public interface FoodCategoryRepository extends JpaRepository&lt;FoodCategory, Long&gt; {}</code></pre>
<hr>
<h3 id="6-swagger-세팅"><strong>6) Swagger 세팅</strong></h3>
<ol>
<li><p><code>build.gradle</code>에 의존성 추가:</p>
<pre><code class="language-gradle"> implementation &#39;org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0&#39;</code></pre>
</li>
<li><p>Swagger 설정 클래스 생성:</p>
<pre><code class="language-java"> @Configuration
 public class SwaggerConfig {
     @Bean
     public OpenAPI umcStudyAPI() {
         return new OpenAPI()
             .addServersItem(new Server().url(&quot;/&quot;))
             .info(new Info().title(&quot;UMC API&quot;).version(&quot;1.0.0&quot;).description(&quot;API 명세서&quot;));
     }
 }</code></pre>
</li>
<li><p>Swagger 확인:<br><code>http://localhost:8080/swagger-ui/index.html#/</code>로 접속합니다.</p>
</li>
</ol>
<hr>
<h3 id="7-어노테이션-기반-validation"><strong>7) 어노테이션 기반 Validation</strong></h3>
<ol>
<li><p>Request DTO에 어노테이션 추가:</p>
<pre><code class="language-java"> public class MemberRequestDTO {
     @Getter
     public static class JoinDto {
         @NotBlank
         String name;
         @NotNull
         Integer gender;
         @Size(min = 5, max = 12)
         String address;
         @ExistCategories
         List&lt;Long&gt; preferCategory;
     }
 }</code></pre>
</li>
<li><p>커스텀 어노테이션 정의:</p>
<pre><code class="language-java"> @Documented
 @Constraint(validatedBy = CategoriesExistValidator.class)
 @Target({ElementType.FIELD})
 @Retention(RetentionPolicy.RUNTIME)
 public @interface ExistCategories {
     String message() default &quot;카테고리 존재하지 않음&quot;;
     Class&lt;?&gt;[] groups() default {};
     Class&lt;? extends Payload&gt;[] payload() default {};
 }</code></pre>
</li>
<li><p>Validator 작성:</p>
<pre><code class="language-java"> @Component
 @RequiredArgsConstructor
 public class CategoriesExistValidator implements ConstraintValidator&lt;ExistCategories, List&lt;Long&gt;&gt; {
     private final FoodCategoryRepository foodCategoryRepository;

     @Override
     public boolean isValid(List&lt;Long&gt; value, ConstraintValidatorContext context) {
         return value.stream().allMatch(foodCategoryRepository::existsById);
     }
 }</code></pre>
</li>
</ol>
<p>그러면 아래 과정을 통해 완성이 되게 됩니다. 
<img src="https://velog.velcdn.com/images/funda__mental_/post/ebfdc1af-4be9-4630-8870-aec74291c93f/image.png" alt=""></p>
<hr>
<h3 id="결론-및-회고"><strong>결론 및 회고</strong></h3>
<p>이번 주 학습에서는 Database와 상호작용하는 API의 기본 흐름을 학습했습니다. 특히 Swagger와 Validation 어노테이션을 통해 <strong>개발자 경험(DX)</strong>을 개선하는 방법을 배우는 것이 핵심이었습니다.
다음 포스팅에서는 목록 조회가 아닌 API를 만들어보면서, 페이징을 적용해보겠습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 7. API 응답 통일 & 에러 핸들러 ]]></title>
            <link>https://velog.io/@funda__mental_/Chapter-7.-API-%EC%9D%91%EB%8B%B5-%ED%86%B5%EC%9D%BC-%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%9F%AC</link>
            <guid>https://velog.io/@funda__mental_/Chapter-7.-API-%EC%9D%91%EB%8B%B5-%ED%86%B5%EC%9D%BC-%EC%97%90%EB%9F%AC-%ED%95%B8%EB%93%A4%EB%9F%AC</guid>
            <pubDate>Fri, 27 Dec 2024 09:26:13 GMT</pubDate>
            <description><![CDATA[<p>이번 시간에는 API 응답 통일과 에러 핸들링을 스프링 부트에서 어떻게 구현하는지 알아봅시다! 😊 이를 제대로 배우면 스프링부트 프로젝트의 상당 부분을 구현할 수 있게 됩니다. </p>
<hr>
<h2 id="🏁-이번-주차-목표">🏁 <strong>이번 주차 목표</strong></h2>
<ul>
<li>API 응답의 일관성을 유지하는 방법을 학습합니다.  </li>
<li>에러 핸들링의 원리를 이해하고, 효율적인 구현 방식을 알아봅니다.</li>
</ul>
<hr>
<h2 id="🌱-1-api-응답-통일이란">🌱 <strong>1. API 응답 통일이란?</strong></h2>
<p>API란 애플리케이션들 간의 대화하는 통로 역할을 하는 것이었는데요. </p>
<h4 id="그렇다면-왜-api-응답을-통일해야-할까요">그렇다면, 왜 API 응답을 통일해야 할까요?</h4>
<p>API 개발 시 응답 형식이 통일되지 않으면 협업과 유지보수에 큰 문제가 생깁니다. 특히 프론트엔드와 대화를 해야하는 수단인 API에서 응답의 형태가 제각각이면, 이를 파악하기가 어렵겠죠?   </p>
<p>따라서 API의 응답을 통일 하는 것은 프로젝트 진행에 있어 매우 필요한 작업입니다.</p>
<h4 id="1-1-통일된-api-응답의-구조">1-1. 통일된 API 응답의 구조</h4>
<p>API의 형태는 프로젝트마다 다르지만, 응답 형식은 대개 다음과 같은 형태를 가집니다:</p>
<pre><code class="language-json">{
  &quot;isSuccess&quot;: true,
  &quot;code&quot;: &quot;200&quot;,
  &quot;message&quot;: &quot;Request Successful&quot;,
  &quot;result&quot;: {
    &quot;data&quot;: &quot;Your Data Here&quot;
  }
}</code></pre>
<p><strong>각 필드의 의미</strong>  </p>
<ul>
<li><strong><code>isSuccess</code></strong>: 요청 성공 여부 (<code>true</code> 또는 <code>false</code>)  </li>
<li><strong><code>code</code></strong>: 상태 코드를 나타내는 값. HTTP 상태코드로는 너무 제한적인 정보만 줄 수 있어서 조금 더 세부적인 응답 상황을 알려주기 위한 필드</li>
<li><strong><code>message</code></strong>: 상태 코드(<code>code</code>)에 추가적으로 내용 설명 </li>
<li><strong><code>result</code></strong>: 요청 결과 데이터를 포함 (<code>null</code>로 설정 가능, DTO와 같은 것) 실제로 클라이언트에게 필요한 데이터가 담깁니다. 보통 에러 상황에는 null을 담지만, 아닌 경우도 있음</li>
</ul>
<h5 id="🪄-http-상태-코드">🪄 HTTP 상태 코드?</h5>
<p>HTTP 상태 코드는 여러가지가 있지만 200번 대, 400번 대, 500번 대만 알아봅시다.
아래에 작성한 상태 코드 정도만 알아도 충분합니다만, 궁금하시면 더 찾아보세요!</p>
<ol>
<li><strong>200번 대</strong> : 문제 없음<ol>
<li><strong>200</strong> : OK 성공임</li>
<li><strong>201</strong> : Created: 네가 준 데이터를 가지고 적절한 과정을 거쳐 새로운 리소스를 만들었어</li>
</ol>
</li>
<li><strong>400번 대</strong> : 클라이언트 측 잘못으로 인한 에러<ol>
<li><strong>400</strong> : Bad Request : 요청 이상하게 함, 필요한 정보 누락됨..</li>
<li><strong>401</strong> : Unauthorized : 인증이 안됨 (로그인이 되어야 하는데 안된 상황)</li>
<li><strong>403</strong> : Forbidden : 권한 없음 (로그인은 되었으나 접근이 안됨, 관리자 페이지 등등)</li>
<li><strong>404</strong> : NotFound :  요청한 정보가 그냥 없음</li>
</ol>
</li>
<li><strong>500번 대</strong> : 서버 측 잘못으로 인한 에러(<del>안돼…….😱</del>)<ol>
<li><strong>500</strong> : Internal Server Error : 서버 터짐…….</li>
<li><strong>504</strong> : Gateway Timeout : 서버가 응답을 안 줌 (<del>그냥 터진거긴 함..</del>)</li>
</ol>
</li>
</ol>
<hr>
<h3 id="🌟-1-2-api-응답-통일하는-방법-알아보기">🌟 <strong>1-2. API 응답 통일하는 방법 알아보기</strong></h3>
<p>이제 실제로 차근차근 API 응답을 어떻게 통일하는지 살펴봅시다. 응답의 경우 <strong>enum</strong>으로 그 형태를 관리합니다.</p>
<p>이 때, 성공 응답과 실패 응답을 하나의 enum으로 관리할 수도 있고, 분리할 수도 있습니다. 이번에는 분리해서 관리하는 걸 같이 봐봅시다. </p>
<h4 id="1-디렉토리-구조-생성">1. 디렉토리 구조 생성</h4>
<p>먼저, apiPayload라는 디렉토리를 만들고 그 아래에 API 응답 통일을 위한 ApiResponse 클래스를 만들어주세요. 그리고 code라는 패키지도 디렉토리 내에 만들어주세요
<img src="https://velog.velcdn.com/images/funda__mental_/post/ff2c0166-f630-447c-91f6-22b9b6339d57/image.png" alt=""></p>
<h4 id="2-apiresponse-기본-틀-생성">2. ApiResponse 기본 틀 생성</h4>
<p>이 클래스는 모든 API 응답의 기본 구조를 제공합니다.  </p>
<pre><code class="language-java">@Getter
@AllArgsConstructor
@JsonPropertyOrder({&quot;isSuccess&quot;, &quot;code&quot;, &quot;message&quot;, &quot;result&quot;})
public class ApiResponse&lt;T&gt; {
    private final Boolean isSuccess; // 성공 여부
    private final String code;       // 상태 코드
    private final String message;    // 응답 메시지
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private final T result;          // 실제 데이터

    // 성공 응답 생성 메서드
    public static &lt;T&gt; ApiResponse&lt;T&gt; onSuccess(T result) {
        return new ApiResponse&lt;&gt;(true, &quot;2000&quot;, &quot;Request Successful&quot;, result);
    }

    // 실패 응답 생성 메서드
    public static &lt;T&gt; ApiResponse&lt;T&gt; onFailure(String code, String message) {
        return new ApiResponse&lt;&gt;(false, code, message, null);
    }
}</code></pre>
<p>result는 어떤 형태의 값이 올지 모르기에 Generic으로 만들어 줍니다.</p>
<h4 id="3--api-응답에-들어갈-code와-message의-형식-생성">3.  API 응답에 들어갈 code와 message의 형식 생성</h4>
<p>아까 만든 폴더 아래에 아래와 같은 폴더 형태를 만들어주세요
<img src="https://velog.velcdn.com/images/funda__mental_/post/532cd075-a523-43cb-a0d1-9a162de51012/image.png" alt=""></p>
<h4 id="3-1-응답을-해주는-errorreasondto와-reasondto의-코드를-적어주기">3-1) 응답을 해주는 ErrorReasonDTO와 ReasonDTO의 코드를 적어주기</h4>
<p>이 코드는 단순히 데이터를 담는 컨테이너 역할으로, 프로젝트가 확장될수록 DTO는 응답 구조를 구체화하는 데 활용될 것입니다. </p>
<pre><code class="language-java">@Getter
@Builder
public class ErrorReasonDTO {

    private HttpStatus httpStatus;

    private final boolean isSuccess;
    private final String code;
    private final String message;

    public boolean getIsSuccess(){return isSuccess;}
}</code></pre>
<pre><code class="language-java">@Getter
@Builder
public class ReasonDTO {

    private HttpStatus httpStatus;

    private final boolean isSuccess;
    private final String code;
    private final String message;

    public boolean getIsSuccess(){return isSuccess;}
}</code></pre>
<blockquote>
<p>둘의 코드가 같은데 왜 따로 만들지?
 두 클래스가 <strong>현재는 동일한 코드</strong>를 가지고 있지만, <strong>구체적인 용도와 의미를 구분하기 위해</strong> 따로 정의된 것
 =&gt; 이러한 구조는 <strong>확장성</strong>과 <strong>유지보수성</strong>을 고려한 설계입니다!</p>
</blockquote>
<h4 id="3-2-basecode와-baseerrorcode-작성">3-2) BaseCode와 BaseErrorCode 작성</h4>
<p>두 코드는 code 내용을 구체화 하는 <code>Status</code>에서 두 개의 메소드를 반드시 Override할 것을 강제하는 역할을 합니다. 상태 코드, 메시지 등의 구조를 정의하고, 이를 Enum과 결합하여 일관된 상태 코드 관리를 가능하게 하는 것이죠.</p>
<pre><code class="language-java">public interface BaseCode {

    Reason getReason();

    Reason getReasonHttpStatus();
}</code></pre>
<pre><code class="language-java">public interface BaseErrorCode {

    ErrorReason getReason();

    ErrorReason getReasonHttpStatus();
}</code></pre>
<blockquote>
<p><strong>Q. DTO와 BaseCode는 어떻게 활용되는 건가요?</strong> 
<em>사용 흐름 예시</em></p>
</blockquote>
<ol>
<li><code>BaseCode</code> / <code>BaseErrorCode</code> 에서 상태 코드와 메시지를 정의.</li>
<li>컨트롤러에서 상태 코드와 메시지를 가져와 <code>ReasonDTO</code>/<code>ReasonErrorDTO</code>를 생성.</li>
<li>DTO를 클라이언트에게 응답으로 전달.</li>
</ol>
<h4 id="3-3-성공-응답을-나타내는-successstatus-작성">3-3) 성공 응답을 나타내는 SuccessStatus 작성</h4>
<pre><code class="language-java">@Getter
@AllArgsConstructor
public enum SuccessStatus implements BaseCode {

    // 일반적인 응답
    _OK(HttpStatus.OK, &quot;COMMON200&quot;, &quot;성공입니다.&quot;);

    private final HttpStatus httpStatus;
    private final String code;
    private final String message;

    @Override
    public ReasonDTO getReason() {
        return ReasonDTO.builder()
                .message(message)
                .code(code)
                .isSuccess(true)
                .build();
    }

    @Override
    public ReasonDTO getReasonHttpStatus() {
        return ReasonDTO.builder()
                .message(message)
                .code(code)
                .isSuccess(true)
                .httpStatus(httpStatus)
                .build()
                ;
    }
}</code></pre>
<p>코드를 자세히 보면, enum 자신의 값으로 가지고 있던 message, code, httpStatus값을 interface의 메소드 오버라이딩을 통하여 DTO를 만드는 것을 확인할 수 있습니다.</p>
<hr>
<h3 id="1-3-spring-boot로-임시-api-만들어보기"><strong>1-3. Spring Boot로 임시 API 만들어보기</strong></h3>
<h4 id="1-api-설계"><strong>1. API 설계</strong></h4>
<p><strong>API 정보</strong></p>
<ul>
<li><strong>HTTP 메서드</strong>: <code>GET</code></li>
<li><strong>URL</strong>: <code>/temp/test</code></li>
<li><strong>Query String, Request Body, Request Header</strong>: 없음</li>
<li><strong>Response</strong>: 아래와 같은 JSON 형식 반환<pre><code class="language-json">{
    &quot;isSuccess&quot;: true,
    &quot;code&quot;: &quot;2000&quot;,
    &quot;message&quot;: &quot;OK&quot;,
    &quot;result&quot;: {
        &quot;testString&quot;: &quot;This is test!&quot;
    }
}</code></pre>
</li>
</ul>
<h4 id="2-dtodata-transfer-object-작성"><strong>2. DTO(Data Transfer Object) 작성</strong></h4>
<p>DTO는 데이터를 전달하기 위한 객체입니다.<br>이번 API에서는 <code>TempResponse</code> 클래스를 작성하여 응답 데이터 구조를 정의합니다. RequestBody에 담겨오는 값은 없으므로, 응답용 DTO만 작성합니다.</p>
<p><strong>TempResponse 코드</strong></p>
<pre><code class="language-java">public class TempResponse {

    @Builder
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class TempTestDTO {
        String testString;
    }
}</code></pre>
<p><strong>코드 분석</strong></p>
<ol>
<li><p><strong><code>@Builder</code></strong></p>
<ul>
<li>객체를 빌더 패턴으로 생성하도록 지원.</li>
<li><strong>가독성</strong>과 <strong>유연성</strong>을 제공하며, 생성자 대신 직관적으로 객체를 생성할 수 있음.</li>
</ul>
<ol start="2">
<li><strong><code>@Getter</code></strong></li>
</ol>
<ul>
<li>DTO 클래스의 모든 필드에 대해 getter 메서드를 자동 생성.</li>
<li>코드 간결화와 캡슐화 유지에 도움.</li>
</ul>
</li>
<li><p><strong><code>@NoArgsConstructor</code></strong></p>
<ul>
<li>매개변수가 없는 기본 생성자를 자동 생성.</li>
</ul>
</li>
<li><p><strong><code>@AllArgsConstructor</code></strong></p>
<ul>
<li>모든 필드를 초기화하는 생성자를 제공.</li>
<li>테스트 코드 작성 시나 특정 상황에서 객체를 한 번에 초기화할 때 유용.</li>
</ul>
</li>
<li><p><strong>Static Class 사용</strong></p>
<ul>
<li>DTO를 큰 묶음으로 관리할 수 있도록 설계.</li>
<li>여러 관련 DTO를 하나의 클래스 내부에서 관리하여 <strong>코드 구조를 간소화</strong>하고 <strong>재사용성을 높임</strong>.</li>
</ul>
<blockquote>
<p><strong>추가 설명</strong>
<strong>빌더 패턴이란?</strong>
빌더 패턴(Builder Pattern)은 객체 생성 패턴 중 하나로, 복잡한 객체를 단계별로 구성할 수 있도록 설계된 디자인 패턴입니다. </p>
</blockquote>
<ul>
<li>주로 객체 생성 과정이 복잡하거나, 동일한 생성 프로세스를 통해 다양한 객체 구성을 제공해야 할 때 사용.</li>
<li>객체 생성 시 생성자 대신 <code>@Builder</code>를 활용하여 <strong>직관적이고 가독성이 높은 코드 작성</strong>이 가능.</li>
<li>필드가 많거나 선택적 필드가 있는 객체를 생성할 때 유리.</li>
<li><em>빌더 패턴 사용 예시*</em><pre><code class="language-java">TempResponse.TempTestDTO dto = TempResponse.TempTestDTO.builder()
   .testString(&quot;This is Test!&quot;)
   .build();</code></pre>
</li>
<li><em>빌더 패턴의 장점*</em></li>
<li>필드 순서에 상관없이 원하는 값만 설정 가능.</li>
<li>코드 가독성과 유지보수성 향상.</li>
</ul>
<blockquote>
<p><strong>왜 <code>public static class</code>를 사용할까?</strong></p>
</blockquote>
<ul>
<li>DTO는 여러 곳에서 사용될 수 있으므로 <strong>범용적인 사용</strong>을 위해 내부 static 클래스로 정의.</li>
<li><strong>장점</strong>:<ul>
<li>별도의 DTO 파일을 만들지 않아도 됨.</li>
<li>관련된 DTO들을 하나의 클래스로 묶어 관리 가능.</li>
</ul>
</li>
</ul>
</li>
</ol>
<h3 id="3-converter-작성"><strong>3. Converter 작성</strong></h3>
<p>Converter는 데이터 변환을 담당하는 클래스입니다.<br>여기서는 <code>TempConverter</code> 클래스를 작성해 <code>TempResponse.TempTestDTO</code> 객체를 생성합니다.</p>
<p><strong>TempConverter 코드</strong></p>
<pre><code class="language-java">public class TempConverter {

    public static TempResponse.TempTestDTO toTempTestDTO() {
        return TempResponse.TempTestDTO.builder()
                .testString(&quot;This is Test!&quot;)
                .build();
    }
}</code></pre>
<p><strong>코드 분석</strong></p>
<ol>
<li><strong><code>toTempTestDTO()</code></strong>: <code>TempTestDTO</code> 객체를 생성하고 반환하는 정적 메서드.</li>
<li>빌더 패턴을 활용하여 가독성과 유연성을 높임.</li>
</ol>
<h3 id="4-controller-작성"><strong>4. Controller 작성</strong></h3>
<p>Controller는 클라이언트 요청을 처리하고 응답을 반환하는 역할을 담당합니다.<br>이번 API는 비즈니스 로직이나 DB 연동이 없으므로 Service와 Repository 계층을 생략하고 작성합니다.</p>
<p> <strong>TempRestController 코드</strong></p>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/temp&quot;)
@RequiredArgsConstructor
public class TempRestController {

    @GetMapping(&quot;/test&quot;)
    public ApiResponse&lt;TempResponse.TempTestDTO&gt; testAPI() {
        return ApiResponse.onSuccess(TempConverter.toTempTestDTO());
    }
}</code></pre>
<p><strong>코드 분석</strong></p>
<ol>
<li><strong><code>@RestController</code></strong>: RESTful 웹 서비스 컨트롤러를 나타냄. JSON 응답 자동 반환.</li>
<li><strong><code>@RequestMapping(&quot;/temp&quot;)</code></strong>: 모든 엔드포인트에 <code>/temp</code> URL prefix를 설정.</li>
<li><strong><code>@GetMapping(&quot;/test&quot;)</code></strong>: <code>/temp/test</code> 경로의 GET 요청을 처리.</li>
<li><strong><code>ApiResponse.onSuccess()</code></strong>: 성공적인 응답 구조를 표준화하여 생성.</li>
</ol>
<h3 id="5-api-실행-결과"><strong>5. API 실행 결과</strong></h3>
<p><strong>로컬 환경에서 API 호출</strong>
<code>localhost:8080/temp/test</code>로 GET 요청을 보내면 다음과 같은 JSON 응답을 반환합니다.</p>
<pre><code class="language-json">{
    &quot;isSuccess&quot;: true,
    &quot;code&quot;: &quot;2000&quot;,
    &quot;message&quot;: &quot;OK&quot;,
    &quot;result&quot;: {
        &quot;testString&quot;: &quot;This is test!&quot;
    }
}</code></pre>
<p><strong>지금은 로컬호스트이지만, 원격 서버 Nginx에 Spring Boot을 연동했다면...</strong></p>
<ol>
<li>클라이언트 요청은 Nginx에 도달.</li>
<li>Nginx는 요청을 Spring Boot 애플리케이션으로 전달(리버스 프록시).</li>
<li>Spring Boot에서 처리한 결과를 Nginx가 클라이언트에 반환.
이 순서로 작동되게 됩니다.</li>
</ol>
<blockquote>
<p><strong>로컬서버 vs Ngnix</strong></p>
</blockquote>
<ul>
<li><strong>로컬 서버</strong>:
내 컴퓨터 안에서만 작동.
예를 들어, localhost:3000 같은 주소를 브라우저에 입력하면, 내 컴퓨터가 서버처럼 작동해서 결과를 보여줌.</li>
<li><strong>Nginx</strong>:
인터넷에 연결된 컴퓨터(서버)에 설치됨.
다른 사람들이 내 웹사이트 주소(<a href="http://www.mywebsite.com)%EC%97%90">www.mywebsite.com)에</a> 접속하면, Nginx가 &quot;어! 여기에 파일 있네!&quot; 하고 데이터를 전달해줌.
추가로, 내가 만든 프로그램(예: Django, Spring 같은 백엔드)도 연결해서 사용자 요청을 처리할 수 있게 도와줌.</li>
</ul>
<hr>
<h2 id="🌱-2에러-핸들링의-중요성">🌱 <strong>2.에러 핸들링의 중요성</strong></h2>
<h4 id="에러를-관리해야-하는-이유">에러를 관리해야 하는 이유</h4>
<ul>
<li><strong>사용자 경험 개선</strong>: 명확한 에러 메시지를 제공하면 사용자가 문제를 이해하고 해결하기 쉬워집니다.  </li>
<li><strong>디버깅 용이성</strong>: 통일된 에러 구조는 개발자가 문제를 빠르게 파악하고 수정하는 데 도움을 줍니다.  </li>
</ul>
<blockquote>
<p>** Enum으로 에러 코드 관리하기 **- 추천 방식</p>
</blockquote>
<ol>
<li>common 에러는 COMMON000 으로 둔다. &lt;- 잘 안쓰지만 마땅하지 않을 때 사용</li>
<li>관련된 경우마다 code에 명시적으로 표현한다.<ul>
<li>예를 들어 멤버 관련이면 MEMBER001 이런 식으로</li>
</ul>
</li>
<li>2번에 이어서 4000번대를 붙인다. 서버측 잘못은 그냥 COMMON 에러의 서버 에러를 쓰면 됨.<ul>
<li>MEMBER400_1 아니면 MEMBER4001 이런 식으로</li>
</ul>
</li>
</ol>
<hr>
<h3 id="2-2-spring-boot-에러-핸들링-구현-과정">2-2. Spring Boot 에러 핸들링 구현 과정</h3>
<p>Spring Boot에서는 <code>@RestControllerAdvice</code>를 사용하여 전역적으로 발생하는 예외를 처리하고, 표준화된 응답을 반환할 수 있습니다. </p>
<h3 id="1-errorstatus-작성하기">1) ErrorStatus 작성하기</h3>
<p><code>ErrorStatus</code>는 애플리케이션에서 발생하는 다양한 에러를 관리하는 에러 상태 코드 클래스입니다.</p>
<pre><code class="language-java">@Getter
@AllArgsConstructor
public enum ErrorStatus implements BaseErrorCode {

    // 공통 에러
    _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, &quot;COMMON500&quot;, &quot;서버 에러, 관리자에게 문의 바랍니다.&quot;),
    _BAD_REQUEST(HttpStatus.BAD_REQUEST, &quot;COMMON400&quot;, &quot;잘못된 요청입니다.&quot;),
    _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, &quot;COMMON401&quot;, &quot;인증이 필요합니다.&quot;),
    _FORBIDDEN(HttpStatus.FORBIDDEN, &quot;COMMON403&quot;, &quot;금지된 요청입니다.&quot;),

    // 사용자 관련 에러
    MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, &quot;MEMBER4001&quot;, &quot;사용자가 없습니다.&quot;),
    NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, &quot;MEMBER4002&quot;, &quot;닉네임은 필수입니다.&quot;),

    // 기타 에러
    ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, &quot;ARTICLE4001&quot;, &quot;게시글이 없습니다.&quot;);

    private final HttpStatus httpStatus;
    private final String code;
    private final String message;

    @Override
    public ErrorReasonDTO getReason() {
        return ErrorReasonDTO.builder()
                .message(message)
                .code(code)
                .isSuccess(false)
                .build();
    }

    @Override
    public ErrorReasonDTO getReasonHttpStatus() {
        return ErrorReasonDTO.builder()
                .message(message)
                .code(code)
                .isSuccess(false)
                .httpStatus(httpStatus)
                .build();
    }
}</code></pre>
<h4 id="errorstatus가-필요한-이유">ErrorStatus가 필요한 이유</h4>
<ul>
<li>에러 코드를 중앙에서 관리할 수 있어, 일관성 있고 읽기 쉬운 에러 처리가 가능합니다.</li>
<li>클라이언트에게 명확한 에러 메시지와 상태 코드를 제공할 수 있습니다.</li>
</ul>
<h3 id="2-generalexception-추가하기">2) GeneralException 추가하기</h3>
<p><code>GeneralException</code>은 특정 조건에서 발생하는 커스텀 에러를 처리하기 위해 작성된 사용자 정의 예외 클래스입니다.</p>
<pre><code class="language-java">@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException {

    private BaseErrorCode code;

    public ErrorReasonDTO getErrorReason() {
        return this.code.getReason();
    }

    public ErrorReasonDTO getErrorReasonHttpStatus() {
        return this.code.getReasonHttpStatus();
    }
}</code></pre>
<h4 id="generalexception이-필요한-이유">GeneralException이 필요한 이유</h4>
<ul>
<li>개발자가 원하는 대로 커스텀 예외를 생성하고 처리할 수 있습니다.</li>
<li>비즈니스 로직에 따라 발생하는 특정 에러를 명확히 처리할 수 있습니다.</li>
</ul>
<h3 id="3-에러-핸들러-만들기-exceptionadvice">3) 에러 핸들러 만들기 (ExceptionAdvice)</h3>
<p><code>ExceptionAdvice</code> 클래스는 애플리케이션의 모든 예외를 처리하고 표준화된 JSON 응답을 반환합니다.</p>
<h4 id="buildgradle에-의존성-추가">build.gradle에 의존성 추가</h4>
<p>유효성 검사 예외를 처리하려면 validation 관련 라이브러리를 추가해야 합니다.</p>
<pre><code class="language-groovy">implementation &#39;org.springframework.boot:spring-boot-starter-validation&#39;</code></pre>
<h4 id="exceptionadvice-코드">ExceptionAdvice 코드</h4>
<pre><code class="language-java">@Slf4j
@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {

    @ExceptionHandler
    public ResponseEntity&lt;Object&gt; validation(ConstraintViolationException e, WebRequest request) {
        String errorMessage = e.getConstraintViolations().stream()
                .map(constraintViolation -&gt; constraintViolation.getMessage())
                .findFirst()
                .orElseThrow(() -&gt; new RuntimeException(&quot;ConstraintViolationException 추출 도중 에러 발생&quot;));

        return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY, request);
    }

    @Override
    public ResponseEntity&lt;Object&gt; handleMethodArgumentNotValid(MethodArgumentNotValidException e, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
        Map&lt;String, String&gt; errors = new LinkedHashMap&lt;&gt;();

        e.getBindingResult().getFieldErrors().forEach(fieldError -&gt; {
            String fieldName = fieldError.getField();
            String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse(&quot;&quot;);
            errors.merge(fieldName, errorMessage, (existing, newMsg) -&gt; existing + &quot;, &quot; + newMsg);
        });

        return handleExceptionInternalArgs(e, HttpHeaders.EMPTY, ErrorStatus._BAD_REQUEST, request, errors);
    }

    @ExceptionHandler
    public ResponseEntity&lt;Object&gt; exception(Exception e, WebRequest request) {
        e.printStackTrace();
        return handleExceptionInternalFalse(e, ErrorStatus._INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(), request, e.getMessage());
    }

    @ExceptionHandler(value = GeneralException.class)
    public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) {
        ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
        return handleExceptionInternal(generalException, errorReasonHttpStatus, null, request);
    }

    private ResponseEntity&lt;Object&gt; handleExceptionInternal(Exception e, ErrorReasonDTO reason, HttpHeaders headers, HttpServletRequest request) {
        ApiResponse&lt;Object&gt; body = ApiResponse.onFailure(reason.getCode(), reason.getMessage(), null);
        WebRequest webRequest = new ServletWebRequest(request);
        return super.handleExceptionInternal(e, body, headers, reason.getHttpStatus(), webRequest);
    }

    private ResponseEntity&lt;Object&gt; handleExceptionInternalFalse(Exception e, ErrorStatus errorCommonStatus, HttpHeaders headers, HttpStatus status, WebRequest request, String errorPoint) {
        ApiResponse&lt;Object&gt; body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), errorPoint);
        return super.handleExceptionInternal(e, body, headers, status, request);
    }

    private ResponseEntity&lt;Object&gt; handleExceptionInternalArgs(Exception e, HttpHeaders headers, ErrorStatus errorCommonStatus, WebRequest request, Map&lt;String, String&gt; errorArgs) {
        ApiResponse&lt;Object&gt; body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), errorArgs);
        return super.handleExceptionInternal(e, body, headers, errorCommonStatus.getHttpStatus(), request);
    }

    private ResponseEntity&lt;Object&gt; handleExceptionInternalConstraint(Exception e, ErrorStatus errorCommonStatus, HttpHeaders headers, WebRequest request) {
        ApiResponse&lt;Object&gt; body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), null);
        return super.handleExceptionInternal(e, body, headers, errorCommonStatus.getHttpStatus(), request);
    }
}</code></pre>
<h4 id="주요-메서드-설명">주요 메서드 설명</h4>
<ol>
<li><p><strong>validation()</strong>: <code>ConstraintViolationException</code> 처리</p>
<ul>
<li>유효성 검사 실패 시 발생하는 예외를 처리하고, 표준화된 응답을 반환합니다.</li>
</ul>
</li>
<li><p><strong>handleMethodArgumentNotValid()</strong>: <code>MethodArgumentNotValidException</code> 처리</p>
<ul>
<li>메서드 인자의 유효성 검사가 실패할 경우, 필드별 에러 메시지를 수집해 반환합니다.</li>
</ul>
</li>
<li><p><strong>exception()</strong>: 일반 예외 처리</p>
<ul>
<li>예상치 못한 예외를 포괄적으로 처리하여 클라이언트에게 서버 에러 메시지를 반환합니다.</li>
</ul>
</li>
<li><p><strong>onThrowException()</strong>: 사용자 정의 예외 처리</p>
<ul>
<li><code>GeneralException</code>을 처리하여, 비즈니스 로직에 따라 커스텀 응답을 생성합니다.</li>
</ul>
</li>
<li><p><strong>handleExceptionInternal()</strong>: 기본 예외 응답 생성</p>
<ul>
<li>표준화된 JSON 응답을 생성합니다.</li>
</ul>
</li>
</ol>
<h4 id="예시-json-응답">예시 JSON 응답</h4>
<pre><code class="language-json">{
  &quot;success&quot;: false,
  &quot;code&quot;: &quot;MEMBER4001&quot;,
  &quot;message&quot;: &quot;사용자가 없습니다.&quot;,
  &quot;data&quot;: null
}</code></pre>
<hr>
<h3 id="2-3-실제로-에러가-처리를-구현하는-과정-예시">2-3. 실제로 에러가 처리를 구현하는 과정 예시</h3>
<p><strong>GET /temp/exception</strong> 요청에 대한 예외 처리 구현 과정을 예시로 살펴봅시다. 이 API는 Query String으로 <code>flag</code>를 받아오며, <code>flag</code> 값이 2일 경우 Exception을 발생시킵니다. </p>
<h4 id="1-errorstatus에-새로운-경우-추가하기">1. ErrorStatus에 새로운 경우 추가하기</h4>
<p><code>ErrorStatus</code>에 테스트용 에러 상태를 추가합니다.</p>
<pre><code class="language-java">// For test
TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, &quot;TEMP4001&quot;, &quot;이거는 테스트&quot;);</code></pre>
<h4 id="2-temp-핸들러-추가하기">2. Temp 핸들러 추가하기</h4>
<p><strong>handler 패키지</strong>에 <code>TempHandler</code> 클래스를 생성합니다. 이 클래스는 <code>GeneralException</code>을 상속받습니다.</p>
<pre><code class="language-java">public class TempHandler extends GeneralException {

    public TempHandler(BaseErrorCode errorCode) {
        super(errorCode);
    }
}</code></pre>
<p><code>TempHandler</code>는 테스트용 예외를 처리하기 위해 작성되었고, 부모 클래스의 생성자를 호출해 에러 코드를 전달합니다.</p>
<h4 id="3-tempresponse-dto-추가하기">3. TempResponse DTO 추가하기</h4>
<p><code>TempResponse</code> 클래스는 예외와 테스트 응답 데이터를 포함합니다.</p>
<pre><code class="language-java">public class TempResponse {

    @Builder
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class TempTestDTO {
        String testString;
    }

    @Builder
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class TempExceptionDTO {
        Integer flag;
    }
}</code></pre>
<ul>
<li><code>TempTestDTO</code>: 테스트 API 응답 데이터.</li>
<li><code>TempExceptionDTO</code>: 예외 발생 시 전달되는 데이터.</li>
</ul>
<h4 id="4-tempconverter-작성하기">4. TempConverter 작성하기</h4>
<p><code>TempConverter</code>는 DTO를 생성하는 정적 메서드를 제공합니다. 이를 통해 <code>TempResponse</code> DTO를 생성해서 반환합니다. </p>
<pre><code class="language-java">public class TempConverter {

    public static TempResponse.TempTestDTO toTempTestDTO() {
        return TempResponse.TempTestDTO.builder()
                .testString(&quot;This is Test!&quot;)
                .build();
    }

    public static TempResponse.TempExceptionDTO toTempExceptionDTO(Integer flag) {
        return TempResponse.TempExceptionDTO.builder()
                .flag(flag)
                .build();
    }
}</code></pre>
<ul>
<li><strong>toTempTestDTO</strong>: 테스트 응답 객체 생성.</li>
<li><strong>toTempExceptionDTO</strong>: 예외 발생 시 전달할 DTO 생성.</li>
</ul>
<h4 id="5-controller-작성하기">5. Controller 작성하기</h4>
<p><code>TempRestController</code>는 <code>@RestController</code>를 사용하여 API 요청을 처리합니다.</p>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/temp&quot;)
@RequiredArgsConstructor
public class TempRestController {

    @GetMapping(&quot;/test&quot;)
    public ApiResponse&lt;TempResponse.TempTestDTO&gt; testAPI() {
        return ApiResponse.onSuccess(TempConverter.toTempTestDTO());
    }

    @GetMapping(&quot;/exception&quot;)
    public ApiResponse&lt;TempResponse.TempExceptionDTO&gt; exceptionAPI(@RequestParam Integer flag) {
        return null;
    }
}</code></pre>
<ul>
<li><code>@RequestParam</code>을 통해 Query String에서 값을 받아옵니다.</li>
<li><code>/test</code> API는 테스트 데이터를 반환합니다.</li>
<li><code>/exception</code> API는 서비스 로직과 연동됩니다.</li>
</ul>
<p>이때, RestControllerAdvice를 통해 @RestController가 붙은 대상에서 Exception이 발생하는 것을 감지하게 됩니다. </p>
<h4 id="6-service-작성하기">6. Service 작성하기</h4>
<blockquote>
<p><strong>🥕서비스 설계 원칙</strong>
  <strong>1. GET 요청과 나머지 요청의 로직을 분리합니다.</strong>
     - GET 요청: <code>TempQueryService</code>.
     - 기타 요청: <code>TempCommandService</code>.
  <strong>2. 인터페이스를 먼저 정의한 뒤 구체화 클래스를 작성합니다.</strong>
     - 인터페이스: <code>TempQueryService</code>.
     - 구현체: <code>TempQueryServiceImpl</code>.
<strong>3. 컨트롤러는 인터페이스에 의존하며, Spring의 의존성 주입을 통해 구현체를 주입받습니다.</strong></p>
</blockquote>
<h4 id="tempqueryservice-인터페이스">TempQueryService 인터페이스</h4>
<pre><code class="language-java">public interface TempQueryService {

    void checkFlag(Integer flag);
}</code></pre>
<h4 id="tempqueryserviceimpl-구현체">TempQueryServiceImpl 구현체</h4>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class TempQueryServiceImpl implements TempQueryService {

    @Override
    public void checkFlag(Integer flag) {
        if (flag == 1)
            throw new TempHandler(ErrorStatus.TEMP_EXCEPTION);
    }
}</code></pre>
<ul>
<li><code>checkFlag</code>: <code>flag</code> 값이 1인 경우 <code>TempHandler</code> 예외를 던집니다.</li>
</ul>
<h4 id="7-controller-완성하기">7. Controller 완성하기</h4>
<p><code>TempRestController</code>의 <code>/exception</code> API를 완성합니다.</p>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/temp&quot;)
@RequiredArgsConstructor
public class TempRestController {

    private final TempQueryService tempQueryService;

    @GetMapping(&quot;/test&quot;)
    public ApiResponse&lt;TempResponse.TempTestDTO&gt; testAPI() {
        return ApiResponse.onSuccess(TempConverter.toTempTestDTO());
    }

    @GetMapping(&quot;/exception&quot;)
    public ApiResponse&lt;TempResponse.TempExceptionDTO&gt; exceptionAPI(@RequestParam Integer flag) {
        tempQueryService.checkFlag(flag);
        return ApiResponse.onSuccess(TempConverter.toTempExceptionDTO(flag));
    }
}</code></pre>
<p><code>tempQueryService.checkFlag(flag)</code>를 호출하여 예외 발생 여부를 확인하고, 예외가 발생하지 않으면 DTO를 반환합니다.</p>
<hr>
<h4 id="위-과정을-따라-에러가-처리되는-과정">위 과정을 따라 에러가 처리되는 과정</h4>
<ol>
<li><p><strong>Service에서 예외 발생</strong>
<code>TempQueryServiceImpl</code>의 <code>checkFlag</code> 메서드에서 <code>flag</code> 값이 1인 경우 <code>TempHandler</code>를 통해 예외를 발생시킵니다.</p>
<pre><code class="language-java">@Override
public void checkFlag(Integer flag) {
    if (flag == 1)
        throw new TempHandler(ErrorStatus.TEMP_EXCEPTION);
}</code></pre>
</li>
<li><p><strong>ExceptionAdvice에서 예외 처리</strong>
<code>TempHandler</code>는 <code>GeneralException</code>을 상속받기 때문에 <code>@RestControllerAdvice</code>에 등록된 <code>ExceptionAdvice</code>가 예외를 처리합니다.</p>
<pre><code class="language-java">@ExceptionHandler(value = GeneralException.class)
public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) {
    ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
    return handleExceptionInternal(generalException, errorReasonHttpStatus, null, request);
}</code></pre>
</li>
<li><p><strong>표준화된 응답 반환</strong>
<code>handleExceptionInternal</code> 메서드를 통해 표준화된 JSON 응답이 클라이언트로 반환됩니다.</p>
<pre><code class="language-java">private ResponseEntity&lt;Object&gt; handleExceptionInternal(Exception e, ErrorReasonDTO reason, HttpHeaders headers, HttpServletRequest request) {
    ApiResponse&lt;Object&gt; body = ApiResponse.onFailure(reason.getCode(), reason.getMessage(), null);
    WebRequest webRequest = new ServletWebRequest(request);
    return super.handleExceptionInternal(e, body, headers, reason.getHttpStatus(), webRequest);
}</code></pre>
</li>
<li><p><strong>응답 결과 확인</strong>
클라이언트는 에러 코드와 메시지를 포함한 표준화된 응답을 받습니다.</p>
</li>
</ol>
<h4 id="예외-응답-예시">예외 응답 예시</h4>
<pre><code class="language-json">{
  &quot;success&quot;: false,
  &quot;code&quot;: &quot;TEMP4001&quot;,
  &quot;message&quot;: &quot;이거는 테스트&quot;
}</code></pre>
<hr>
<h3 id="📝-정리하며">📝 <strong>정리하며</strong></h3>
<ul>
<li><strong>API 응답 통일</strong>은 협업과 유지보수성을 높이는 핵심입니다.  </li>
<li><strong>에러 핸들링</strong>은 명확하고 일관된 방식으로 사용자와 개발자 모두에게 유용한 정보를 제공합니다.  </li>
</ul>
<p>🎯 <strong>다음 시간에는 오늘 배운 내용을 더 구체적인 과정으로 하나하나씩 살펴보고 실제 예제를 같이 보겠습니다!</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 6. JPA 활용: 즉시 로딩, N+1 문제, 영속성 컨텍스트, JPQL, QueryDSL]]></title>
            <link>https://velog.io/@funda__mental_/Chapter-6.-JPA-%ED%99%9C%EC%9A%A9-%EC%A6%89%EC%8B%9C-%EB%A1%9C%EB%94%A9-N1-%EB%AC%B8%EC%A0%9C-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-JPQL-QueryDSL</link>
            <guid>https://velog.io/@funda__mental_/Chapter-6.-JPA-%ED%99%9C%EC%9A%A9-%EC%A6%89%EC%8B%9C-%EB%A1%9C%EB%94%A9-N1-%EB%AC%B8%EC%A0%9C-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-JPQL-QueryDSL</guid>
            <pubDate>Thu, 26 Dec 2024 07:01:22 GMT</pubDate>
            <description><![CDATA[<p>이번 시간에는 저번 포스팅에서 배운 JPA를 더 자세히 배워보는 시간을 갖겠습니다!</p>
<hr>
<h2 id="🌟-학습-목표">🌟 학습 목표</h2>
<ol>
<li><strong>즉시 로딩과 지연 로딩</strong>의 전략 차이를 이해하고, <strong>지연 로딩</strong>을 추천하는 이유를 살펴본다.  </li>
<li><strong>JPQL</strong>과 <strong>QueryDSL</strong>의 차이를 파악하고, 실제 프로젝트에서의 활용 방법을 익힌다.  </li>
<li>JPA 사용 방법 및 반복 작업을 줄이는 JPA의 장점을 이해한다.</li>
</ol>
<hr>
<h3 id="1-spring-data-jpa-영속성-컨텍스트">1) Spring Data JPA 영속성 컨텍스트</h3>
<h4 id="영속성-컨텍스트persistence-context란">영속성 컨텍스트(Persistence Context)란?</h4>
<p>영속성 컨텍스트는 <strong>JPA가 엔티티 객체를 관리하고 데이터베이스와 연동하는 가상의 메모리 공간</strong>입니다. ORM, 즉 객체와  RDB 데이터를 자동으로 매핑해주는 기능을 제공하는 JPA에서 아주 핵심적인 녀석이죠. </p>
<p>일종의 <strong>캐시 메모리라고 생각</strong>하시면 됩니다.</p>
<p>먼저 최초로 엔티티에 접근하면, DB에서 끌어온 엔티티를 영속성 컨텍스트에 등록해줍니다. 
이후에 Spring Data JPA에서 <strong><em>Repository 인터페이스</em></strong> 를 통하여 같은 엔티티에 접근하면, DB에 쿼리를 날리는 게 아닌 영속성 컨텍스트에 있는 데이터를 조회하게 되는 거죠.
JPA에서는 테이블과 매핑해주는 엔티티 객체를 영속성 컨텍스트를 통해 애플리케이션 내부에서 오래 지속되도록 돕습니다.
<strong>JPA를 통해 데이터베이스에서 조회한 엔티티는 영속성 컨텍스트에 저장</strong>되고, 그 안에서 문맥적(contextual)으로 하나의 상태를 유지하면서, 여러 기능을 할 수 있게 하죠.
다시 말해, 애플리케이션과 데이터베이스 사이에서 데이터를 저장하는 가상의 데이터베이스 같은 역할을 합니다. </p>
<blockquote>
<p><code>@EntityManager</code>을 이용하여 접근합니다 ‼️</p>
</blockquote>
<p><strong>영속성 컨텍스트는 이처럼 데이터베이스 요청을 최소화해 성능을 높이고, 엔티티 변경 사항을 감지하는 등 다양한 이점을 제공합니다.</strong></p>
<h3 id="영속성-컨텍스트의-주요-장점">영속성 컨텍스트의 주요 장점</h3>
<p>객체가 ‘영속’ 상태이면, 다음과 같이 크게 3가지 장점을 지닙니다.</p>
<blockquote>
<ul>
<li><strong>1차 캐시</strong></li>
</ul>
</blockquote>
<ul>
<li><strong>변경 감지</strong>(Dirty Checking)</li>
<li><strong>지연 로딩</strong>(FetchType.LAZY)</li>
</ul>
<table>
<thead>
<tr>
<th>기능</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>1차 캐시</strong></td>
<td>동일한 엔티티를 조회할 때, DB 쿼리 생략 후 캐시 데이터를 반환합니다.</td>
<td><code>em.find(Member.class, 1L);</code> 캐시에 저장된 데이터 반환</td>
</tr>
<tr>
<td><strong>변경 감지(Dirty Checking)</strong></td>
<td>엔티티 변경 사항을 감지하고 트랜잭션 종료 시 자동으로 DB에 반영합니다.</td>
<td><code>member.setName(&quot;New Name&quot;);</code> → <code>UPDATE member</code> 쿼리 실행</td>
</tr>
<tr>
<td><strong>지연 로딩</strong></td>
<td>연관 데이터를 필요할 때만, 분리하여 DB가 아닌 프록시에서 데이터를 가져옵니다‼️</td>
<td><code>FetchType.LAZY</code> 설정 시, 연관 데이터 접근 시점에 쿼리 실행</td>
</tr>
</tbody></table>
<hr>
<h3 id="즉시-로딩fetchtypeeager-vs-지연-로딩fetchtypelazy">즉시 로딩(FetchType.EAGER) VS 지연 로딩(FetchType.LAZY)</h3>
<h4 id="즉시-로딩fetchtypeeager">즉시 로딩(FetchType.EAGER)</h4>
<p>즉시 로딩은 <strong>연관된 모든 데이터를 한 번에 가져오는 로딩 전략</strong>입니다.  </p>
<ul>
<li><strong>사용 방법</strong>: <code>@ManyToOne(fetch = FetchType.EAGER)</code></li>
<li><strong>장점</strong>: 필요한 데이터를 미리 로딩해, 추가적인 쿼리 없이 사용할 수 있습니다.  </li>
<li><strong>단점</strong>: 연관된 데이터가 많을 경우 <strong>N+1 문제</strong>가 발생할 가능성이 높습니다.  </li>
</ul>
<h4 id="지연-로딩fetchtypelazy">지연 로딩(FetchType.LAZY)</h4>
<p>지연 로딩은 <strong>실제로 데이터가 필요할 때 DB에서 가져오는 로딩 전략</strong>입니다.  </p>
<ul>
<li><strong>사용 방법</strong>: <code>@ManyToOne(fetch = FetchType.LAZY)</code></li>
<li><strong>장점</strong>: 초기 쿼리 수를 최소화해 성능 최적화를 이룰 수 있습니다.  </li>
<li><strong>단점</strong>: 데이터 접근 시마다 추가적인 쿼리가 발생할 수 있어 주의가 필요합니다.  </li>
<li><em>🌟 지연 로딩은 DB가 아닌 프록시에서 데이터를 가져옵니다‼️*</em></li>
</ul>
<p>지연 로딩은 <code>MemberPrefer</code>과 <code>Member</code>, <code>FoodCategory</code>이 각각 분리되어 <code>MemberPrefer</code>은 DB를, <code>Member</code> 및 <code>FoodCategory</code> 은 프록시를 조회합니다.</p>
<pre><code class="language-java">@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class MemberPrefer extends BaseEntity {

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

    **@ManyToOne(fetch = FetchType.LAZY)**
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member;

    **@ManyToOne(fetch = FetchType.LAZY)**
    @JoinColumn(name = &quot;category_id&quot;)
    private FoodCategory foodCategory;

}</code></pre>
<p>즉, MemberPrefer을 조회할 때, Member과 FoodCategory 를 굳이 조회할 필요가 없다면 분리하여 조회하는 지연 로딩 방법을 사용합시다👍</p>
<h3 id="즉시-로딩과-지연-로딩의-비교">즉시 로딩과 지연 로딩의 비교</h3>
<table>
<thead>
<tr>
<th>로딩 전략</th>
<th>특징</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td>** 즉시 로딩(FetchType.EAGER)**</td>
<td>연관된 데이터를 한 번에 가져옴</td>
<td>연관된 데이터 접근 시 추가 쿼리가 발생하지 않음</td>
<td>불필요한 데이터 로딩, N+1 문제 발생 가능</td>
</tr>
<tr>
<td><strong>지연 로딩(FetchType.LAZY)</strong></td>
<td>데이터 접근 시점에 DB에서 가져옴</td>
<td>초기 로딩 속도가 빠르고, 필요 시점에만 데이터를 로딩</td>
<td>데이터 접근 시마다 추가 쿼리 발생</td>
</tr>
</tbody></table>
<hr>
<h3 id="n1-문제란">N+1 문제란?</h3>
<p>즉시 로딩에서 발생할 수 있다는 N+1 문제는 뭘까요?</p>
<h4 id="문제-정의">문제 정의</h4>
<p>N+1 문제는 <strong>하나의 메인 쿼리</strong>로 N개의 데이터를 가져온 뒤, 각 데이터에 대해 <strong>N개의 추가 쿼리</strong>가 실행되는 문제입니다.<br>RDB와 객체 지향의 패러다임 간극에 의해 발생하는 문제인데, 1개의 쿼리를 실행한 후에, 관련된 N개의 데이터를 각각 가져오기 위해 추가적으로 N번의 불필요한 쿼리가 실행되어 성능 저하가 발생합니다.</p>
<h4 id="해결-방법">해결 방법</h4>
<table>
<thead>
<tr>
<th>방법</th>
<th>설명</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Fetch Join</strong></td>
<td>연관 데이터를 한 번의 쿼리로 로딩</td>
<td>N+1 문제 해결 가능, 성능 최적화</td>
<td>복잡한 쿼리에선 성능 저하 가능</td>
</tr>
<tr>
<td><strong>Batch Size</strong></td>
<td>지연 로딩 시 여러 데이터를 한 번에 로딩</td>
<td>추가 쿼리 수를 줄여 N+1 문제 해결</td>
<td>설정에 따라 불필요한 데이터 로딩 가능</td>
</tr>
<tr>
<td><strong>@EntityGraph</strong></td>
<td>특정 필드만 즉시 로딩으로 변경</td>
<td>간결한 설정, 필요 시점에만 즉시 로딩 가능</td>
<td>JPQL 사용 시 비효율적</td>
</tr>
<tr>
<td><strong>QueryDSL</strong></td>
<td>동적 쿼리를 통해 필요한 데이터만 로딩</td>
<td>타입 안전성 보장, 동적 쿼리 작성 가능</td>
<td>설정이 다소 복잡</td>
</tr>
</tbody></table>
<hr>
<h3 id="2-jpql-객체-지향-쿼리-언어">2) JPQL: 객체 지향 쿼리 언어</h3>
<h4 id="jpql이란">JPQL이란?</h4>
<p>JPQL(Java Persistence Query Language)은 <strong>JPA 엔티티 객체를 대상으로 작성하는 쿼리 언어</strong>입니다.  </p>
<ul>
<li>SQL과 유사하지만, 데이터베이스 테이블이 아닌 <strong>엔티티 객체</strong>를 사용하여 쿼리를 작성합니다.  </li>
<li>즉, JPQL은 데이터베이스와 상호작용한다는 기본 컨셉은 유지하되, <strong>객체 지향적인 특성을 반영</strong>하여 작성할 수 있는 것이 가장 큰 특징입니다.  </li>
</ul>
<h3 id="jpql-사용-방법"><strong>JPQL 사용 방법</strong></h3>
<p>JPQL을 사용하는 방법에는 두 가지가 있습니다:  </p>
<blockquote>
<p>[1] <strong>EntityManager 인터페이스</strong><br>[2] <strong>Spring Data JPA의 Repository 인터페이스</strong>  </p>
</blockquote>
<h4 id="1-entitymanager-인터페이스를-통한-jpql-사용">[1] <strong>EntityManager 인터페이스</strong>를 통한 JPQL 사용</h4>
<p><strong>EntityManager란?</strong>  </p>
<ul>
<li>JPA의 핵심 인터페이스로, 엔티티를 관리하고 JPQL 쿼리를 실행하는 데 사용됩니다.</li>
<li><strong>Native SQL</strong>과 병행 사용이 가능하며, 트랜잭션 관리와 엔티티 작업 관리에 탁월합니다.</li>
</ul>
<h5 id="기본-사용-예시">기본 사용 예시</h5>
<pre><code class="language-java">// EntityManager 생성 및 트랜잭션 시작
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();

// JPQL 쿼리 작성
String jpql = &quot;SELECT m FROM Member m WHERE m.name = :name&quot;;
TypedQuery&lt;Member&gt; query = em.createQuery(jpql, Member.class);
query.setParameter(&quot;name&quot;, &quot;Alice&quot;);

// 결과 리스트로 받기
List&lt;Member&gt; members = query.getResultList();
for (Member member : members) {
    System.out.println(&quot;Member Name: &quot; + member.getName());
}

// 트랜잭션 종료
em.getTransaction().commit();
em.close();</code></pre>
<h5 id="주요-메서드">주요 메서드</h5>
<ul>
<li><strong><code>createQuery(String jpql, Class&lt;T&gt; resultClass)</code></strong>: JPQL 쿼리를 작성하고 실행합니다.</li>
<li><strong><code>setParameter(String name, Object value)</code></strong>: JPQL 쿼리에서 사용할 파라미터를 설정합니다.</li>
<li><strong><code>getResultList()</code></strong>: 결과를 리스트로 반환합니다.</li>
<li><strong><code>getSingleResult()</code></strong>: 단일 결과를 반환합니다.</li>
</ul>
<h5 id="업데이트삭제-쿼리-예시">업데이트/삭제 쿼리 예시</h5>
<p>JPQL을 사용하면 업데이트나 삭제 작업도 가능합니다.  </p>
<pre><code class="language-java">em.getTransaction().begin();

// JPQL 업데이트 쿼리
String jpql = &quot;UPDATE Member m SET m.age = :age WHERE m.name = :name&quot;;
Query query = em.createQuery(jpql);
query.setParameter(&quot;age&quot;, 30);
query.setParameter(&quot;name&quot;, &quot;Alice&quot;);

int rowsUpdated = query.executeUpdate(); // 업데이트된 행 수 반환
System.out.println(&quot;Rows updated: &quot; + rowsUpdated);

em.getTransaction().commit();</code></pre>
<hr>
<h4 id="span-stylebackground-colorfff5b12-repository-인터페이스를-통한-jpql-사용span"><span style="background-color:#fff5b1">[2] <strong>Repository 인터페이스</strong>를 통한 JPQL 사용</span></h4>
<p>Spring Data JPA는 <strong>Repository 인터페이스</strong>를 통해 JPQL 쿼리를 간편하게 작성할 수 있습니다.<br>이 방법은 더 직관적이고 간결하며, Spring 환경과 잘 통합됩니다.</p>
<h5 id="메서드-이름-기반-쿼리-생성">메서드 이름 기반 쿼리 생성</h5>
<p>Spring Data JPA는 메서드 이름을 기반으로 JPQL 쿼리를 자동 생성합니다.  </p>
<ul>
<li><strong>예시</strong>: 특정 조건에 맞는 회원을 조회하는 메서드  </li>
</ul>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    List&lt;Member&gt; findByNameAndStatus(String name, MemberStatus status);
}

// 호출
List&lt;Member&gt; activeMembers = memberRepository.findByNameAndStatus(&quot;Alice&quot;, MemberStatus.ACTIVE);</code></pre>
<p><strong>Spring 내부 동작</strong>:  
위 메서드 호출은 다음 JPQL 쿼리로 변환됩니다.  </p>
<pre><code class="language-sql">SELECT m FROM Member m WHERE m.name = &#39;Alice&#39; AND m.status = &#39;ACTIVE&#39;;</code></pre>
<h5 id="query-어노테이션을-통한-jpql-작성">@Query 어노테이션을 통한 JPQL 작성</h5>
<p>더 복잡한 쿼리가 필요할 경우, <strong><code>@Query</code> 어노테이션</strong>을 사용해 직접 JPQL을 작성할 수 있습니다.  </p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    @Query(&quot;SELECT m FROM Member m WHERE m.name = :name AND m.status = :status&quot;)
    List&lt;Member&gt; findByNameAndStatus(@Param(&quot;name&quot;) String name, @Param(&quot;status&quot;) MemberStatus status);
}</code></pre>
<p><strong>장점</strong>  </p>
<ul>
<li>간결한 코드로 복잡한 JPQL 작성 가능.  </li>
<li>메서드 이름 기반 쿼리 생성보다 더 높은 유연성 제공.  </li>
</ul>
<h4 id="jpql과-sql의-차이점">JPQL과 SQL의 차이점</h4>
<table>
<thead>
<tr>
<th>구분</th>
<th>JPQL</th>
<th>SQL</th>
</tr>
</thead>
<tbody><tr>
<td><strong>대상</strong></td>
<td>엔티티 객체를 대상으로 작성</td>
<td>테이블을 대상으로 작성</td>
</tr>
<tr>
<td><strong>언어</strong></td>
<td>객체 지향적 문법 반영</td>
<td>관계형 데이터베이스 중심의 문법</td>
</tr>
<tr>
<td><strong>예시</strong></td>
<td><code>SELECT m FROM Member m</code></td>
<td><code>SELECT * FROM member</code></td>
</tr>
<tr>
<td><strong>사용 환경</strong></td>
<td>JPA 환경에서 사용</td>
<td>JPA 외에 모든 SQL 환경에서 사용 가능</td>
</tr>
</tbody></table>
<h4 id="jpql-사용-시-주의사항">JPQL 사용 시 주의사항</h4>
<ol>
<li><p><strong>엔티티 필드 이름을 사용</strong>  </p>
<ul>
<li>JPQL은 엔티티의 필드 이름을 사용하므로, 테이블 컬럼 이름이 아니라 엔티티의 필드 이름과 일치해야 합니다.  </li>
<li>예: <code>m.name</code>은 엔티티의 <code>name</code> 필드를 참조.</li>
</ul>
</li>
<li><p><strong>Lazy Loading과의 연관성</strong>  </p>
<ul>
<li>지연 로딩이 설정된 엔티티를 JPQL로 조회할 경우, 필요 시점에 추가 쿼리가 발생할 수 있습니다.</li>
</ul>
</li>
<li><p><strong>동적 쿼리 작성</strong>  </p>
<ul>
<li>복잡한 조건을 가진 동적 쿼리를 작성해야 할 경우, <strong>QueryDSL</strong>과 같은 도구를 사용하는 것이 더 효율적일 수 있습니다.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="3-querydsl-동적-쿼리를-위한-강력한-도구">3) QueryDSL: 동적 쿼리를 위한 강력한 도구</h3>
<p>JPQL 사용으로는 작성하기 너무 복잡한 동적 쿼리를 위해서는 QueryDSL를 사용하는 게 효과적인 방법입니다. </p>
<h4 id="querydsl이란">QueryDSL이란?</h4>
<p>QueryDSL은 <strong>코드 기반의 쿼리 빌더 라이브러리</strong>로, <strong>타입 안전성</strong>과 <strong>동적 쿼리 작성</strong>에 강점을 가진 도구입니다.</p>
<ul>
<li><strong>타입 안전성 보장</strong>: 컴파일 시점에 쿼리 오류를 잡을 수 있어 안전합니다.</li>
<li><strong>동적 쿼리 작성</strong>: 조건이나 구조가 실행 시점에 유동적으로 변경될 수 있는 쿼리를 쉽게 작성할 수 있습니다.</li>
<li><strong>메서드 체이닝</strong>: 조건과 동작을 체계적으로 연결해 직관적이고 간결한 쿼리 작성이 가능합니다.</li>
</ul>
<h3 id="querydsl-사용법">QueryDSL 사용법</h3>
<h4 id="1️⃣-querydsl-기본-설정"><strong>1️⃣ QueryDSL 기본 설정</strong></h4>
<p>QueryDSL을 사용하려면 <strong>Q 클래스를 자동 생성</strong>해야 합니다. 이를 위해 <code>build.gradle</code> 파일에 다음 설정을 추가합니다.</p>
<pre><code class="language-groovy">plugins {
    id &quot;com.ewerk.gradle.plugins.querydsl&quot; version &quot;1.0.10&quot;
}

dependencies {
    implementation &#39;com.querydsl:querydsl-jpa:5.0.0:jakarta&#39;
    annotationProcessor &quot;com.querydsl:querydsl-apt:5.0.0:jakarta&quot;
}
sourceSets {
    main.java.srcDir &#39;src/main/generated&#39;
}</code></pre>
<h4 id="2️⃣-q-클래스-생성"><strong>2️⃣ Q 클래스 생성</strong></h4>
<p><code>Q</code> 클래스는 각 엔티티를 기준으로 자동 생성됩니다.<br><code>src/main/generated</code> 폴더에 생성된 <code>QMember</code> 또는 <code>QStore</code> 등을 사용하여 QueryDSL로 쿼리를 작성합니다.</p>
<h4 id="3️⃣-jpaqueryfactory-설정"><strong>3️⃣ JPAQueryFactory 설정</strong></h4>
<p>QueryDSL은 <code>JPAQueryFactory</code>를 사용해 쿼리를 작성합니다.<br><code>JPAQueryFactory</code>를 Bean으로 등록하려면 다음과 같은 설정이 필요합니다.</p>
<pre><code class="language-java">@Configuration
@RequiredArgsConstructor
public class QueryDSLConfig {
    private final EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}</code></pre>
<h4 id="4️⃣-querydsl로-동적-쿼리-작성"><strong>4️⃣ QueryDSL로 동적 쿼리 작성</strong></h4>
<h5 id="booleanbuilder-활용">BooleanBuilder 활용</h5>
<p><code>BooleanBuilder</code>를 사용해 조건을 동적으로 추가할 수 있습니다.</p>
<pre><code class="language-java">private List&lt;Store&gt; dynamicQueryWithBooleanBuilder(String name, Float score) {
    BooleanBuilder predicate = new BooleanBuilder();

    if (name != null) {
        predicate.and(store.name.eq(name));
    }
    if (score != null) {
        predicate.and(store.score.goe(score));
    }

    return jpaQueryFactory
        .selectFrom(store)
        .where(predicate)
        .fetch();
}</code></pre>
<ul>
<li><strong><code>.and()</code></strong>: 조건 추가</li>
<li><strong><code>.or()</code></strong>: 조건 선택적으로 추가</li>
</ul>
<h5 id="repository-인터페이스-구성">Repository 인터페이스 구성</h5>
<ol>
<li><p><strong>Custom Interface 생성</strong></p>
<pre><code class="language-java">public interface StoreRepositoryCustom {
    List&lt;Store&gt; dynamicQueryWithBooleanBuilder(String name, Float score);
}</code></pre>
</li>
<li><p><strong>Custom 구현체 작성</strong></p>
<pre><code class="language-java">@Repository
@RequiredArgsConstructor
public class StoreRepositoryImpl implements StoreRepositoryCustom {
    private final JPAQueryFactory jpaQueryFactory;
    private final QStore store = QStore.store;

    @Override
    public List&lt;Store&gt; dynamicQueryWithBooleanBuilder(String name, Float score) {
        BooleanBuilder predicate = new BooleanBuilder();

        if (name != null) predicate.and(store.name.eq(name));
        if (score != null) predicate.and(store.score.goe(score));

        return jpaQueryFactory
            .selectFrom(store)
            .where(predicate)
            .fetch();
    }
}</code></pre>
</li>
<li><p><strong>Repository 연결</strong></p>
<pre><code class="language-java">public interface StoreRepository extends JpaRepository&lt;Store, Long&gt;, StoreRepositoryCustom {
}</code></pre>
</li>
<li><p><strong>Service Layer에서 호출</strong></p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class StoreQueryServiceImpl implements StoreQueryService {
    private final StoreRepository storeRepository;

    public List&lt;Store&gt; findStoresByNameAndScore(String name, Float score) {
        return storeRepository.dynamicQueryWithBooleanBuilder(name, score);
    }
}</code></pre>
</li>
</ol>
<hr>
<h3 id="querydsl의-장단점">QueryDSL의 장단점</h3>
<table>
<thead>
<tr>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td><strong>타입 안전성 보장</strong></td>
<td>초기 설정 및 학습 곡선 필요</td>
</tr>
<tr>
<td><strong>동적 쿼리 작성 간편</strong></td>
<td>QueryDSL 라이브러리 의존성 추가 필요</td>
</tr>
<tr>
<td><strong>메서드 체이닝으로 직관적 쿼리 작성 가능</strong></td>
<td>설정 과정이 다소 복잡할 수 있음</td>
</tr>
</tbody></table>
<hr>
<h3 id="실습을-통한-querydsl-요약">실습을 통한 QueryDSL 요약</h3>
<ol>
<li><p><strong>QueryDSL 설정</strong></p>
<ul>
<li><code>build.gradle</code>에 종속성 추가</li>
<li><code>Q</code> 클래스 자동 생성 설정</li>
</ul>
</li>
<li><p><strong><code>JPAQueryFactory</code> Bean 등록</strong></p>
<ul>
<li><code>EntityManager</code>와 연동</li>
</ul>
</li>
<li><p><strong>Repository 구성</strong></p>
<ul>
<li><code>Custom Interface</code>와 <code>Custom 구현체</code> 작성</li>
<li>동적 쿼리를 작성해 비즈니스 로직 적용</li>
</ul>
</li>
<li><p><strong>Service Layer에서 활용</strong></p>
<ul>
<li>동적 조건에 따라 유연하게 데이터를 조회하고 비즈니스 로직에 적용</li>
</ul>
</li>
</ol>
<hr>
<h3 id="정리">정리</h3>
<ol>
<li><strong>즉시 로딩(FetchType.EAGER)</strong>은 N+1 문제를 일으킬 수 있으므로, <strong>지연 로딩(FetchType.LAZY)</strong>을 기본으로 설정.</li>
<li>JPQL은 객체 지향 쿼리 언어로 SQL보다 객체 매핑에 적합.</li>
<li>QueryDSL은 동적 쿼리를 작성할 때 강력한 도구로 활용 가능.</li>
</ol>
<p>```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 5. Spring Boot와 JPA로 Entity 설계하기]]></title>
            <link>https://velog.io/@funda__mental_/Chapter-5.-Spring-Boot%EC%99%80-JPA%EB%A1%9C-Entity-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@funda__mental_/Chapter-5.-Spring-Boot%EC%99%80-JPA%EB%A1%9C-Entity-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 26 Dec 2024 06:35:24 GMT</pubDate>
            <description><![CDATA[<p>지난주에는 데이터베이스 설계를 위한 ERD를 완성하며 데이터 구조를 정의했습니다.<br>이번 주차에서는 이 데이터를 활용하기 위한 <strong>Spring Boot와 JPA를 사용한 엔티티 설계</strong>를 다룹니다.  </p>
<p>Spring Boot와 JPA를 통해 데이터를 쉽게 저장하고 조회하며, 프로젝트의 비즈니스 로직을 깔끔하게 구현할 수 있습니다.  </p>
<hr>
<h2 id="학습-목표-🎯">학습 목표 🎯</h2>
<p>1️⃣ JPA의 개념과 필요성을 이해합니다.<br>2️⃣ Spring Data JPA를 사용하여 ERD 기반으로 <strong>엔티티를 설계하고 매핑</strong>합니다.</p>
<hr>
<h2 id="🌟-1-jpa와-spring-data-jpa란">🌟 1. JPA와 Spring Data JPA란?</h2>
<p><strong>객체 지향 프로그래밍</strong>의 목표는 캡슐화, 상속, 다형성을 활용하여 데이터를 관리하는 것이고,<br><strong>RDBMS</strong>는 데이터를 정교하게 정규화하여 저장하는 것이 목표입니다.  </p>
<p>이 패러다임 차이로 인해 아래와 같은 문제가 발생합니다:<br>1️⃣ 데이터를 SQL로 저장하고,<br>2️⃣ SQL 결과를 자바 객체로 변환해야 하는 번거로움.  </p>
<p><strong>해결책</strong>  </p>
<ul>
<li><strong>JPA와 Spring Data JPA</strong>를 사용하여 반복 작업을 줄이고, 비즈니스 로직에만 집중합니다.  </li>
</ul>
<h3 id="span-stylebackground-colorfff5b1jpajava-persistence-apispan란"><span style="background-color:#fff5b1">JPA(Java Persistence API)</span>란?</h3>
<p>JPA는 <strong>Java 진영에서 ORM(Object Relational Mapping) 기술의 표준 인터페이스</strong>입니다.<br>쉽게 말해, <strong>애플리케이션의 객체를 관계형 데이터베이스(RDBMS) 테이블에 매핑</strong>해주는 기술입니다. <strong>객체 지향적 언어인 자바</strong>는 궁극적으로 <strong>캡슐화/상속/다형성을 활용하는 것이 목표</strong>이고, <strong>RDBMS(관계형 데이터베이스)</strong>는 <strong>데이터를 정교하게 구성하는 것이 목표</strong>입니다. 두 언어가 목표가 다르기 때문에 원래는 개발자가 이를 하나하나 Mapping 해줘야 합니다. 이를 해결하기 위해 JPA가 사용됩니다!</p>
<h4 id="ormobject-relational-mapping이란">ORM(Object Relational Mapping)이란?</h4>
<p><strong>객체 지향 언어와 관계형 데이터베이스 간의 불일치를 해결</strong>하는 기술입니다. 기술적인 측면에서는 애플리케이션의 객체를 RDB 클래스에 자동으로 연결시켜준다고 보시면 됩니다.
즉, <strong>반복적인 CRUD SQL 쿼리문을 굳이 짤 필요 없이 자동으로 매핑하여 날려준다</strong>고 생각하시면 됩니다👍👍  </p>
<ul>
<li><strong>객체</strong>: 자바에서 클래스를 통해 데이터를 관리.  </li>
<li><strong>RDBMS</strong>: 테이블과 컬럼을 기반으로 데이터를 저장.  </li>
</ul>
<blockquote>
<p>JPA는 SQL 작성 없이도 객체와 테이블 간의 변환을 자동으로 처리하여 개발자의 생산성을 높입니다.</p>
</blockquote>
<h3 id="🛠️-spring-data-jpa란">🛠️ Spring Data JPA란?</h3>
<p>Spring Data JPA는 <strong>JPA를 더 간편하게 사용할 수 있도록 지원하는 스프링 모듈</strong>입니다.  </p>
<ul>
<li><strong>CRUD 쿼리 자동 생성</strong>: 단순한 쿼리는 메서드 선언만으로 자동 생성.  </li>
<li><strong>JPQL 지원</strong>: 복잡한 쿼리도 쉽게 작성 가능.  </li>
<li><strong>Repository 기반 개발</strong>: 데이터베이스와의 상호작용을 추상화.  </li>
</ul>
<hr>
<h2 id="✏️-2-프로젝트-구조-설계">✏️ 2. 프로젝트 구조 설계</h2>
<h3 id="🏗️-프로젝트-디렉토리-구조">🏗️ 프로젝트 디렉토리 구조</h3>
<p>Spring Boot 프로젝트는 아래와 같은 디렉토리 구조를 권장합니다.<br>이 디렉토리 구조는 <strong>모듈화</strong>와 <strong>유지보수성</strong>을 높이기 위해 설계되었습니다.  </p>
<pre><code>src/main/java
│
├── controller        // HTTP 요청/응답 처리
├── service           // 비즈니스 로직
├── repository        // DB 접근 로직
├── domain            // Entity 클래스
├── dto               // 데이터 전송 객체
└── base              // 공통 클래스 (e.g., BaseEntity)</code></pre><h3 id="디렉토리-역할-설명">디렉토리 역할 설명</h3>
<ol>
<li><p><strong>controller</strong>  </p>
<ul>
<li>사용자의 요청을 받아 처리하고, 결과를 응답으로 반환하는 계층입니다.  </li>
<li>Spring MVC의 핵심 구성요소로 <code>@RestController</code> 또는 <code>@Controller</code>를 주로 사용합니다.  </li>
<li>예: 로그인 요청, 회원가입 요청 등을 처리.</li>
</ul>
</li>
<li><p><strong>service</strong>  </p>
<ul>
<li>비즈니스 로직을 처리하는 계층입니다.  </li>
<li>Controller에서 받은 요청을 기반으로 데이터를 가공하거나, 여러 Repository를 조합해 복잡한 작업을 처리합니다.  </li>
<li>예: 회원 가입 시 이메일 중복 체크와 데이터 저장 처리.</li>
</ul>
</li>
<li><p><strong>repository</strong>  </p>
<ul>
<li>데이터베이스와의 직접적인 상호작용을 담당하는 계층입니다.  </li>
<li>Spring Data JPA의 <code>JpaRepository</code>를 확장해 사용하며, CRUD 작업을 간편하게 처리할 수 있습니다.</li>
</ul>
</li>
<li><p><strong>domain</strong>  </p>
<ul>
<li>데이터베이스의 테이블과 1:1로 매핑되는 Entity 클래스들이 위치합니다.  </li>
<li>JPA에서 사용되는 주요 어노테이션(<code>@Entity</code>, <code>@Id</code>, <code>@GeneratedValue</code> 등)을 사용합니다.  </li>
<li>예: <code>User</code>, <code>Order</code>, <code>Product</code> 등 주요 데이터를 표현.</li>
</ul>
</li>
<li><p><strong>dto</strong>  </p>
<ul>
<li>클라이언트와의 데이터 교환을 위한 객체(Data Transfer Object)입니다.  </li>
<li>Entity와 달리 특정 요청이나 응답에 필요한 데이터만 포함합니다.  </li>
<li>Entity 변경 시 클라이언트에 영향을 주지 않도록 데이터 전달을 추상화하는 역할을 합니다.</li>
</ul>
</li>
<li><p><strong>base</strong>  </p>
<ul>
<li>모든 Entity에서 공통으로 사용할 필드나 기능을 정의합니다.  </li>
<li>예: 생성일(<code>created_at</code>), 수정일(<code>updated_at</code>) 등을 처리하는 <code>BaseEntity</code>.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="1️⃣-entity-설계-및-매핑">1️⃣ Entity 설계 및 매핑</h3>
<h4 id="엔티티entity란">엔티티(Entity)란?</h4>
<ul>
<li>엔티티는 데이터베이스의 <strong>테이블과 1:1로 매핑</strong>되는 클래스입니다.  </li>
<li>JPA를 사용하면 엔티티 클래스를 통해 SQL 없이 데이터베이스 작업을 수행할 수 있습니다.  </li>
<li>Entity 클래스는 주로 <strong>데이터의 상태</strong>와 <strong>관계</strong>를 표현합니다.</li>
</ul>
<hr>
<h3 id="erd-기반-설계">ERD 기반 설계</h3>
<h4 id="erdentity-relationship-diagram란">ERD(Entity Relationship Diagram)란?</h4>
<ul>
<li><strong>ERD</strong>는 데이터베이스 테이블 간의 관계를 시각적으로 표현한 다이어그램입니다.  </li>
<li>JPA를 사용하여 엔티티를 설계하기 전, ERD를 기반으로 테이블 구조를 정의합니다.  </li>
<li>예제 ERD:<br><img src="https://velog.velcdn.com/images/funda__mental_/post/a4d35890-a3fa-4c60-8927-dd3617126e50/image.png" alt=""></li>
</ul>
<hr>
<h4 id="member-엔티티-예제">Member 엔티티 예제</h4>
<pre><code class="language-java">@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Member extends BaseEntity {

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

    @Column(nullable = false, length = 20)
    private String name;

    @Enumerated(EnumType.STRING)
    private Gender gender;

    @Column(nullable = false)
    private String email;
}</code></pre>
<p>*<em>주요 어노테이션 설명  *</em></p>
<ol>
<li><p><strong>@Entity</strong>  </p>
<ul>
<li>JPA가 관리할 클래스임을 명시.  </li>
<li>데이터베이스 테이블로 매핑됩니다.  </li>
</ul>
</li>
<li><p><strong>@Id</strong>  </p>
<ul>
<li>기본 키(primary key)를 설정합니다.  </li>
<li>모든 엔티티 클래스에는 반드시 하나의 기본 키가 있어야 합니다.</li>
</ul>
</li>
<li><p><strong>@GeneratedValue</strong>  </p>
<ul>
<li>기본 키의 값을 자동으로 생성하도록 설정합니다.  </li>
<li><code>GenerationType.IDENTITY</code>: MySQL의 AUTO_INCREMENT처럼 자동 증가하도록 설정.</li>
</ul>
</li>
<li><p><strong>@Column</strong>  </p>
<ul>
<li>테이블 컬럼의 속성을 설정합니다.  </li>
<li>예: <code>nullable = false</code>로 필수 입력 값을 설정하거나, <code>length</code>로 문자열 길이를 제한.</li>
</ul>
</li>
<li><p><strong>@Enumerated</strong>  </p>
<ul>
<li>Enum 타입을 데이터베이스에 저장하기 위한 설정입니다.  </li>
<li><code>EnumType.STRING</code>: Enum 값을 문자열로 저장합니다.  </li>
<li><code>EnumType.ORDINAL</code>은 Enum의 순서를 저장하지만, 값 변경 시 오류가 발생할 수 있으므로 사용하지 않습니다.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="2️⃣baseentity로-공통-필드-처리">2️⃣BaseEntity로 공통 필드 처리</h3>
<h4 id="공통-필드-처리-예제">공통 필드 처리 예제</h4>
<p>모든 테이블에서 공통적으로 사용하는 생성일자와 수정일자를 관리하기 위해 <code>BaseEntity</code>를 작성합니다.  </p>
<pre><code class="language-java">@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class BaseEntity {

    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}</code></pre>
<p>*<em>주요 어노테이션 설명  *</em></p>
<ol>
<li><p><strong>@MappedSuperclass</strong>  </p>
<ul>
<li>이 클래스의 필드를 상속받는 엔티티에서 사용할 수 있도록 설정합니다.  </li>
</ul>
</li>
<li><p><strong>@EntityListeners(AuditingEntityListener.class)</strong>  </p>
<ul>
<li>Spring Data JPA에서 제공하는 감사(Auditing) 기능을 활성화합니다.  </li>
</ul>
</li>
<li><p><strong>@CreatedDate, @LastModifiedDate</strong>  </p>
<ul>
<li>데이터 생성 및 수정 시간을 자동으로 기록합니다.  </li>
</ul>
</li>
</ol>
<hr>
<h3 id="3️⃣-연관관계-매핑">3️⃣ 연관관계 매핑</h3>
<h4 id="단방향-연관관계-member와-memberprefer">단방향 연관관계: Member와 MemberPrefer</h4>
<pre><code class="language-java">@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class MemberPrefer extends BaseEntity {

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member;
}</code></pre>
<p>*<em>주요 어노테이션 설명  *</em></p>
<ol>
<li><p><strong>@ManyToOne</strong>  </p>
<ul>
<li>다대일(N:1) 관계를 설정합니다.  </li>
<li>예: 여러 <code>MemberPrefer</code>가 하나의 <code>Member</code>에 연결됩니다.  </li>
</ul>
</li>
<li><p><strong>@JoinColumn</strong>  </p>
<ul>
<li>외래 키 이름을 명시합니다.  </li>
</ul>
</li>
<li><p><strong>fetch = FetchType.LAZY</strong>  </p>
<ul>
<li>지연 로딩 설정: 필요한 시점에 연관된 데이터를 가져옵니다.  </li>
<li>성능 최적화를 위해 기본값인 <code>LAZY</code>를 사용합니다.  </li>
</ul>
</li>
</ol>
<hr>
<h3 id="span-stylebackground-colorfff5b1양방향-연관관계-설정"><span style="background-color:#fff5b1">양방향 연관관계 설정</h3>
<p>양방향 연관관계는 <strong>객체 지향적인 설계</strong>에서 중요한 역할을 합니다.<br>데이터베이스 설계에서는 단방향 연관관계만으로도 모든 데이터 처리가 가능하지만,<br>JPA에서는 양방향 연관관계를 설정함으로써 추가적인 이점을 얻을 수 있습니다.  </p>
<h4 id="양방향-연관관계란">양방향 연관관계란?</h4>
<p>  양방향 매핑은 <strong>두 엔티티가 서로를 참조</strong>하는 관계를 의미합니다.  </p>
<ul>
<li>예: <code>Member</code> 엔티티는 <code>MemberPrefer</code>를 참조하고,  </li>
<li><code>MemberPrefer</code> 엔티티는 다시 <code>Member</code>를 참조.  </li>
</ul>
<p>이러한 구조를 통해 데이터베이스의 데이터를 JPA가 관리하는 <strong>객체 그래프 탐색</strong>을 쉽게 할 수 있습니다.<br>즉, <strong>Member</strong> 객체를 통해 관련된 <strong>MemberPrefer</strong> 객체들을 탐색하거나,<br>반대로 <strong>MemberPrefer</strong>에서 <strong>Member</strong>를 탐색할 수 있습니다.  </p>
<h4 id="양방향-매핑의-장점">양방향 매핑의 장점</h4>
<p>1️⃣ <strong>객체 그래프 탐색 가능</strong>  </p>
<ul>
<li>예: <code>member.getMemberPreferList()</code>와 같이 한쪽 엔티티에서 관련된 엔티티들을 쉽게 탐색할 수 있습니다.  </li>
<li>객체 지향적인 코드 작성이 가능하며, 데이터 간 관계를 명확히 표현할 수 있습니다.  </li>
</ul>
<p>2️⃣ <strong>Cascade 설정 가능</strong>  </p>
<ul>
<li>Cascade는 <strong>부모 엔티티의 변경이 자식 엔티티에도 자동으로 적용</strong>되도록 설정하는 기능입니다.  </li>
<li>예: <code>Member</code> 삭제 시, 연관된 <code>MemberPrefer</code> 엔티티도 자동으로 삭제되도록 설정할 수 있습니다.  </li>
</ul>
<p>3️⃣ <strong>편리한 데이터 삭제 및 관리</strong>  </p>
<ul>
<li>양방향 매핑이 없으면, 연관된 데이터를 수동으로 삭제하거나 관리해야 하는 번거로움이 있습니다.  </li>
<li>Cascade 설정을 통해 부모와 자식 엔티티 간 데이터 관리의 편의성을 높입니다.  </li>
</ul>
<h4 id="cascade와-연관관계의-주인-문제-💥">Cascade와 연관관계의 주인 문제 💥</h4>
<p>데이터베이스에서는 <strong>Cascade</strong> 설정이 외래키(Foreign Key)를 소유한 테이블(연관관계의 주인)에 적용됩니다.<br>하지만 JPA에서는 <strong>Cascade 설정을 연관관계의 주인이 아닌 엔티티에서 설정</strong>해야 합니다.  </p>
<p>이 점에서 JPA의 Cascade 설정은 <strong>단방향 매핑만으로는 올바르게 동작하지 않을 수 있는 문제</strong>를 포함하고 있습니다.  </p>
<p>단방향 매핑 상태에서 Cascade를 설정할 경우:  </p>
<ul>
<li><strong>참조를 하는 쪽(자식 엔티티)</strong>에서 Cascade가 잘못 동작하여,<br>자식 엔티티 삭제 시 부모 엔티티가 함께 삭제되는 문제가 발생할 수 있습니다.  </li>
</ul>
<p>예를 들어:  </p>
<ul>
<li><strong><code>Member</code></strong>를 참조하는 <strong><code>MemberPrefer</code></strong>가 삭제될 때,  </li>
<li><strong><code>Member</code></strong>도 함께 삭제되는 의도치 않은 상황이 발생할 수 있습니다.  </li>
</ul>
<hr>
<h4 id="올바른-cascade-적용을-위한-양방향-매핑">올바른 Cascade 적용을 위한 양방향 매핑</h4>
<p>양방향 매핑을 사용하면 JPA가 연관관계의 전체 흐름을 명확히 이해하고, Cascade를 올바르게 처리할 수 있습니다.  </p>
<pre><code class="language-java">@Entity
public class Member {

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

    @OneToMany(mappedBy = &quot;member&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
    private List&lt;MemberPrefer&gt; memberPreferList = new ArrayList&lt;&gt;();

    public void addMemberPrefer(MemberPrefer memberPrefer) {
        memberPrefer.setMember(this);
        memberPreferList.add(memberPrefer);
    }

    public void removeMemberPrefer(MemberPrefer memberPrefer) {
        memberPreferList.remove(memberPrefer);
        memberPrefer.setMember(null);
    }
}</code></pre>
<ol>
<li><p><strong>Cascade 설정</strong>  </p>
<ul>
<li>부모 엔티티(<code>Member</code>) 삭제 시 자식 엔티티(<code>MemberPrefer</code>)도 함께 삭제됩니다.  </li>
<li><strong>CascadeType.ALL</strong>: 부모 엔티티의 모든 변경사항(저장, 삭제 등)이 자식 엔티티에도 적용됩니다.  </li>
</ul>
</li>
<li><p><strong>Orphan Removal 설정</strong>  </p>
<ul>
<li>부모와의 관계가 끊어진 자식 엔티티를 자동으로 삭제합니다.  </li>
<li>예: <code>memberPreferList.remove(memberPrefer)</code>를 호출하면 해당 자식 엔티티가 삭제됩니다.  </li>
</ul>
</li>
<li><p><strong>연관관계 편의 메서드 추가</strong>  </p>
<ul>
<li>양방향 매핑에서는 연관관계 편의 메서드를 추가하여 양쪽 엔티티의 상태를 동기화해야 합니다.  </li>
<li>위 예제에서 <code>addMemberPrefer()</code>와 <code>removeMemberPrefer()</code>가 이에 해당합니다.  </li>
<li>예: <code>member.addMemberPrefer(memberPrefer)</code>를 호출하면,<br><code>memberPrefer.setMember(member)</code>도 자동으로 호출되어 양쪽 관계가 일치하게 됩니다.  </li>
</ul>
</li>
</ol>
<hr>
<h4 id="양방향-매핑-시-주의점">양방향 매핑 시 주의점</h4>
<p>1️⃣ <strong>무한 루프 방지</strong>  </p>
<ul>
<li>양방향 매핑에서는 한쪽에서 다른 쪽을 참조하는 형태로 무한 루프가 발생할 수 있습니다.  </li>
<li>예: <code>Member</code> → <code>MemberPrefer</code> → 다시 <code>Member</code>로 계속 참조.  </li>
<li>해결 방법: <strong><code>@JsonIgnore</code></strong> 또는 DTO를 사용해 필요 이상의 데이터 참조를 차단합니다.  </li>
</ul>
<pre><code class="language-java">@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = &quot;member_id&quot;)
@JsonIgnore
private Member member;</code></pre>
<p>2️⃣ <strong>Cascade 설정 남용 금지</strong>  </p>
<ul>
<li>Cascade는 부모-자식 관계에서만 사용하는 것이 바람직합니다.  </li>
<li>서로 독립적인 엔티티 간에 Cascade를 설정하면 데이터 누락이나 불필요한 삭제가 발생할 수 있습니다.  </li>
</ul>
<p>3️⃣ <strong>비즈니스 로직과의 협의</strong>  </p>
<ul>
<li>특정 연관관계를 Cascade로 처리할지 여부는 기획 단계에서 결정해야 합니다.  </li>
<li>예: 회원 탈퇴 시 회원의 모든 게시글을 삭제할지 남겨둘지 기획자와 논의 필요.  <ul>
<li>에브리타임 같은 서비스에서는 탈퇴해도 게시글이 삭제되지 않는 경우가 많습니다.  </li>
</ul>
</li>
</ul>
<hr>
<h3 id="3️⃣-칼럼-별-세부적인-설정">3️⃣ 칼럼 별 세부적인 설정</h3>
<p>엔티티의 각 멤버 변수는 데이터베이스의 컬럼과 매핑되며, 이때 <strong>컬럼의 속성</strong>을 정확히 정의해야 합니다.<br>이를 통해 JPA가 테이블을 올바르게 생성하고 관리할 수 있습니다.  </p>
<h4 id="1-칼럼-설정의-필요성">1) 칼럼 설정의 필요성</h4>
<p>기본적으로 JPA는 엔티티 필드의 타입과 속성을 기반으로 데이터베이스 컬럼을 생성합니다.<br>예를 들어, <code>String</code> 타입의 필드는 데이터베이스에서 <code>VARCHAR</code>로 매핑됩니다.<br>하지만, 필드의 길이, 유니크 여부, 기본값 등 <strong>추가적인 제약 조건</strong>은 명시적으로 설정해야 합니다.  </p>
<ul>
<li><strong>문제점</strong><br>JPA를 사용할 때 별도의 설정 없이 <code>String name</code> 필드를 생성하면,<br>기본적으로 데이터베이스에서는 <code>VARCHAR(255)</code>로 처리됩니다.<br>하지만, 실제 요구사항에서는 <strong><code>name</code> 칼럼의 최대 길이가 20자</strong>로 제한되어야 할 수 있습니다.  </li>
</ul>
<h4 id="2-column-어노테이션"><strong>2) @Column 어노테이션</strong></h4>
<p>JPA에서는 <strong><code>@Column</code> 어노테이션</strong>을 사용하여 칼럼의 속성을 상세히 정의할 수 있습니다.  </p>
<pre><code class="language-java">@Column(
    name = &quot;name&quot;,            // 데이터베이스 컬럼명 (기본값: 필드명과 동일)
    nullable = false,         // NOT NULL 제약 조건
    unique = true,            // UNIQUE 제약 조건
    length = 20               // VARCHAR 길이 제한
)
private String name;</code></pre>
<h4 id="3-주요-속성">3) 주요 속성</h4>
<p>1️⃣ <strong>name</strong>  </p>
<ul>
<li>데이터베이스 컬럼명을 지정합니다.  </li>
<li>기본값은 필드명과 동일하지만, 필요에 따라 별도의 이름을 지정할 수 있습니다.  <pre><code class="language-java">@Column(name = &quot;member_name&quot;)
private String name;</code></pre>
</li>
</ul>
<p>2️⃣ <strong>nullable</strong>  </p>
<ul>
<li>해당 필드가 <code>NULL</code>을 허용하는지 여부를 설정합니다.  </li>
<li>기본값은 <code>true</code>로 설정되며, <code>nullable = false</code>로 지정하면 데이터베이스에 <code>NOT NULL</code> 제약 조건이 추가됩니다.  <pre><code class="language-java">@Column(nullable = false)
private String email;  // NOT NULL</code></pre>
</li>
</ul>
<p>3️⃣ <strong>unique</strong>  </p>
<ul>
<li>해당 컬럼에 고유 제약 조건(UNIQUE)을 추가합니다.  </li>
<li>동일한 값이 중복 저장되지 않도록 보장합니다.  <pre><code class="language-java">@Column(unique = true)
private String email;  // UNIQUE</code></pre>
</li>
</ul>
<p>4️⃣ <strong>length</strong>  </p>
<ul>
<li>문자열 필드의 최대 길이를 지정합니다.  </li>
<li>데이터베이스에서 <code>VARCHAR</code> 또는 <code>CHAR</code> 타입의 길이를 제한하는 역할을 합니다.  <pre><code class="language-java">@Column(length = 50)
private String address;</code></pre>
</li>
</ul>
<p>5️⃣ <strong>columnDefinition</strong>  </p>
<ul>
<li>SQL을 직접 작성하여 데이터베이스 컬럼 정의를 세밀하게 조정할 수 있습니다.  </li>
<li>기본값을 설정하거나 데이터 타입을 명시적으로 지정할 때 유용합니다.  <pre><code class="language-java">@Column(columnDefinition = &quot;VARCHAR(20) DEFAULT &#39;UNKNOWN&#39;&quot;)
private String status;</code></pre>
</li>
</ul>
<p>6️⃣ <strong>insertable, updatable</strong>  </p>
<ul>
<li>해당 칼럼이 <code>INSERT</code> 또는 <code>UPDATE</code> 쿼리에 포함될지 여부를 설정합니다.  <pre><code class="language-java">@Column(insertable = false, updatable = false)
private LocalDateTime createdAt;  // 자동 생성/수정 필드로 직접 변경 불가</code></pre>
</li>
</ul>
<hr>
<h4 id="예제-member-엔티티의-세부-설정">예제: Member 엔티티의 세부 설정</h4>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Member {

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

    @Column(nullable = false, length = 20)
    private String name;  // 이름은 반드시 입력해야 하며, 최대 길이는 20자

    @Column(nullable = false, unique = true)
    private String email;  // 이메일은 고유하고 반드시 입력해야 함

    @Column(columnDefinition = &quot;VARCHAR(10) DEFAULT &#39;ACTIVE&#39;&quot;)
    private String status;  // 기본값은 &#39;ACTIVE&#39;

    @Column(updatable = false)
    private LocalDateTime createdAt;  // 생성 시에만 값 설정 가능

    @Column(nullable = true)
    private String phoneNumber;  // 전화번호는 입력하지 않아도 됨
}</code></pre>
<h4 id="결과적으로-생성되는-테이블-스키마-mysql-기준">결과적으로 생성되는 테이블 스키마 (MySQL 기준)</h4>
<pre><code class="language-sql">CREATE TABLE member (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(20) NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE,
    status VARCHAR(10) DEFAULT &#39;ACTIVE&#39;,
    created_at DATETIME,
    phone_number VARCHAR(255)
);</code></pre>
<hr>
<h4 id="주의사항">주의사항</h4>
<p>1️⃣ <strong>length 기본값</strong>  </p>
<ul>
<li><code>@Column</code>의 <code>length</code> 속성을 지정하지 않으면 기본값은 <code>255</code>로 설정됩니다.  </li>
<li>요구사항에 맞게 항상 적절한 길이를 지정해야 효율적인 테이블 설계가 가능합니다.  </li>
</ul>
<p>2️⃣ <strong>nullable과 기본값 충돌</strong>  </p>
<ul>
<li><code>nullable = false</code>와 <code>columnDefinition</code>의 <code>DEFAULT</code> 값은 함께 사용할 때 주의해야 합니다.  </li>
<li>데이터베이스는 <code>DEFAULT</code> 값을 설정하더라도, JPA는 이를 인지하지 못하므로 삽입 시 명시적으로 값을 설정해야 합니다.  </li>
</ul>
<p>3️⃣ <strong>columnDefinition 사용 시 의존성</strong>  </p>
<ul>
<li><code>columnDefinition</code>은 데이터베이스에 강하게 의존하므로, 다중 데이터베이스 환경에서는 호환성 문제가 발생할 수 있습니다.  </li>
<li>가능한 경우 JPA 표준 어노테이션을 사용하는 것이 권장됩니다.  </li>
</ul>
<hr>
<h3 id="4️⃣-applicationyml-설정">4️⃣ <strong>Application.yml 설정</strong></h3>
<p>Spring Boot 애플리케이션에서 데이터베이스 및 JPA 설정을 관리하는 파일은 <code>application.yml</code>입니다.<br>이 파일을 통해 데이터베이스 연결 정보와 JPA 동작 방식을 설정할 수 있습니다.  </p>
<h4 id="예제-데이터베이스-설정"><strong>예제: 데이터베이스 설정</strong></h4>
<pre><code class="language-yaml">spring:
  datasource:
    url: jdbc:mysql://localhost:3306/study   # 연결할 데이터베이스 URL
    username: your_username                 # 데이터베이스 사용자명
    password: your_password                 # 데이터베이스 비밀번호
  jpa:
    hibernate:
      ddl-auto: update                      # 엔티티 변경 시 테이블 자동 업데이트
    show-sql: true                          # 실행되는 SQL 쿼리를 출력</code></pre>
<h4 id="주요-설정-설명"><strong>주요 설정 설명</strong></h4>
<p>1️⃣ <strong><code>spring.datasource.url</code></strong>  </p>
<ul>
<li><p>애플리케이션이 연결할 데이터베이스의 주소를 설정합니다.  </p>
<ul>
<li><code>jdbc:mysql://localhost:3306/study</code>: MySQL 데이터베이스 <code>study</code>에 연결.  </li>
<li><code>localhost</code> 대신 <strong>클라우드 DB</strong>를 사용하면 해당 주소를 입력합니다.  </li>
<li>포트 번호(<code>3306</code>)는 MySQL 기본 포트입니다.  </li>
</ul>
<pre><code class="language-yaml">spring:
  datasource:
    url: jdbc:mysql://aws-db-instance:3306/study</code></pre>
</li>
</ul>
<p>2️⃣ <strong><code>spring.jpa.hibernate.ddl-auto</code></strong>  </p>
<ul>
<li><p>엔티티의 변경 사항이 데이터베이스 테이블에 반영되는 방식을 설정합니다.  </p>
<ul>
<li><strong><code>update</code></strong>: 엔티티 변경 사항을 반영해 테이블 구조를 자동으로 수정.  </li>
<li><strong><code>validate</code></strong>: 엔티티와 테이블의 스키마를 비교해 일치 여부만 확인.  </li>
<li><strong><code>none</code></strong>: 테이블 생성 및 수정 작업을 수행하지 않음.  </li>
</ul>
<p>💡 <strong>권장 사항</strong>  </p>
<ul>
<li>개발 환경: <strong><code>update</code></strong> (편리한 수정 작업을 위해)  </li>
<li>운영 환경: <strong><code>validate</code></strong> 또는 <strong><code>none</code></strong> (데이터 손실 방지)  </li>
</ul>
</li>
</ul>
<p>3️⃣ <strong><code>spring.jpa.show-sql</code></strong>  </p>
<ul>
<li>실행되는 SQL 쿼리를 콘솔에 출력합니다.  </li>
<li>디버깅과 테스트 시 유용하며, 복잡한 쿼리 작성 시 쿼리 최적화에 도움을 줍니다.  <pre><code class="language-yaml">spring:
  jpa:
    show-sql: true</code></pre>
</li>
</ul>
<hr>
<h4 id="추가-옵션"><strong>추가 옵션</strong></h4>
<p>1️⃣ <strong><code>spring.jpa.properties.hibernate.format_sql</code></strong>  </p>
<ul>
<li>SQL 쿼리를 가독성이 높게 포맷팅하여 출력합니다.  <pre><code class="language-yaml">spring:
  jpa:
    properties:
      hibernate.format_sql: true</code></pre>
</li>
</ul>
<p>2️⃣ <strong>데이터베이스 연결 풀 설정 (HikariCP)</strong>  </p>
<ul>
<li>Spring Boot는 기본적으로 HikariCP를 데이터베이스 연결 풀로 사용합니다.  </li>
<li>연결 풀 크기 등을 설정할 수 있습니다.  <pre><code class="language-yaml">spring:
  datasource:
    hikari:
      maximum-pool-size: 10  # 최대 연결 수
      minimum-idle: 5        # 최소 유휴 연결 수</code></pre>
</li>
</ul>
<p>3️⃣ <strong>데이터베이스 드라이버 설정</strong>  </p>
<ul>
<li>데이터베이스에 맞는 드라이버 클래스를 명시적으로 설정할 수 있습니다.  <pre><code class="language-yaml">spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver</code></pre>
</li>
</ul>
<h4 id="환경별-설정-관리"><strong>환경별 설정 관리</strong></h4>
<p>운영 환경과 개발 환경에 따라 설정을 분리하려면 <code>application-{profile}.yml</code> 파일을 사용합니다.<br>예: <code>application-dev.yml</code> (개발 환경), <code>application-prod.yml</code> (운영 환경).  </p>
<pre><code class="language-yaml">spring:
  profiles:
    active: dev</code></pre>
<hr>
<h3 id="정리">정리</h3>
<p>  이번 시간에는 지금까지 배웠던 내용을 바탕으로 스프링 프로젝트를 시작하는 과정과 틀에 대해 배워봤습니다. 이제 혼자서도 스프링 프로젝트를 만들어낼 수 있게 된 겁니다!!!(와~~) 다음 시간에는 JPA를 더 구체적으로 살펴보면서 오늘 배운 내용의 심화 버전을 배워보겠습니다!🤗😊😁</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[GPT Playground로 프롬프트 엔지니어링 해보기(프롬포트 엔지니어링 팁을 곁들여...)]]></title>
            <link>https://velog.io/@funda__mental_/GPT-Playground%EB%A1%9C-%ED%94%84%EB%A1%AC%ED%94%84%ED%8A%B8-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81-%ED%95%B4%EB%B3%B4%EA%B8%B0-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC</link>
            <guid>https://velog.io/@funda__mental_/GPT-Playground%EB%A1%9C-%ED%94%84%EB%A1%AC%ED%94%84%ED%8A%B8-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81-%ED%95%B4%EB%B3%B4%EA%B8%B0-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC</guid>
            <pubDate>Sun, 24 Nov 2024 17:00:09 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! 이번 글에서는 <strong>GPT Playground</strong>를 활용해 <strong>다음 달 예산 짜기</strong>라는 실질적인 AI 기능을 구현하는 과정을 안내해 드릴게요. 이 글은 AI 모델을 통해 현실적인 문제를 해결하는 방법을 배우는 데 초점을 맞춘 가이드입니다. 특히, <strong>프롬프트 엔지니어링</strong>을 통해 원하는 결과를 정확하게 얻는 방법과 이를 구현하는 코드를 소개합니다.</p>
<h3 id="목차"><strong>목차</strong></h3>
<ol>
<li><strong>다음 달 예산 짜기: 메인 기능 소개</strong></li>
<li><strong>더미 데이터와 모범 답안 구성</strong></li>
<li><strong>GPT Playground에서 프롬프트 작성하기</strong></li>
<li><strong>Temperature, Top-p, Penalty 설정 이해하기</strong></li>
<li><strong>구현된 로직을 gpt 프롬포트로 구현하기</strong> </li>
<li><strong>결론 및 최적화 팁</strong></li>
</ol>
<hr>
<h3 id="1-다음-달-예산-짜기-메인-기능-소개"><strong>1. 다음 달 예산 짜기: 메인 기능 소개</strong></h3>
<p>저희 팀 &#39;탐탐&#39;은 일정을 고려해 자동으로 사용자 맞춤화 가계부 서비스를 졸업프로젝트로 디벨롭하고 있습니다. 여기서 구현하고자 하는 메인 기능 중 하나는 사용자 데이터를 기반으로 <strong>사용자 맞춤화 다음 달 예산</strong>을 합리적으로 제공하고, 그렇게 예산을 짠 이유를 제공하는 거예요. 사용자가 입력한 고정 지출, 현재 예산, 다음 달 일정 등을 바탕으로, AI가 예산 항목을 자동으로 재분배해주는 거죠.
이렇게 gpt를 사용하거나 코드를 짜서 어떤 기능을 구현하고자 한다면, 자신이 구현하고자 하는 기능을 매우 구체적으로 생각해서 이 기능을 구현할 때 필요한 데이터, 어떤 로직(순서)로 기능이 구현되어야 하는지, 원하는 결과값을 생각하는 과정이 매우 중요합니다. 여러분들도 구현하고 싶은 기능이 있으시면 곰곰이 생각해보고 이를 정리해보세요!</p>
<h3 id="기능-시나리오"><strong>기능 시나리오</strong></h3>
<p>저희가 다음달 예산을 짤 때 고려할 것들은 다음과 같습니다. </p>
<ul>
<li><p><strong>입력</strong>:</p>
<ul>
<li>이번 달 예산: 이번달의 총 예산과 카테고리별 예산</li>
<li>이번 달 피드백 (예: &quot;영화 관람에서 초과 지출&quot;): 이번달 예산에 대한 AI 피드백 summary</li>
<li>고정 지출: 월세, 통신비, 넷플릭스 구독료 등 사용자가 입력해놓은 매달 필수적으로 지출되어야 하는 항목들</li>
<li>사용자 의견 (예: &quot;기타 학원 등록하고 싶어.&quot;): 이번 달 예산 피드백을 보고 사용자가 적은 다음 달 예산에서 원하는 점을</li>
<li>다음 달 일정 (예: &quot;도쿄 여행 예정&quot;): 사용자의 캘린더를 읽어와 그 중 AI를 활용해 유의미한 소비가 지출될 만한 것들을 추려놓은 데이터</li>
</ul>
</li>
<li><p><strong>출력</strong>:
  (JSON 형식)</p>
<ul>
<li>다음 달 예산 </li>
<li>예산 배정에 대한 상세 설명 및 이유</li>
</ul>
</li>
</ul>
<p>이 기능을 통해 사용자는 자신만의 생활 패턴과 목표를 고려한 <strong>개인화된 예산 관리</strong>를 경험할 수 있어요. 원래 예산을 짜려면 자신의 소비 내역을 전부 보면서 분석하고 예산을 짠 후 실천하고, 이를 피드백해서 다음 달의 일정을 고려해 직접 다음 달 예산을 짜야했다면, AI를 통해 이를 해주는 서비스를 구현할 수 있는 거죠. </p>
<hr>
<h3 id="2-더미-데이터와-모범-답안-구성"><strong>2. 더미 데이터와 모범 답안 구성</strong></h3>
<p><strong>GPT Playground</strong>에서 실험을 하기 전에, AI가 이해할 수 있는 데이터를 미리 준비하는 것이 중요해요. 더미 데이터는 <strong>실제 사용자 데이터</strong>처럼 구성해야 AI가 자연스럽게 작업을 수행할 수 있어서 다음과 같이 준비했습니다. 위에서 작성한 입력과 출력 데이터를 실제 데이터처럼 구성해놓았습니다. gpt는 자연어 기반의 AI이지만, JSON 형식 또한 해석할 수 있어 JSON 형식으로 데이터를 작성해주면 됩니다. 원하는 데이터 형식을 정해놓고 gpt를 사용해서 값을 채워 넣으면 효율적으로 작업할 수 있습니다. </p>
<h4 id="1-이번-달-예산"><strong>(1) 이번 달 예산</strong></h4>
<pre><code class="language-json">{
  &quot;monthly_budget&quot;: {
    &quot;total_budget&quot;: 800000,
    &quot;category_budgets&quot;: {
      &quot;교육/학습&quot;: 32000,
      &quot;교통&quot;: 80000,
      &quot;문화/여가&quot;: 120000,
      &quot;식비&quot;: 240000,
      &quot;의료/건강&quot;: 56000,
      &quot;카페/간식&quot;: 64000,
      &quot;패션/쇼핑&quot;: 24000
    }
  }
}
</code></pre>
<h4 id="2-이번-달-피드백"><strong>(2) 이번 달 피드백</strong></h4>
<pre><code class="language-json">{
  &quot;이번 달 총 예산을 잘 지키며 소비하셨네요! 특히 외식비를 관리하신 점이 돋보여요. 🎉 하지만 의류 구매와 영화 관람에서 예산을 초과한 점은 아쉬운 부분이에요. 다음 달에는 쇼핑과 여가 활동에서의 소비를 조금 줄이고, 그 절약한 금액을 다른 필요한 곳에 투자해보는 것은 어떨까요? 이렇게 하면 더 많은 저축이 가능할 거예요! 😊&quot;
}
</code></pre>
<h4 id="3-고정-지출"><strong>(3) 고정 지출</strong></h4>
<pre><code class="language-json">{
  &quot;fixed_expenses&quot;: [
    {
      &quot;name&quot;: &quot;교통비(정기권)&quot;,
      &quot;amount&quot;: 30000,
      &quot;frequency&quot;: &quot;monthly&quot;,
      &quot;type&quot;: &quot;교통&quot;,
      &quot;description&quot;: &quot;지하철/버스 정기권&quot;
    },
    {
      &quot;name&quot;: &quot;구독 서비스(넷플릭스)&quot;,
      &quot;amount&quot;: 12000,
      &quot;frequency&quot;: &quot;monthly&quot;,
      &quot;type&quot;: &quot;문화/여가&quot;,
      &quot;description&quot;: &quot;넷플릭스 스트리밍 구독&quot;
    }
  ]
}
</code></pre>
<h4 id="4-사용자-의견"><strong>(4) 사용자 의견</strong></h4>
<pre><code class="language-json">&quot;다음 달에 기타 학원(30만원)을 등록하고싶어. 그리고 식비를 줄일래.&quot;
</code></pre>
<h4 id="5-다음-달-주요-일정"><strong>(5) 다음 달 주요 일정</strong></h4>
<pre><code class="language-json">{
  &quot;다음 달의 주요 일정&quot;: [
    {
      &quot;이름&quot;: &quot;지민이랑 도쿄 일정&quot;,
      &quot;시간&quot;: &quot;2024-12-26 ~ 2024-12-30&quot;,
      &quot;카테고리&quot;: &quot;여행/숙박&quot;
    }
  ]
}
</code></pre>
<h4 id="ai-응답-예시-모범-답안"><strong>AI 응답 예시 (모범 답안)</strong></h4>
<pre><code class="language-json">{
  &quot;monthly_budget&quot;: {
    &quot;total_budget&quot;: 800000,
    &quot;category_budgets&quot;: {
      &quot;교육/학습&quot;: 300000,
      &quot;교통&quot;: 30000,
      &quot;문화/여가&quot;: 80000,
      &quot;식비&quot;: 200000,
      &quot;의료/건강&quot;: 56000,
      &quot;카페/간식&quot;: 64000,
      &quot;여행/숙박&quot;: 50000
    }
  },
  &quot;reason&quot;: &quot;다음 달 기타 학원 등록을 위해 교육/학습 항목에 300,000원을 배정했어요. 식비를 줄이고 싶다고 하셔서 200,000원으로 조정했답니다. 도쿄 여행을 위해 여행/숙박 항목에 50,000원을 추가 배정했어요.&quot;
}
</code></pre>
<hr>
<h3 id="3-gpt-playground에서-프롬프트-작성하기"><strong>3. GPT Playground에서 프롬프트 작성하기</strong></h3>
<p>GPT Playground는 OpenAI가 제공하는 프롬프트 기반 테스트 환경으로, AI 모델(GPT)의 동작 방식을 실험하고 최적화할 수 있는 도구입니다. 이 플랫폼을 사용하면 다양한 입력 데이터를 기반으로 모델의 응답을 미리 테스트하고, 하이퍼파라미터를 조정하여 원하는 결과를 얻을 수 있습니다.
<img src="https://velog.velcdn.com/images/funda__mental_/post/cad9ced3-1f95-4265-96d7-02fe980f424b/image.png" alt=""></p>
<hr>
<h4 id="3-1-gpt-playground의-주요-기능"><strong>3-1. GPT Playground의 주요 기능</strong></h4>
<p>GPT Playground는 다음과 같은 주요 기능을 제공합니다:</p>
<ol>
<li><p><strong>프롬프트 설계 및 실험</strong></p>
<ul>
<li>사용자가 작성한 프롬프트에 따라 AI의 응답을 확인.</li>
<li>응답의 형태, 길이, 창의성 등을 실험적으로 조정 가능.</li>
</ul>
</li>
<li><p><strong>모델 선택</strong></p>
<ul>
<li>OpenAI에서 제공하는 다양한 모델(GPT-4, GPT-3.5 등)을 선택해 테스트.</li>
</ul>
</li>
<li><p><strong>하이퍼파라미터 조정</strong></p>
<ul>
<li><strong>Temperature</strong>: 모델 응답의 창의성과 안정성 조정.</li>
<li><strong>Top-p</strong>: 확률 분포 상위 후보를 선택하는 범위 제한.</li>
<li><strong>Frequency Penalty</strong>: 단어 반복 억제.</li>
<li><strong>Presence Penalty</strong>: 새로운 단어를 유도.</li>
</ul>
</li>
<li><p><strong>출력 형식 선택</strong></p>
<ul>
<li>응답을 텍스트, JSON, 또는 코드 형식으로 반환.</li>
</ul>
</li>
<li><p><strong>컨텍스트 추가</strong></p>
<ul>
<li>시스템 메시지를 활용하여 모델의 행동 및 역할 설정.</li>
</ul>
</li>
</ol>
<hr>
<h4 id="3-2-gpt-playground에서-설정할-수-있는-것들"><strong>3-2. GPT Playground에서 설정할 수 있는 것들</strong></h4>
<p><strong>(1) 모델 설정</strong></p>
<ul>
<li><strong>모델 선택</strong>: GPT-4, GPT-3.5 등 다양한 모델을 선택.</li>
<li><strong>Max Tokens</strong>: 생성 가능한 응답의 최대 토큰 수.</li>
</ul>
<p><strong>(2) 하이퍼파라미터 설정</strong></p>
<ul>
<li><strong>Temperature</strong>: 낮을수록 응답이 안정적, 높을수록 창의적.</li>
<li><strong>Top-p</strong>: 확률 분포에서 상위 후보 단어만 선택.</li>
<li><strong>Frequency Penalty</strong>: 이미 사용된 단어의 반복 억제.</li>
<li><strong>Presence Penalty</strong>: 새로운 단어의 사용을 유도.</li>
</ul>
<p><strong>(3) 응답 형식</strong></p>
<ul>
<li>응답을 텍스트, JSON, 코드로 출력.</li>
<li>응답 길이와 스타일을 세부적으로 조정 가능.</li>
</ul>
<p><strong>(4) 시스템 메시지</strong></p>
<ul>
<li>AI의 역할과 성격을 미리 지정하여 컨텍스트 제공. Assistant와 User의 role을 선택할 수 있기 때문에 어떤 context에서 말하는 것인지 설정을 하며 더 정확한 프롬포팅 가능. </li>
</ul>
<hr>
<h4 id="3-3-gpt-playground에-접속하는-방법"><strong>3-3. GPT Playground에 접속하는 방법</strong></h4>
<ol>
<li><p><strong>OpenAI 계정 생성</strong></p>
<ul>
<li><a href="https://platform.openai.com/">OpenAI 홈페이지</a>에서 계정을 생성.</li>
</ul>
</li>
<li><p><strong>GPT Playground 접속</strong></p>
<ul>
<li>계정에 로그인 후, <a href="https://platform.openai.com/playground">Playground 페이지</a>로 이동. 이때, 결제를 해야지만 더 다양한 모델을 확인할 수 있다는 점을 참고하세요!</li>
</ul>
</li>
<li><p><strong>환경 설정</strong></p>
<ul>
<li>사용할 모델과 설정값(Temperature, Top-p 등)을 선택.</li>
</ul>
</li>
<li><p><strong>프롬프트 작성</strong></p>
<ul>
<li>AI 모델에게 제공할 지시문을 작성하고 결과를 확인.</li>
</ul>
</li>
</ol>
<hr>
<h4 id="3-4-왜-gpt-playground를-사용해야-하나요"><strong>3-4. 왜 GPT Playground를 사용해야 하나요?</strong></h4>
<ol>
<li><strong>빠른 테스트와 피드백</strong>: AI 모델의 응답을 실시간으로 확인.</li>
<li><strong>직관적 실험 환경</strong>: 코드 작성 없이 AI의 작동 방식을 실험.</li>
<li><strong>서비스 품질 향상</strong>: 최적화된 프롬프트를 통해 더 나은 사용자 경험 제공.
따라서 내 프롬포팅 결과를 실시간으로 보면서 설정값을 조절하고 싶을 때 GPT playground를 사용하면 좋습니다😊</li>
</ol>
<hr>
<h3 id="효과적인-프롬프트-작성법"><strong>효과적인 프롬프트 작성법</strong></h3>
<ol>
<li><p><strong>역할 지정하기</strong></p>
<p> 모델에게 역할을 주면 응답이 훨씬 명확해져요. 또한 gpt는 아직까지는 영어로 된 응답에 더 익숙하기 때문에 영어로 스크립트를 작성하는 걸 추천해요!</p>
<p> 예: *&quot;너는 재무 컨설턴트야. 사용자 데이터를 분석해 예산을 추천해줘.&quot;*</p>
<pre><code> You are a financial assistant AI responsible for creating a personalized monthly budget.
 Analyze the provided data and generate a JSON budget plan.
 Explain the reasoning for allocations in Korean.
</code></pre></li>
<li><p><strong>구체적인 요구사항 전달하기</strong></p>
<p> AI가 어떤 작업을 해야 할지, 어떤 결과를 기대하는지 명확히 설명하세요.</p>
<p> 예: *&quot;다음 달 예산을 짜고, 각 항목의 배정 이유를 JSON 형식으로 설명해줘.&quot;*</p>
</li>
<li><p><strong>예시 제공하기</strong></p>
<p> AI가 정확히 이해할 수 있도록 예제를 포함하세요.</p>
<p> 예:</p>
<pre><code> 사용자 데이터:
 - 식비: 240,000원
 - 교통: 80,000원</code></pre></li>
<li><p><strong>출력 형식 요청하기</strong></p>
<p> AI가 반환할 결과의 형식을 명확히 지정하세요.</p>
<p> 예: *&quot;JSON 형식으로 반환해줘.&quot;*</p>
</li>
</ol>
<p>이런 과정을 통해서 짜여진 저의 prompt는 아래와 같습니다. </p>
<pre><code class="language-JSON">You are a financial assistant AI responsible for creating a personalized monthly budget. Your task is to analyze the provided data and generate a budget plan for the next month in JSON format. Ensure the budget is tailored for 30 days (one month). Assign categories reasonably, especially for essential costs like food and living expenses. Explain the reasoning behind your allocations in detail in Korean in the `reason` field. YOU SHOULD ASSING REASONABLE BUDGET FOR SCHEDULE AND EACH CATEGORIES. 

### Inputs:
1. **This Month&#39;s Budget**: The budget allocated to each category this month. 
2. **This Month&#39;s Spending Feedback**: Feedback on whether the user overspent or underspent in various categories this month.
3. **Fixed Expenses**: Non-negotiable expenses such as rent, utilities, or loan repayments that must be covered.
4. **User&#39;s Preferences**: Specific preferences or priorities for the next month. 
   - If the user&#39;s preferences cannot be fully accommodated without exceeding the total budget or making other categories severely underfunded, partially adjust the allocations and explain the reasons clearly in the `reason` field. If you cannot make it, YOU SHOULD EXPLAIN IN THE RESON THO.
5. **Next Month&#39;s Plans**: Include additional expenses for special events, holidays, or other significant activities. You should assign reasonable amount of cost. Especially if it&#39;s travel to somewhere, you should calculate all the flight and accomodations, and food, etc. 
6. **Total Budget**: The maximum budget available for allocation next month (e.g., 800,000 KRW).

### Output Requirements:
Generate the budget in the following JSON format:
```json
{
  &quot;monthly_budget&quot;: {
    &quot;total_budget&quot;: 800000,
    &quot;category_budgets&quot;: {
      &quot;경조/선물&quot;: 0,
      &quot;교육/학습&quot;: 0,
      &quot;교통&quot;: 0,
      &quot;금융&quot;: 0,
      &quot;문화/여가&quot;: 0,
      &quot;반려동물&quot;: 0,
      &quot;뷰티/미용&quot;: 0,
      &quot;생활&quot;: 0,
      &quot;술/유흥&quot;: 0,
      &quot;식비&quot;: 0,
      &quot;여행/숙박&quot;: 0,
      &quot;온라인 쇼핑&quot;: 0,
      &quot;의료/건강&quot;: 0,
      &quot;자녀/육아&quot;: 0,
      &quot;자동차&quot;: 0,
      &quot;주거/통신&quot;: 0,
      &quot;카페/간식&quot;: 0,
      &quot;패션/쇼핑&quot;: 0
    }
  },
  &quot;reason&quot;: &quot;Explain the reasoning behind the budget allocations in Korean. Example of response is like the following, 이번 달 예산은 사용자님의 생활 패턴과 피드백을 꼼꼼히 반영해서 짜 보았어요. 먼저, 필수적으로 지출해야 하는 고정비(주거/통신 150,000원, 교통 70,000원)는 꼭 필요한 만큼 먼저 배정했어요. 다음달에는 지민님이 다음 달에는 기타 학원 등록을 원하셨기 때문에 교육/학습 항목에 300,000원을 충분히 배정했어요. 지난달 식비가 조금 초과되었다고 하셔서, 충분히 여유롭게 드실 수 있도록 260,000원으로 살짝 늘렸어요. 카페/간식은 딱 적당히 사용하신 것 같아 이번에도 그대로 유지했어요. 문화/여가 쪽은 사용이 적어서 30,000원을 줄이고, 그 금액은 다른 꼭 필요한 항목에 넣었답니다. 또한, 다음 달 주말 여행을 계획하셨다고 하셔서, 여행/숙박 항목에 150,000원을 새로 배정했어요. 요청하신 다른 항목들 중에서 예산에 부담이 되는 부분은 조금 줄였지만, 최대한 반영하려 노력했답니다. 술/유흥은 특별히 많이 필요하지 않으실 것 같아 조금 줄였고, 생활비는 꼭 필요한 만큼 그대로 유지했어요. 의료/건강 항목은 정기 검진 등 필요한 비용을 고려해서 10,000원을 추가했어요. 지난 달 피드백에 따라 쇼핑에서 지출을 줄여서 다음 달 주요 일정이 여행을 위한 돈을 모아보는 건 어떻까요? 전체 예산이 800,000원을 넘지 않도록 하나하나 신경 써서 조정했으니, 다음 달도 편안하게 보내실 수 있기를 바라요. 😊&quot; 
}
</code></pre>
<p>읽어보시면 role을 부여하고 각 input을 어떻게 처리해야할지, 어떤 output을 어떤 형식으로 주어야할지 매우 세세하게 지시하고 있는 걸 알 수 있습니다. 특히, gpt의 경우 같은 말이어도 어떻게 하냐에 따라 매우 다른 성능을 보이니 아래 사이트를 참고해서 프롬포트 작성에 대한 방향성을 잡아서 작성하는 것을 추천드립니다. </p>
<p><a href="https://platform.openai.com/docs/examples">OPEN AI 사이트 - prompt refereneces</a></p>
<hr>
<h3 id="4-temperature-top-p-penalty-설정-이해하기"><strong>4. Temperature, Top-p, Penalty 설정 이해하기</strong></h3>
<p>이번에는 GPT Playground에서 생성형 AI 모델의 응답을 세밀하게 조정할 수 있는 하이퍼파라미터인 <strong>Temperature, Top-p, Frequency Penalty, Presence Penalty</strong>를 깊이 탐구해볼 거예요. 이 글에서는 각각의 설정이 어떻게 작동하는지, 실제 실험 데이터를 기반으로 어떤 결과를 얻을 수 있는지 하나씩 설명드리겠습니다.
<img src="https://velog.velcdn.com/images/funda__mental_/post/2015f16f-9034-4c9f-b80d-6008e6d8a18a/image.png" alt="">
아까 저희가 봤던 화면에서 보라색 사각형 부분에서 다양한 파라미터를 조절할 수 있는 부분을 확인할 수 있습니다. </p>
<h4 id="1-temperature-창의성과-안정성의-균형"><strong>1) Temperature: 창의성과 안정성의 균형</strong></h4>
<p><code>Temperature</code>는 출력 확률 분포를 조정하여 모델의 응답에서 <strong>창의성</strong>과 <strong>예측 가능성</strong>의 균형을 맞추는 데 사용됩니다. 전체 파라미터 중 가장 먼저, 많이 조정하는 값입니다. </p>
<ul>
<li><strong>낮은 값 (0.2)</strong>: 결과가 더 <strong>결정적</strong>이고 예측 가능해요.</li>
<li><strong>높은 값 (1.0 이상)</strong>: 결과가 다양하고 <strong>창의적</strong>입니다.</li>
</ul>
<p><strong>작동 방식</strong>
<code>Temperature</code>는 모델의 확률 분포를 변경해 낮은 확률의 선택지가 얼마나 고려될지를 결정합니다.</p>
<ul>
<li><strong>낮은 Temperature</strong>: 확률이 높은 선택지만 선택 → <strong>안정적 결과</strong>.</li>
<li><strong>높은 Temperature</strong>: 확률이 낮은 선택지도 고려 → <strong>더 다양한 결과</strong>.</li>
</ul>
<p><strong>비유</strong>
<code>Temperature</code>는 얼음과 불 같아요.</p>
<ul>
<li><strong>낮은 Temperature</strong>는 얼음처럼 차분하고 제한적 선택.</li>
<li><strong>높은 Temperature</strong>는 불처럼 자유롭고 다양한 선택!</li>
</ul>
<h4 id="실험-결과-분석"><strong>실험 결과 분석</strong></h4>
<p>_아래의 예시들은 이번 달 소비에 대한 피드백을 위한 실험값입니다. 위에서 로직과 데이터를 어떻게 처리해야하는지를 알려드리기 위해 &quot;다음 달 예산 짜기&quot;를 사용했지만, 아래 gpt playground를 통한 파라미터 조정은 &quot;이번 달 소비에 대한 피드백&quot;이 피드백을 사용자에게 직접 제공한다는 측면에서 더 이해하시기 쉬울 거 같아 이렇게 보여드립니다! 실제로는 모든 기능에 playground를 사용해 프롬포팅을 튜닝했습니다.  _</p>
<p><strong>Temperature = 0.2 (낮음)</strong></p>
<pre><code class="language-json">&quot;이번 달 예산을 잘 지키셨네요. 다음 달에도 계획대로 진행하시면 좋을 것 같아요.&quot;
</code></pre>
<ul>
<li>안정적이고 <strong>예측 가능한</strong> 답변. 안정적이지만, 너무 유사한 답변만을 매번 내놓을 수 있다는 단점이 있음.</li>
</ul>
<p><strong>Temperature = 1.0 (기본값)</strong></p>
<pre><code class="language-json">&quot;이번 달 예산을 잘 관리하셨네요! 🎉 특히 식비 절약이 돋보입니다. 하지만 문화/여가 지출에서 초과된 부분을 조정하면 더 좋을 것 같아요.&quot;
</code></pre>
<ul>
<li>조금 더 <strong>창의적이고 부드러운 문장</strong>.</li>
</ul>
<p><strong>Temperature = 1.5 (높음)</strong></p>
<pre><code class="language-json">&quot;이번 달 예산은 멋지게 관리하셨습니다! 🎉 하지만 교통비와 문화 활동에 대한 새로운 접근을 시도해보시면 어떨까요? 😊&quot;
</code></pre>
<ul>
<li><strong>다양하고 자유로운 표현</strong>, 하지만 예측 가능성은 줄어듦. 다양한 응답을 준다는 점에서 매우 매력적이지만, gpt를 사용해서 서비스를 구성해야하는 상황에서 gpt의 응답의 예측이 어렵다면 대답 중에서 원하는 컴포넌트를 뽑아내 내 서비스 내에서 사용하기 어려울 수 있음.
특히 아래처럼 예상치 못한 에러 아닌 에러가 나타날 수도 있습니다. 
<img src="https://velog.velcdn.com/images/funda__mental_/post/1bb8d049-aa84-476c-9e8d-8fe6c8aafd4f/image.png" alt=""></li>
</ul>
<hr>
<h4 id="2-top-p-선택의-폭을-제한하는-필터"><strong>2) Top-p: 선택의 폭을 제한하는 필터</strong></h4>
<p><code>Top-p</code>는 확률 분포에서 누적 확률이 특정 임계값(<code>p</code>)에 도달할 때까지 <strong>상위 후보 단어만 선택</strong>하는 방식입니다.</p>
<ul>
<li><strong>낮은 값 (0.1)</strong>: 확률이 높은 상위 후보들만 선택 → <strong>보수적 결과</strong>.</li>
<li><strong>높은 값 (0.9)</strong>: 확률이 낮은 후보도 포함 → <strong>다양한 결과</strong>.</li>
</ul>
<p><strong>작동 방식</strong>
모델이 계산한 다음 단어의 확률 분포를 누적 합산하여, <code>p</code> 이하의 확률만 포함합니다.</p>
<p><strong>비유</strong>
<code>Top-p</code>는 &quot;상위 몇 %의 후보를 선택할지 결정하는 체계적인 필터&quot;입니다.</p>
<ul>
<li>낮은 <code>Top-p</code>는 <strong>필터링을 빡세게</strong>, 높은 <code>Top-p</code>는 <strong>느슨하게</strong>.</li>
</ul>
<h4 id="실험-결과-분석-1"><strong>실험 결과 분석</strong></h4>
<p><strong>Top-p = 0.3 (낮음)</strong></p>
<pre><code class="language-json">&quot;식비 절약을 잘 하셨네요. 교통비도 계획적으로 관리해보세요.&quot;
</code></pre>
<ul>
<li>확률이 높은 단어들만 사용 → <strong>단순하고 보수적 표현</strong>.</li>
</ul>
<p><strong>Top-p = 0.9 (높음)</strong></p>
<pre><code class="language-json">&quot;이번 달 예산을 잘 지키셨네요! 특히 식비와 카페 비용에서 멋지게 절약하셨어요. 😊&quot;
</code></pre>
<ul>
<li>다양한 단어 조합 → <strong>좀 더 창의적이고 풍부한 표현</strong>.</li>
</ul>
<hr>
<h4 id="3-frequency-penalty-반복을-피하기-위한-설정"><strong>3) Frequency Penalty: 반복을 피하기 위한 설정</strong></h4>
<p><code>Frequency Penalty</code>는 텍스트에서 동일한 단어가 <strong>반복적으로 등장하지 않도록</strong> 조정합니다.</p>
<ul>
<li><strong>낮은 값 (0.0)</strong>: 단어가 반복될 가능성이 큼.</li>
<li><strong>높은 값 (2.0)</strong>: 반복 사용을 줄여 <strong>더 다양한 표현</strong> 유도.</li>
</ul>
<p><strong>작동 방식</strong></p>
<p>이미 생성된 단어가 텍스트에 <strong>몇 번 등장했는지</strong>를 기준으로, 해당 단어의 확률을 줄입니다.</p>
<h4 id="실험-결과-분석-2"><strong>실험 결과 분석</strong></h4>
<p><strong>Frequency Penalty = 0.0 (기본값)</strong></p>
<pre><code class="language-json">&quot;이번 달 예산을 잘 지키셨네요. 예산을 잘 지키셨습니다!&quot;
</code></pre>
<ul>
<li>반복 표현 발생 가능성 증가.</li>
</ul>
<p><strong>Frequency Penalty = 2.0 (높음)</strong></p>
<pre><code class="language-json">&quot;이번 달 예산을 멋지게 관리하셨어요! 다양한 항목에서 절약 노력이 돋보입니다.&quot;
</code></pre>
<ul>
<li>반복 방지 → <strong>더 창의적이고 유연한 표현</strong>.</li>
</ul>
<hr>
<h4 id="4-presence-penalty-새로운-단어를-유도하는-설정"><strong>4) Presence Penalty: 새로운 단어를 유도하는 설정</strong></h4>
<p><code>Presence Penalty</code>는 이미 텍스트에 등장한 단어를 <strong>다시 사용하는 가능성을 낮추고</strong>, 새로운 단어를 선택하도록 유도합니다.</p>
<ul>
<li><strong>낮은 값 (0.0)</strong>: 기존 단어를 반복 사용하는 경향.</li>
<li><strong>높은 값 (2.0)</strong>: 새로운 단어를 더 많이 생성.</li>
</ul>
<h4 id="실험-결과-분석-3"><strong>실험 결과 분석</strong></h4>
<p><strong>Presence Penalty = 0.0 (기본값)</strong></p>
<pre><code class="language-json">&quot;이번 달 예산을 잘 지키셨네요. 예산 항목에서 잘 관리하셨습니다.&quot;
</code></pre>
<ul>
<li>기존 단어를 여러 번 사용.</li>
</ul>
<p><strong>Presence Penalty = 2.0 (높음)</strong></p>
<pre><code class="language-json">&quot;이번 달 예산 관리를 성공적으로 하셨습니다! 다음 달에는 다양한 절약 아이디어를 시도해보세요.&quot;
</code></pre>
<ul>
<li>새로운 단어 사용 → <strong>더 풍부한 텍스트</strong>.</li>
</ul>
<hr>
<h4 id="5-파라미터들을-조합해보기"><strong>5) 파라미터들을 조합해보기</strong></h4>
<p>각 파라미터들은 독립적으로 작동하지만, 함께 사용하면 <strong>더 정교하게 출력 제어</strong>가 가능합니다.</p>
<p>아래는 다양한 <code>Temperature</code>, <code>Top-p</code>, <code>Frequency Penalty</code>, <code>Presence Penalty</code> 조합으로 생성된 응답 예시입니다.</p>
<table>
<thead>
<tr>
<th><strong>설정값</strong></th>
<th><strong>결과 분석</strong></th>
</tr>
</thead>
<tbody><tr>
<td>Temperature = 0.9, Frequency Penalty = 0.1, Presence Penalty = 0.1</td>
<td>&quot;이번 달 예산을 잘 지키셨네요! 😊 특히 카페와 간식비에서 멋지게 절약하셨어요.&quot;</td>
</tr>
<tr>
<td>Temperature = 0.9, Frequency Penalty = 2.0, Presence Penalty = 2.0</td>
<td>&quot;이번 달 예산 관리를 성공적으로 하셨습니다! 🎉 다음 달에는 새로운 절약 아이디어를 시도해보세요.&quot;</td>
</tr>
<tr>
<td>Temperature = 1.2, Top-p = 0.8</td>
<td>&quot;이번 달 예산을 멋지게 관리하셨어요! 특히 식비에서의 절약이 돋보입니다. 😊 다음 달에는 문화 활동도 조금 줄여보면 더 많은 저축이 가능할 거예요.&quot;</td>
</tr>
</tbody></table>
<hr>
<h4 id="6-프롬포팅에-대한-정리-및-팁"><strong>6) 프롬포팅에 대한 정리 및 팁</strong></h4>
<ol>
<li><p><strong>명확한 목표 설정 및 데이터 준비</strong></p>
<ul>
<li>구현하고자 하는 기능의 <strong>로직과 필요한 데이터</strong>를 명확히 정의하기</li>
<li>예상되는 <strong>결과값</strong>을 고안하고, AI가 이해하기 쉽게 <strong>예시 데이터</strong>를 준비하기</li>
</ul>
</li>
<li><p><strong>프롬프트 내용 및 역할 설정</strong></p>
<ul>
<li><strong>명확하고 체계적인 영어로 지시</strong>를 작성하기<ul>
<li>AI가 수행할 작업을 구체적으로 설명하고, 입력값과 출력 형식 등을 명시</li>
</ul>
</li>
<li><strong>역할(Role) 부여</strong>:<ul>
<li>Assistant와 User의 역할을 명확히 구분</li>
<li>AI에게 적합한 성격이나 역할(예: 재무 컨설턴트, 교육 코치 등)을 부여하면 더 적절한 응답을 얻기 가능</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>설정값 최적화 및 실험</strong></p>
<ul>
<li>다양한 <strong>하이퍼파라미터(Temperature, Top-p, Penalty)</strong> 조합을 실험하여 최적의 설정값을 찾기</li>
<li>실험 결과를 분석해 <strong>목표에 가장 적합한 응답</strong>을 생성할 수 있도록 설정을 조정하기</li>
</ul>
</li>
</ol>
<p>이제 여러분도 위의 팁들을 사용해 직접 gpt prompt를 작성해보세요! </p>
<hr>
<h3 id="5-실험한-결과를-바탕으로-gpt-프롬포트-완성하기"><strong>5. 실험한 결과를 바탕으로 gpt 프롬포트 완성하기</strong></h3>
<p>실제로 코드에 각 컴포넌트로 분리해서 들어가 이와 같은 형태로 합쳐져 gpt에게 request를 보내는 API call이 일어나게 됩니다. 컴포넌트 분리는 백엔드의 domain이나 service가 어떻게 구성되냐에 따라 다르게 설정됩니다. </p>
<pre><code class="language-json">{
  &quot;model&quot;: &quot;gpt-4o-mini&quot;,
  &quot;messages&quot;: [
    {
      &quot;role&quot;: &quot;system&quot;,
      &quot;content&quot;: [
        {
          &quot;type&quot;: &quot;text&quot;,
          &quot;text&quot;: &quot;You are a financial assistant AI responsible for creating a personalized monthly budget. Your task is to analyze the provided data and generate a budget plan for the next month in JSON format. Ensure the budget is tailored for 30 days (one month). Assign categories reasonably, especially for essential costs like food and living expenses. Explain the reasoning behind your allocations in detail in Korean in the `reason` field. YOU SHOULD ASSING REASONABLE BUDGET FOR SCHEDULE AND EACH CATEGORIES. \n\n### Inputs:\n1. **This Month&#39;s Budget**: The budget allocated to each category this month. \n2. **This Month&#39;s Spending Feedback**: Feedback on whether the user overspent or underspent in various categories this month.\n3. **Fixed Expenses**: Non-negotiable expenses such as rent, utilities, or loan repayments that must be covered.\n4. **User&#39;s Preferences**: Specific preferences or priorities for the next month. \n   - If the user&#39;s preferences cannot be fully accommodated without exceeding the total budget or making other categories severely underfunded, partially adjust the allocations and explain the reasons clearly in the `reason` field. If you cannot make it, YOU SHOULD EXPLAIN IN THE RESON THO.\n5. **Next Month&#39;s Plans**: Include additional expenses for special events, holidays, or other significant activities. You should assign reasonable amount of cost. Especially if it&#39;s travel to somewhere, you should calculate all the flight and accomodations, and food, etc. \n6. **Total Budget**: The maximum budget available for allocation next month (e.g., 800,000 KRW).\n\n### Output Requirements:\nGenerate the budget in the following JSON format:\n```json\n{\n  \&quot;monthly_budget\&quot;: {\n    \&quot;total_budget\&quot;: 800000,\n    \&quot;category_budgets\&quot;: {\n      \&quot;경조/선물\&quot;: 0,\n      \&quot;교육/학습\&quot;: 0,\n      \&quot;교통\&quot;: 0,\n      \&quot;금융\&quot;: 0,\n      \&quot;문화/여가\&quot;: 0,\n      \&quot;반려동물\&quot;: 0,\n      \&quot;뷰티/미용\&quot;: 0,\n      \&quot;생활\&quot;: 0,\n      \&quot;술/유흥\&quot;: 0,\n      \&quot;식비\&quot;: 0,\n      \&quot;여행/숙박\&quot;: 0,\n      \&quot;온라인 쇼핑\&quot;: 0,\n      \&quot;의료/건강\&quot;: 0,\n      \&quot;자녀/육아\&quot;: 0,\n      \&quot;자동차\&quot;: 0,\n      \&quot;주거/통신\&quot;: 0,\n      \&quot;카페/간식\&quot;: 0,\n      \&quot;패션/쇼핑\&quot;: 0\n    }\n  },\n  \&quot;reason\&quot;: \&quot;Explain the reasoning behind the budget allocations in Korean. Example of response is like the following, 이번 달 예산은 사용자님의 생활 패턴과 피드백을 꼼꼼히 반영해서 짜 보았어요. 먼저, 필수적으로 지출해야 하는 고정비(주거/통신 150,000원, 교통 70,000원)는 꼭 필요한 만큼 먼저 배정했어요. 다음달에는 지민님이 다음 달에는 기타 학원 등록을 원하셨기 때문에 교육/학습 항목에 300,000원을 충분히 배정했어요. 지난달 식비가 조금 초과되었다고 하셔서, 충분히 여유롭게 드실 수 있도록 260,000원으로 살짝 늘렸어요. 카페/간식은 딱 적당히 사용하신 것 같아 이번에도 그대로 유지했어요. 문화/여가 쪽은 사용이 적어서 30,000원을 줄이고, 그 금액은 다른 꼭 필요한 항목에 넣었답니다. 또한, 다음 달 주말 여행을 계획하셨다고 하셔서, 여행/숙박 항목에 150,000원을 새로 배정했어요. 요청하신 다른 항목들 중에서 예산에 부담이 되는 부분은 조금 줄였지만, 최대한 반영하려 노력했답니다. 술/유흥은 특별히 많이 필요하지 않으실 것 같아 조금 줄였고, 생활비는 꼭 필요한 만큼 그대로 유지했어요. 의료/건강 항목은 정기 검진 등 필요한 비용을 고려해서 10,000원을 추가했어요. 지난 달 피드백에 따라 쇼핑에서 지출을 줄여서 다음 달 주요 일정이 여행을 위한 돈을 모아보는 건 어떻까요? 전체 예산이 800,000원을 넘지 않도록 하나하나 신경 써서 조정했으니, 다음 달도 편안하게 보내실 수 있기를 바라요. 😊\&quot; \n}\n&quot;
        }
      ]
    },
    {
      &quot;role&quot;: &quot;user&quot;,
      &quot;content&quot;: [
        {
          &quot;type&quot;: &quot;text&quot;,
          &quot;text&quot;: &quot;{ \&quot;current_budget\&quot;: { \&quot;monthly_budget\&quot;: { \&quot;total_budget\&quot;: 800000, \&quot;category_budgets\&quot;: { \&quot;경조/선물\&quot;: 0, \&quot;교육/학습\&quot;: 32000, \&quot;교통\&quot;: 80000, \&quot;금융\&quot;: 0, \&quot;문화/여가\&quot;: 120000, \&quot;반려동물\&quot;: 0, \&quot;뷰티/미용\&quot;: 40000, \&quot;생활\&quot;: 40000, \&quot;술/유흥\&quot;: 24000, \&quot;식비\&quot;: 240000, \&quot;여행/숙박\&quot;: 0, \&quot;온라인 쇼핑\&quot;: 0, \&quot;의료/건강\&quot;: 56000, \&quot;자녀/육아\&quot;: 0, \&quot;자동차\&quot;: 0, \&quot;주거/통신\&quot;: 0, \&quot;카페/간식\&quot;: 64000, \&quot;패션/쇼핑\&quot;: 24000 } } }, \&quot;current_feedback\&quot;: { \&quot;message\&quot;: \&quot;이번 달 총 예산을 잘 지키며 소비하셨네요! 특히 외식비를 관리하신 점이 돋보여요. 🎉 하지만 의류 구매와 영화 관람에서 예산을 초과한 점은 아쉬운 부분이에요. 다음 달에는 쇼핑과 여가 활동에서의 소비를 조금 줄이고, 그 절약한 금액을 다른 필요한 곳에 투자해보는 것은 어떨까요? 이렇게 하면 더 많은 저축이 가능할 거예요! 😊\&quot; }, \&quot;fixed_expenses\&quot;: [ { \&quot;fixed_expenses\&quot;: [ { \&quot;name\&quot;: \&quot;교통비(정기권)\&quot;, \&quot;amount\&quot;: 30000, \&quot;frequency\&quot;: \&quot;monthly\&quot;, \&quot;type\&quot;: \&quot;교통\&quot;, \&quot;description\&quot;: \&quot;지하철/버스 정기권\&quot; }, { \&quot;name\&quot;: \&quot;구독 서비스(넷플릭스)\&quot;, \&quot;amount\&quot;: 12000, \&quot;frequency\&quot;: \&quot;monthly\&quot;, \&quot;type\&quot;: \&quot;문화/여가\&quot;, \&quot;description\&quot;: \&quot;넷플릭스 스트리밍 구독\&quot; } ] } ], \&quot;user_opinions\&quot;: { \&quot;plans\&quot;: \&quot;다음 달에 기타 학원(30만원)을 등록하고싶어. 그리고 식비를 줄일래.\&quot; }, \&quot;next_month_schedules\&quot;: [ { \&quot;이름\&quot;: \&quot;지민이랑 도쿄 일정\&quot;, \&quot;시간\&quot;: \&quot;2024-12-26 ~ 2024-12-30\&quot;, \&quot;색깔\&quot;: \&quot;Blue\&quot;, \&quot;카테고리\&quot;: \&quot;여행/숙박\&quot; } ] }&quot;
        }
      ]
    },
    {
      &quot;role&quot;: &quot;assistant&quot;,
      &quot;content&quot;: [
        {
          &quot;type&quot;: &quot;text&quot;,
          &quot;text&quot;: &quot;{\n  \&quot;monthly_budget\&quot;: {\n    \&quot;total_budget\&quot;: 800000,\n    \&quot;category_budgets\&quot;: {\n      \&quot;경조/선물\&quot;: 0,\n      \&quot;교육/학습\&quot;: 300000,\n      \&quot;교통\&quot;: 30000,\n      \&quot;금융\&quot;: 0,\n      \&quot;문화/여가\&quot;: 80000,\n      \&quot;반려동물\&quot;: 0,\n      \&quot;뷰티/미용\&quot;: 40000,\n      \&quot;생활\&quot;: 40000,\n      \&quot;술/유흥\&quot;: 24000,\n      \&quot;식비\&quot;: 200000,\n      \&quot;여행/숙박\&quot;: 50000,\n      \&quot;온라인 쇼핑\&quot;: 0,\n      \&quot;의료/건강\&quot;: 56000,\n      \&quot;자녀/육아\&quot;: 0,\n      \&quot;자동차\&quot;: 0,\n      \&quot;주거/통신\&quot;: 0,\n      \&quot;카페/간식\&quot;: 64000,\n      \&quot;패션/쇼핑\&quot;: 24000\n    }\n  },\n  \&quot;reason\&quot;: \&quot;이번 달 예산은 사용자님의 생활 패턴과 피드백을 반영하여 짜 보았어요. 지민님이 원하는 기타 학원 등록을 위해 교육/학습 항목에 300,000원을 배정했어요. 이는 사용자님의 구체적인 요구를 충족하기 위한 충분한 금액입니다. 식비는 지난달에 비해 줄이고 싶다고 하셨으므로, 200,000원으로 설정했어요. 외식비 관리가 잘 되었다고 하셨기 때문에 이 정도 금액이면 충분히 문제가 없으리라 판단했답니다. 문화/여가 항목은 영화 관람에서 예산을 초과한 점을 고려하여 80,000원으로 조정했어요. 교통비는 꼭 필요한 정기권이 있으니 30,000원으로 유지하되, 여행/숙박에는 50,000원을 새롭게 배정해 도쿄 일정에 대한 일부 비용을 충당할 수 있도록 했어요. 카페/간식은 사용이 적당하여 그대로 두었고, 뷰티/미용이나 술/유흥 부분은 필요에 맞게 조정했어요. 전체적으로 예산을 800,000원 내에서 효과적으로 조합하여 사용자님의 필요를 최대한 반영해 보았답니다. 다음 달도 좋은 한 달 되시길 바랍니다! 😊\&quot;\n}&quot;
        }
      ]
    }
  ],
  &quot;response_format&quot;: {
    &quot;type&quot;: &quot;json_object&quot;
  },
  &quot;temperature&quot;: 1,
  &quot;max_tokens&quot;: 2048,
  &quot;top_p&quot;: 1,
  &quot;frequency_penalty&quot;: 0,
  &quot;presence_penalty&quot;: 0
}</code></pre>
<hr>
<p>오늘은 <strong>GPT Playground</strong>를 통해 AI를 활용한 문제 해결 방법을 탐구했는데요, 프롬포트를 잘 설계하고 설정값을 조정하면 AI가 훨씬 효율적이고 정확하게 응답한다는 걸 확인할 수 있었습니다.
이번 결과는 정말 만족스러웠습니다! 특히, <strong>Temperature와 Top-p</strong>를 실험하며 다양한 결과를 만들어내는 과정이 흥미로웠어요. 프롬포팅 설계의 중요성도 다시 한번 느꼈고요. 
다음에는 더 재미있는 주제로 돌아오겠습니다! 궁금한 점은 언제든 댓글로 남겨주세요. 그럼 저는 이만! 🤗</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 4. Spring Boot의 코어 개념]]></title>
            <link>https://velog.io/@funda__mental_/Chapter-4.-Spring-Boot%EC%9D%98-%EC%BD%94%EC%96%B4-%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@funda__mental_/Chapter-4.-Spring-Boot%EC%9D%98-%EC%BD%94%EC%96%B4-%EA%B0%9C%EB%85%90</guid>
            <pubDate>Thu, 31 Oct 2024 06:08:47 GMT</pubDate>
            <description><![CDATA[<p>이번 시간에는 백엔드의 기반이 될 스프링에 대해 배워봅시다!!</p>
<h3 id="이번-주차-목표">이번 주차 목표</h3>
<ol>
<li>Spring 프레임워크와 API의 개념을 이해한다.  </li>
<li>Spring의 핵심 개념인 DI, IoC, 서블릿에 대해 알아본다.  </li>
</ol>
<hr>
<h3 id="🌱-스프링-프레임워크-기초---spring-framework-di와-ioc란">🌱 <strong>스프링 프레임워크 기초 - Spring Framework, DI와 IoC란?</strong></h3>
<h3 id="1-스프링spring이란"><strong>1. 스프링(Spring)이란?</strong></h3>
<p>스프링은 Java로 애플리케이션을 쉽게 개발할 수 있도록 도와주는 프레임워크입니다.
즉, 개발자가 복잡한 작업에 집중하지 않고 필요한 기능을 빠르게 구현할 수 있도록 도구와 규칙을 제공하는 틀입니다.</p>
<ul>
<li>자바 기반 개발에서 널리 사용되며, <strong>복잡한 작업을 간단히 처리할 수 있는 다양한 도구</strong>를 제공합니다.  </li>
<li>특히 <strong>의존성 관리(DI)</strong>와 <strong>제어 흐름 관리(IoC)</strong>를 통해 개발자가 복잡한 비즈니스 로직에 집중할 수 있도록 돕습니다.  </li>
</ul>
<h4 id="스프링을-쉽게-비유하자면"><strong>스프링을 쉽게 비유하자면?</strong></h4>
<p>1️⃣ <strong>종합 요리 키트</strong>  </p>
<ul>
<li><strong>요리사가 모든 재료를 준비하고 요리법을 알아야 한다면 시간이 오래 걸리겠죠?</strong><br>스프링은 요리사가 아니라도 쉽게 요리를 만들 수 있도록, <strong>필요한 재료와 조리법(구조와 규칙)을 제공하는 키트</strong>와 같습니다.  <ul>
<li><strong>재료</strong>: 스프링이 제공하는 기능들 (예: DI, IoC, 서블릿, 데이터 처리 등).  </li>
<li><strong>조리법</strong>: 스프링이 정해둔 규칙과 패턴을 따라 작업.  </li>
</ul>
</li>
</ul>
<p>2️⃣ <strong>블록 조립 키트</strong>  </p>
<ul>
<li>마치 레고 블록처럼 <strong>미리 만들어진 조각(컴포넌트)</strong>들을 끼워 맞춰서 원하는 애플리케이션을 만드는 느낌입니다.  <ul>
<li><strong>개발자는 각 블록(스프링 기능)을 조립하기만 하면, 복잡한 작업도 간단히 구현</strong>할 수 있습니다.  </li>
</ul>
</li>
</ul>
<hr>
<h3 id="왜-스프링을-사용할까"><strong>왜 스프링을 사용할까?</strong></h3>
<p>1️⃣ <strong>복잡한 작업을 자동화</strong>  </p>
<ul>
<li>객체를 생성하고, 서로 연결하고, 삭제하는 작업을 스프링이 대신 처리.  </li>
<li>개발자는 비즈니스 로직(실제 기능 구현)에 집중할 수 있음.  </li>
</ul>
<p>2️⃣ <strong>코드 간소화</strong>  </p>
<ul>
<li>불필요한 반복 코드를 줄이고, <strong>중요한 부분에만 집중</strong>할 수 있게 해줌.  </li>
</ul>
<p>3️⃣ <strong>유연성과 확장성</strong>  </p>
<ul>
<li>다양한 애플리케이션(웹, API, 데이터베이스 등)에 사용할 수 있으며, 필요한 기능을 추가하기 쉽습니다.  </li>
</ul>
<p>4️⃣ <strong>표준화된 설계 방식</strong>  </p>
<ul>
<li>스프링이 정해준 규칙에 따라 개발하면, <strong>다른 개발자와 협업하거나 유지보수하기 쉬움</strong>.  </li>
</ul>
<hr>
<h4 id="스프링의-주요-기능"><strong>스프링의 주요 기능</strong></h4>
<p>1️⃣ <strong>IoC (Inversion of Control)</strong>  </p>
<ul>
<li>객체 생성과 관리(초기화, 삭제 등)를 스프링이 대신 처리.  </li>
<li>개발자가 객체를 일일이 관리하지 않아도 됨.  </li>
</ul>
<p>2️⃣ <strong>DI (Dependency Injection)</strong>  </p>
<ul>
<li>객체 간의 관계를 스프링이 설정해 줌.  </li>
<li>개발자는 &quot;이 객체가 무엇과 연결될지&quot;를 걱정하지 않아도 됨.  </li>
</ul>
<p>3️⃣ <strong>MVC 패턴 지원</strong>  </p>
<ul>
<li>스프링은 <strong>Model-View-Controller</strong> 패턴을 사용해 <strong>웹 애플리케이션의 구조를 명확히 정의</strong>.  </li>
<li>프론트엔드(화면)와 백엔드(데이터 처리)가 분리되어 개발과 유지보수가 쉬움.  </li>
</ul>
<p>4️⃣ <strong>AOP (Aspect-Oriented Programming)</strong>  </p>
<ul>
<li>중복되는 로직(예: 로깅, 트랜잭션 관리)을 분리해 코드 중복을 줄이고 효율성을 높임.  </li>
</ul>
<hr>
<h3 id="쉽게-이해하기-위한-예"><strong>쉽게 이해하기 위한 예</strong></h3>
<h4 id="스프링이-없는-개발"><strong>스프링이 없는 개발</strong></h4>
<ul>
<li>객체를 직접 생성하고, 서로 연결하며, 요청을 처리하는 로직을 전부 작성해야 함.  </li>
<li>복잡한 설정 작업과 반복적인 코드가 많아짐.  </li>
</ul>
<pre><code class="language-java">UserService userService = new UserService();
DatabaseConnection dbConnection = new DatabaseConnection();
userService.setDatabaseConnection(dbConnection);
userService.processRequest(request);</code></pre>
<h4 id="스프링을-사용한-개발"><strong>스프링을 사용한 개발</strong></h4>
<ul>
<li>객체 생성과 의존성 설정을 스프링이 자동으로 처리.  </li>
<li>개발자는 주요 로직만 작성.  </li>
</ul>
<pre><code class="language-java">@Autowired
private UserService userService;

userService.processRequest(request);</code></pre>
<hr>
<h3 id="2-프레임워크와-api의-차이점"><strong>2. 프레임워크와 API의 차이점</strong></h3>
<p>프레임워크와 API는 비슷하게 들리지만, <strong>사용 방식과 역할에서 차이</strong>가 있습니다.  </p>
<table>
<thead>
<tr>
<th><strong>구분</strong></th>
<th><strong>프레임워크</strong></th>
<th><strong>API</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>정의</strong></td>
<td>코드 구조와 흐름을 미리 정의하고, <strong>개발자가 이를 따라 작업</strong>하도록 제공되는 틀</td>
<td>외부 소프트웨어와의 상호작용을 가능하게 하는 <strong>인터페이스</strong></td>
</tr>
<tr>
<td><strong>제어 흐름</strong></td>
<td>제어 흐름이 프레임워크에 의해 <strong>거꾸로(역전)</strong> 적용됨 (IoC)</td>
<td>개발자가 필요한 기능을 호출 (제어 흐름은 개발자에게 있음)</td>
</tr>
<tr>
<td><strong>예시</strong></td>
<td>스프링(Spring), 장고(Django)</td>
<td>Java의 HttpClient API, JDBC API</td>
</tr>
</tbody></table>
<h4 id="비유">비유</h4>
<ul>
<li><strong>프레임워크</strong>는 <strong>요리 레시피</strong>처럼 개발자가 따라야 하는 규칙과 구조를 제공합니다.  </li>
<li><strong>API</strong>는 <strong>재료를 요청하거나 상호작용하는 도구</strong>와 같습니다.  </li>
</ul>
<hr>
<h3 id="3-iocinversion-of-control란"><strong>3. IoC(Inversion of Control)란?</strong></h3>
<p>그러면 각각의 스프링의 기능에 대해 더 자세히 알아봅시다. </p>
<p><strong>IoC(Inversion of Control)</strong>는 제어의 흐름을 <strong>개발자가 아닌 스프링 컨테이너가 관리</strong>하도록 바꾸는 개념입니다.<br>즉, <strong>객체를 만들고, 연결하고, 없애는 일을 스프링 컨테이너에게 맡긴다</strong>고 생각하면 됩니다.  </p>
<h4 id="왜-ioc가-필요할까"><strong>왜 IoC가 필요할까?</strong></h4>
<h4 id="과거의-개발-방식-모든-것을-직접-관리">과거의 개발 방식: 모든 것을 직접 관리</h4>
<p>개발자가 직접 객체를 생성하고, 필요한 다른 객체와 연결하며, 객체를 없애는 과정까지 전부 관리해야 했습니다.  </p>
<ul>
<li><strong>문제점</strong>:  <ol>
<li>객체를 생성하고 관리하는 코드가 반복적으로 생김.  </li>
<li><strong>객체 간 연결(의존성)</strong>이 너무 밀접해서 수정하기 어려움.  </li>
<li>코드가 복잡해지고, 유지보수에 시간이 많이 듦.  </li>
</ol>
</li>
</ul>
<h4 id="ioc-방식-제어를-스프링-컨테이너에-위임">IoC 방식: 제어를 스프링 컨테이너에 위임</h4>
<p>스프링에서는 객체를 직접 생성하거나 연결하지 않아도 됩니다.  </p>
<ul>
<li><strong>해결</strong>:  <ol>
<li>객체를 생성하고 연결하는 작업을 <strong>스프링 컨테이너</strong>가 처리.  </li>
<li>개발자는 &quot;무엇을 해야 할지&quot;만 신경 쓰고, &quot;어떻게 할지&quot;는 스프링이 담당.  </li>
<li><strong>결합도가 낮아지고 코드가 단순화</strong>되어 유지보수가 쉬워짐.  </li>
</ol>
</li>
</ul>
<h4 id="ioc의-작동-방식"><strong>IoC의 작동 방식</strong></h4>
<p>1️⃣ <strong>클래스 정의</strong>  </p>
<ul>
<li>필요한 기능을 가진 클래스(재료)를 정의합니다.  <pre><code class="language-java">public class OrderService {
    private PaymentService paymentService;
}</code></pre>
</li>
</ul>
<p>2️⃣ <strong>의존성 설정</strong>  </p>
<ul>
<li>스프링 설정 파일(XML, Java Config) 또는 어노테이션을 사용해 객체 간 연결(의존성)을 설정합니다.  <pre><code class="language-java">@Autowired
private PaymentService paymentService; // OrderService에서 PaymentService가 필요!</code></pre>
</li>
</ul>
<p>3️⃣ <strong>컨테이너 관리</strong>  </p>
<ul>
<li>스프링 컨테이너가 객체를 생성하고 연결하며, 필요 없으면 소멸까지 자동으로 처리합니다.<br>개발자는 <code>OrderService</code>를 바로 사용할 수 있습니다.  <pre><code class="language-java">@Autowired
private OrderService orderService;</code></pre>
</li>
</ul>
<h4 id="ioc로-바뀌면-뭐가-좋아질까"><strong>IoC로 바뀌면 뭐가 좋아질까?</strong></h4>
<ol>
<li><strong>복잡성 감소</strong>  </li>
</ol>
<ul>
<li>객체 생성과 관리 로직이 없어지니 코드가 간단해집니다.  </li>
</ul>
<ol start="2">
<li><strong>유지보수 용이</strong>  </li>
</ol>
<ul>
<li>객체 간 연결이 유연해지면서, 수정이나 추가 작업을 할 때 <strong>다른 코드에 영향을 주지 않음</strong>.  <ol start="3">
<li><strong>확장성 향상</strong>  </li>
</ol>
</li>
<li>새로운 객체나 기능을 추가할 때 스프링 설정만 변경하면 되므로 코드 수정이 적습니다.  </li>
</ul>
<h4 id="간단한-코드-비교"><strong>간단한 코드 비교</strong></h4>
<h4 id="ioc-없이-직접-객체-관리">IoC 없이 직접 객체 관리</h4>
<pre><code class="language-java">public class OrderService {
    private PaymentService paymentService;

    public OrderService() {
        this.paymentService = new PaymentService(); // 직접 객체 생성
    }

    public void processOrder() {
        paymentService.pay();
    }
}</code></pre>
<h4 id="ioc로-스프링-컨테이너에-위임">IoC로 스프링 컨테이너에 위임</h4>
<pre><code class="language-java">@Component
public class OrderService {
    @Autowired
    private PaymentService paymentService; // 스프링이 자동으로 주입

    public void processOrder() {
        paymentService.pay();
    }
}</code></pre>
<hr>
<h3 id="4-didependency-injection란"><strong>4. DI(Dependency Injection)란?</strong></h3>
<p>다음으로는 의존성 주입(DI)에 대해 살펴보도록 하겠습니다.</p>
<p><strong>DI(Dependency Injection)</strong>는 객체 간의 <strong>의존성을 외부에서 주입</strong>하는 방식입니다.<br>즉, 객체가 필요한 다른 객체(의존성)를 스스로 생성하지 않고, <strong>스프링이 대신 연결해 주는 것</strong>을 의미합니다.</p>
<h4 id="di가-왜-필요할까"><strong>DI가 왜 필요할까?</strong></h4>
<ol>
<li><strong>결합도를 낮춤</strong>  <ul>
<li>객체 간 연결이 느슨해져서, 하나의 객체를 수정해도 다른 객체에 영향을 주지 않음.  </li>
</ul>
</li>
<li><strong>테스트와 유지보수 용이</strong>  <ul>
<li>객체를 쉽게 교체하거나 테스트용 객체(mock 객체)로 대체 가능.  </li>
</ul>
</li>
<li><strong>코드 단순화</strong>  <ul>
<li>객체 생성과 연결을 스프링이 처리하니, 개발자는 주요 로직에 집중할 수 있음.  </li>
</ul>
</li>
</ol>
<h3 id="di의-주요-방식"><strong>DI의 주요 방식</strong></h3>
<p>의존성 주입의 방식에는 대표적으로 생성자 주입, Setter 주입, 필드 주입 총 3가지 있습니다. 하나씩 살펴봅시다. </p>
<p>1️⃣ <strong>생성자 주입</strong>  </p>
<ul>
<li>객체 생성 시 <strong>생성자를 통해 의존성</strong>을 설정.  </li>
<li><strong>장점</strong>: 의존성이 항상 설정되므로 안전하며, 테스트에 적합.  </li>
<li><strong>단점</strong>: 의존성이 많으면 생성자가 복잡해질 수 있음.  </li>
</ul>
<pre><code class="language-java">@Component
public class OrderService {
    private final PaymentService paymentService;

    @Autowired
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService; // 생성자로 주입
    }
}</code></pre>
<p>2️⃣ <strong>Setter 주입</strong>  </p>
<ul>
<li>Setter 메서드를 통해 <strong>의존성을 주입</strong>.  </li>
<li><strong>장점</strong>: 선택적으로 의존성을 설정 가능.  </li>
<li><strong>단점</strong>: 의존성 누락 여부를 컴파일 시점에 확인할 수 없음.  </li>
</ul>
<pre><code class="language-java">@Component
public class OrderService {
    private PaymentService paymentService;

    @Autowired
    public void setPaymentService(PaymentService paymentService) {
        this.paymentService = paymentService; // Setter로 주입
    }
}</code></pre>
<p>3️⃣ <strong>필드 주입</strong>  </p>
<ul>
<li><strong>필드에 직접 의존성을 주입</strong>.  </li>
<li><strong>장점</strong>: 코드가 간결함.  </li>
<li><strong>단점</strong>: 테스트와 유지보수에 어려움이 있어 권장되지 않음.  </li>
</ul>
<pre><code class="language-java">@Component
public class OrderService {

    @Autowired
    private PaymentService paymentService; // 필드로 바로 주입
}</code></pre>
<p><strong>결론</strong>  </p>
<p>쉽게 비유하자면,</p>
<ul>
<li><strong>생성자 주입</strong>: 처음부터 <strong>필수 재료</strong>를 다 챙겨 요리를 시작.  </li>
<li><strong>Setter 주입</strong>: 요리를 하다 필요할 때마다 <strong>재료를 추가</strong>.  </li>
<li><strong>필드 주입</strong>: 재료가 항상 <strong>자동으로 주방에 배치</strong>되어 있음.  </li>
</ul>
<p>DI는 객체를 스스로 생성하지 않고, 스프링이 필요한 객체를 대신 연결해 주어 <strong>결합도를 낮추고, 코드 유지보수를 쉽게</strong> 만들어 줍니다.<br><strong>생성자 주입</strong>은 객체 생성 시 필요한 모든 의존성을 반드시 제공하도록 강제하기 때문에, 의존성 누락으로 생기는 문제를 컴파일 시점에 방지해줍니다. 따라서, 안전성과 테스트 용이성 측면에서 <strong>가장 권장되는 방식입니다!</strong></p>
<hr>
<h3 id="5-스프링-빈bean이란"><strong>5. 스프링 빈(Bean)이란?</strong></h3>
<p><strong>스프링 빈(Bean)</strong>은 <strong>스프링 컨테이너에서 관리하는 객체</strong>를 의미합니다.  </p>
<ul>
<li>** 빈은 스프링 컨테이너가 관리**하며, 애플리케이션의 주요 구성 요소를 뜻합니다.  </li>
<li>개발자는 컨테이너에 빈을 등록하고, 컨테이너는 필요한 의존성을 자동으로 주입합니다.  </li>
</ul>
<h4 id="✅-스프링-빈의-특징"><strong>✅ 스프링 빈의 특징</strong></h4>
<ul>
<li><strong>컨테이너 관리</strong>: 빈은 스프링 컨테이너(ApplicationContext)에 의해 생성되고 관리됩니다.  </li>
<li><strong>주요 구성 요소</strong>: 애플리케이션의 핵심 기능(서비스, DAO 등)을 담당하는 객체.  </li>
<li><strong>싱글톤 기본 설정</strong>: 빈은 기본적으로 <strong>싱글톤</strong>(애플리케이션에서 하나의 인스턴스만 존재)으로 관리되며, 필요시 scope를 변경할 수 있습니다.  </li>
</ul>
<hr>
<h4 id="🌟-스프링-빈의-동작-과정"><strong>🌟 스프링 빈의 동작 과정</strong></h4>
<p>1️⃣ <strong>빈 정의</strong>: 스프링 설정 파일(XML, Java Config)이나 어노테이션(<code>@Component</code>, <code>@Bean</code>)으로 정의.<br>2️⃣ <strong>빈 등록</strong>: 컨테이너가 빈을 읽어 생성하고 등록.<br>3️⃣ <strong>빈 사용</strong>: 컨테이너가 요청에 따라 빈을 반환하거나, 자동으로 주입.  </p>
<h3 id="✅-빈-등록-방법"><strong>✅ 빈 등록 방법</strong></h3>
<p>스프링에서는 빈을 등록하는 방식이 <strong>자동 등록</strong>과 <strong>명시적 등록</strong>으로 나뉩니다.</p>
<h4 id="1️⃣-묵시적-빈-정의-component-autowired"><strong>1️⃣ 묵시적 빈 정의 (<code>@Component</code>, <code>@Autowired</code>)</strong></h4>
<p>스프링이 <strong>자동으로 빈을 스캔</strong>하고 등록하는 방식입니다.  </p>
<ul>
<li>클래스에 <code>@Component</code>, <code>@Service</code>, <code>@Repository</code>, <code>@Controller</code>를 붙이면 스프링이 빈으로 등록.  </li>
<li>의존성은 <code>@Autowired</code>로 주입.</li>
</ul>
<pre><code class="language-java">@Component // 빈 자동 등록
public class MyService {
    @Autowired // 빈 의존성 자동 주입
    private MyRepository repository;

    public void execute() {
        repository.save();
    }
}</code></pre>
<p><strong>🌟 묵시적 빈 정의의 장점</strong>  </p>
<ul>
<li><strong>간단한 구조</strong>로 빠르게 빈을 등록할 수 있음.  </li>
<li>반복적인 코드 작성이 필요 없음.  </li>
</ul>
<h4 id="2️⃣-명시적-빈-정의--configuration-bean"><strong>2️⃣ 명시적 빈 정의  (<code>@Configuration</code>, <code>@Bean</code>)</strong></h4>
<p>개발자가 빈의 등록 과정을 직접 제어하는 방식입니다.  </p>
<ul>
<li>클래스에 <code>@Configuration</code>을 붙이고, 메서드에 <code>@Bean</code>을 사용하여 빈을 등록.  </li>
<li>복잡한 초기화 작업이나 의존성 설정이 필요한 경우 사용.  </li>
</ul>
<pre><code class="language-java">@Configuration // 설정 클래스
public class AppConfig {
    @Bean // 명시적 빈 등록
    public MyService myService() {
        return new MyService(myRepository());
    }

    @Bean
    public MyRepository myRepository() {
        return new MyRepository();
    }
}</code></pre>
<p><strong>🌟명시적 빈 정의의 장점</strong>  </p>
<ul>
<li>빈의 생성 과정을 <strong>세밀하게 제어</strong>할 수 있음.  </li>
<li>복잡한 의존성이나 초기화 작업에 적합.  </li>
</ul>
<h4 id="📝-묵시적-빈-정의-vs-명시적-빈-정의--비교"><strong>📝 묵시적 빈 정의 vs 명시적 빈 정의  비교</strong></h4>
<table>
<thead>
<tr>
<th><strong>구분</strong></th>
<th><strong>묵시적 빈 정의 (<code>@Component → @Autowired</code>)</strong></th>
<th><strong>명시적 빈 정의 (`@Configuration → @Bean)</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>사용 방법</strong></td>
<td>클래스에 어노테이션(<code>@Component</code>, <code>@Service</code>) 추가</td>
<td><code>@Configuration</code> 클래스에서 <code>@Bean</code> 메서드 사용</td>
</tr>
<tr>
<td><strong>장점</strong></td>
<td>구조가 간단하고 반복 작업 감소</td>
<td>생성 과정을 세밀하게 제어 가능</td>
</tr>
<tr>
<td><strong>단점</strong></td>
<td>복잡한 구성에는 적합하지 않음</td>
<td>코드가 길어질 수 있음</td>
</tr>
<tr>
<td><strong>추천 상황</strong></td>
<td>간단한 서비스, 레포지토리 등록에 적합</td>
<td>의존성 설정이나 초기화 작업이 필요한 경우 적합</td>
</tr>
</tbody></table>
<h4 id="결론-스프링-빈의-역할"><strong>결론: 스프링 빈의 역할</strong></h4>
<p>1️⃣ <strong>스프링 빈은 애플리케이션의 주요 구성 요소</strong>이며, 컨테이너에서 객체 생성 및 생명 주기를 관리합니다.<br>2️⃣ 자동 등록은 간단하고 반복적인 작업에 적합하며, 명시적 등록은 복잡한 작업에 적합합니다.<br>3️⃣ DI와 결합되어, 스프링 빈은 <strong>유연성과 유지보수성</strong>을 극대화합니다.  </p>
<hr>
<h3 id="6-서블릿servlet과-dispatcherservlet"><strong>6. 서블릿(Servlet)과 DispatcherServlet</strong></h3>
<h4 id="span-stylebackground-colorfff5b1서블릿servletspan이란"><span style="background-color:#fff5b1"><strong>서블릿(Servlet)</span>이란?</strong></h4>
<p><strong>서블릿(Servlet)</strong>은 <strong>웹 애플리케이션에서 클라이언트의 요청(HTTP Request)을 처리하고 응답(HTTP Response)을 생성</strong>하는 자바 기반의 컴포넌트입니다.<br>서블릿은 동적인 웹 페이지를 생성하는 데 사용되며, 클라이언트 요청에 따라 결과를 반환하는 역할을 합니다.  </p>
<p>웹 애플리케이션에서 서블릿은 다음과 같은 역할을 합니다:  </p>
<ol>
<li><strong>HTTP 요청 수신</strong>: 클라이언트가 입력하거나 클릭한 요청을 서버로 전달.  </li>
<li><strong>비즈니스 로직 처리</strong>: 요청 데이터를 분석하고 필요한 작업 수행.  </li>
<li><strong>응답 생성</strong>: 처리 결과를 HTML, JSON, XML 등의 형식으로 클라이언트에 반환.  </li>
</ol>
<h4 id="서블릿의-동작-과정">서블릿의 동작 과정</h4>
<p><img src="https://velog.velcdn.com/images/funda__mental_/post/a17f56ba-e45e-4d95-b74a-5d0550bcdce2/image.png" alt=""></p>
<ol>
<li><strong>사용자 요청</strong>: 클라이언트가 URL을 통해 HTTP 요청을 서버로 보냄.  </li>
<li><strong>서블릿 컨테이너</strong>: 요청을 받아 적합한 서블릿을 실행.  </li>
<li><strong>비즈니스 로직 처리</strong>: 서블릿이 데이터를 처리하거나 데이터베이스와 상호작용. </li>
<li><strong>결과 반환</strong>: 처리 결과를 HTTP 응답으로 클라이언트에게 전송.  </li>
</ol>
<h4 id="서블릿의-생명주기"><strong>서블릿의 생명주기</strong></h4>
<ol>
<li><strong>초기화(init)</strong>: 서블릿 컨테이너가 서블릿 객체를 생성하고 초기화.  </li>
<li><strong>서비스(service)</strong>: HTTP 요청을 처리하며 <code>doGet()</code>, <code>doPost()</code> 메서드를 실행.  </li>
<li><strong>소멸(destroy)</strong>: 서블릿 컨테이너가 서블릿 객체를 소멸하고 자원 해제.
<img src="https://velog.velcdn.com/images/funda__mental_/post/8e8117a5-9016-4481-8187-f0d19ba2a253/image.png" alt=""></li>
</ol>
<h3 id="servlet-container란"><strong>Servlet Container란?</strong></h3>
<p><strong>Servlet Container(서블릿 컨테이너)</strong>는 서블릿을 관리하고 실행하는 환경을 제공합니다. Servlet이 어떤 역할을 수행하는 <strong>메뉴얼</strong>이라면, Servlet 컨테이너는 해당 메뉴얼을 보고 직접 <strong>핸들링</strong>한다고 생각하시면 됩니다.</p>
<p>서블릿 컨테이너는 클라이언트의 요청(Request)를 받아주고 응답(Response)할 수 있게, 웹 서버와 Socket으로 통신합니다.
대표적인 서블릿 컨테이너로는 <strong>Apache Tomcat</strong>이 있으며, 다음과 같은 역할을 수행합니다:  </p>
<ol>
<li><strong>요청 관리</strong>: 클라이언트의 요청을 받아 적절한 서블릿으로 전달.  </li>
<li><strong>객체 관리</strong>: 서블릿의 생성, 초기화, 실행, 소멸 등을 자동으로 관리.  </li>
<li><strong>네트워크 통신 지원</strong>: 클라이언트-서버 간의 네트워크 통신을 처리.  </li>
<li><strong>비즈니스 로직 분리</strong>: 개발자가 요청 처리에만 집중할 수 있도록 도움.</li>
</ol>
<h3 id="spring의-dispatcherservlet"><strong>Spring의 DispatcherServlet</strong></h3>
<p>스프링에서 가장 중요한 서블릿은 <strong>DispatcherServlet</strong>입니다.<br>DispatcherServlet은 <strong>스프링 MVC의 중심</strong>으로, 클라이언트의 모든 HTTP 요청을 받아 적절한 <strong>Controller</strong>로 전달하고 처리 결과를 반환합니다. 이는 Front Controller 패턴을 구현한 서블릿으로, 모든 HTTP 요청을 받는 녀석이에요. 이후, 요청을 적절한 Controller로 전달하고, 로직을 실행한 후 응답을 생성해줍니다.</p>
<h4 id="dispatcherservlet의-처리-과정">DispatcherServlet의 처리 과정</h4>
<ol>
<li><strong>요청 수신</strong>: 클라이언트가 보낸 모든 HTTP 요청을 수신.  </li>
<li><strong>Handler Mapping</strong>: 요청 URL에 따라 적합한 컨트롤러를 검색.  </li>
<li><strong>Controller 실행</strong>: 컨트롤러에서 비즈니스 로직을 처리하고 결과를 반환.  </li>
<li><strong>View Resolver</strong>: 처리 결과를 View로 렌더링하여 응답 생성.  </li>
<li><strong>클라이언트 응답</strong>: HTML, JSON 등의 형태로 결과를 반환.  </li>
</ol>
<h4 id="dispatcherservlet의-특징">DispatcherServlet의 특징</h4>
<ul>
<li><strong>Front Controller 패턴 구현</strong>: 모든 요청을 중앙에서 관리.  </li>
<li><strong>Controller-서비스-DAO 계층 구조</strong>를 통해 코드의 모듈화와 유지보수를 용이하게 만듦.  </li>
</ul>
<h4 id="예제-코드">예제 코드</h4>
<pre><code class="language-java">@Controller
public class MyController {

    @GetMapping(&quot;/umc&quot;)
    public String hello(Model model) {
        model.addAttribute(&quot;message&quot;, &quot;UMC Spring Fighting!&quot;);
        return &quot;greeting&quot;;  
    }
}</code></pre>
<ol>
<li>클라이언트가 <code>/umc</code> URL을 요청하면, <strong>DispatcherServlet</strong>이 요청을 수신. </li>
<li>DispatcherServlet의 Handler Mapping: DispatcherServlet가 해당 HTTP Request를 처리할 수 있는 적합한 컨트롤러(<code>MyController</code>)를 찾아 <code>hello()</code> 메서드를 호출.  </li>
<li>비즈니스 로직을 처리한 결과를 View 이름(<code>greeting</code>)으로 반환.  </li>
<li>ViewResolver가 HTML 페이지를 렌더링하여 클라이언트에 응답.</li>
</ol>
<hr>
<h3 id="서블릿과-스프링-빈의-관계"><strong>서블릿과 스프링 빈의 관계</strong></h3>
<p>전체적인 흐름을 이해하기 위해, 서블릿과 스프링 빈 어떤 관계인지 살펴보고 마무리합시다!</p>
<h4 id="1-서블릿은-웹-애플리케이션의-핵심-요청-응답을-처리">1. 서블릿은 웹 애플리케이션의 핵심 요청-응답을 처리</h4>
<ul>
<li>서블릿은 클라이언트의 HTTP 요청을 가장 먼저 수신하여 처리하는 역할을 합니다.  </li>
</ul>
<h4 id="2-스프링-빈은-서블릿의-요청-처리를-돕는-객체">2. 스프링 빈은 서블릿의 요청 처리를 돕는 객체</h4>
<ul>
<li>서블릿(특히 Spring의 <strong>DispatcherServlet</strong>)이 HTTP 요청을 처리할 때, 요청을 <strong>스프링 빈</strong>(Controller, Service, Repository 등)에 전달하여 필요한 작업을 수행합니다.  </li>
</ul>
<h4 id="3-서블릿은-스프링-애플리케이션의-일부로-포함">3. 서블릿은 스프링 애플리케이션의 일부로 포함</h4>
<ul>
<li><strong>DispatcherServlet</strong>은 서블릿의 한 종류이며, 스프링 MVC에서 모든 요청을 받아 스프링 빈과 상호작용하며 작업을 처리합니다.  </li>
</ul>
<hr>
<h3 id="서블릿과-스프링-빈이-협력하는-과정"><strong>서블릿과 스프링 빈이 협력하는 과정</strong></h3>
<ol>
<li><p><strong>서블릿 수신</strong><br>클라이언트가 보낸 HTTP 요청은 서블릿 컨테이너(Tomcat 등)로 전달되고, 해당 요청이 <strong>DispatcherServlet</strong>으로 처리됩니다.  </p>
</li>
<li><p><strong>DispatcherServlet 역할</strong><br>요청을 분석하여 적절한 <strong>스프링 빈(Controller)</strong>을 호출.  </p>
</li>
<li><p><strong>스프링 빈 처리</strong><br>Controller, Service, Repository 계층에서 비즈니스 로직을 수행하고 결과를 반환.  </p>
</li>
<li><p><strong>결과 반환</strong><br>DispatcherServlet이 처리 결과를 클라이언트에 응답.  </p>
</li>
</ol>
<h4 id="비유로-이해하기"><strong>비유로 이해하기</strong></h4>
<ol>
<li><p><strong>서블릿</strong>: 식당의 <strong>매니저</strong>  </p>
<ul>
<li>손님(클라이언트)이 요청하면 매니저(서블릿)가 요청을 접수하고 주방이나 계산대로 전달.  </li>
<li>최종 결과(요리나 계산 결과)를 손님에게 전달.  </li>
</ul>
</li>
<li><p><strong>스프링 빈</strong>: 식당의 <strong>직원</strong>  </p>
<ul>
<li>매니저가 전달한 요청을 실제로 처리.  </li>
<li>요리사(비즈니스 로직), 서빙 직원(데이터 전달), 계산원(데이터 처리)이 모두 스프링 빈의 역할.  </li>
</ul>
</li>
</ol>
<p>정리하자면,</p>
<ul>
<li><strong>서블릿</strong>: 클라이언트 요청을 처리하고 응답을 생성하는 <strong>웹 애플리케이션의 요청-응답 처리자</strong>.  </li>
<li><strong>스프링 빈</strong>: 애플리케이션의 비즈니스 로직과 핵심 기능을 담당하며, <strong>스프링 컨테이너에서 관리</strong>.  </li>
</ul>
<p>둘은 역할이 다르지만, 협력하여 <strong>클라이언트 요청을 처리</strong>하고 <strong>애플리케이션의 동작을 구현</strong>합니다.  </p>
<hr>
<h3 id="🎯-핵심-키워드-정리">🎯 <strong>핵심 키워드 정리</strong></h3>
<p>오늘 배운 내용 중 핵심 키워드를 정리해보겠습니다. </p>
<ul>
<li><strong>DI (Dependency Injection)</strong>: 객체 의존성을 외부에서 주입하여 결합도를 낮추는 기법.  </li>
<li><strong>IoC (Inversion of Control)</strong>: 객체 생성과 제어를 개발자가 아닌 스프링 컨테이너에 위임.  </li>
<li><strong>프레임워크 vs API</strong>: 프레임워크는 코드 흐름 제공, API는 상호작용 인터페이스 제공.  </li>
<li><strong>스프링 빈</strong>: 스프링 컨테이너에서 관리되는 객체.  </li>
<li><strong>DispatcherServlet</strong>: 클라이언트 요청을 적절한 컨트롤러로 전달하는 서블릿.  </li>
</ul>
<hr>
<h3 id="💡-정리하며">💡 <strong>정리하며</strong></h3>
<p>스프링의 DI, IoC, DispatcherServlet 개념은 애플리케이션 설계의 복잡성을 줄이고 유지보수를 쉽게 만드는 핵심입니다. 이 기본 개념을 통해, 스프링 기반의 프로젝트를 체계적으로 설계할 수 있습니다.<br>다음 시간부터는 오늘 배운 스프링부트를 본격적으로 학습하면서 JPA에 대해 배워보도록 하겠습니다.😊</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 3. API URL의 설계 & 프로젝트 세팅]]></title>
            <link>https://velog.io/@funda__mental_/Chapter-3.-API-URL%EC%9D%98-%EC%84%A4%EA%B3%84-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%B8%ED%8C%85</link>
            <guid>https://velog.io/@funda__mental_/Chapter-3.-API-URL%EC%9D%98-%EC%84%A4%EA%B3%84-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%B8%ED%8C%85</guid>
            <pubDate>Sat, 05 Oct 2024 08:12:53 GMT</pubDate>
            <description><![CDATA[<p>지난주에는 <strong>ERD 설계</strong>를 통해 데이터베이스 구조를 설계하며 프로젝트의 데이터를 정의했습니다.<br>이번 주차에서는 데이터를 활용하기 위한 <strong>API 설계</strong>를 다룹니다.  </p>
<p>API는 단순히 클라이언트와 서버 간의 통신을 위한 도구가 아닙니다.<br>API 설계는 프로젝트의 기능을 <strong>구체화하고, 팀원 간 협업을 원활히 하기 위한 로드맵</strong>을 만드는 작업입니다.  </p>
<hr>
<h3 id="왜-api-설계가-중요한가요">왜 API 설계가 중요한가요?</h3>
<p>1️⃣ <strong>프로젝트의 큰 틀을 잡는 과정</strong>  </p>
<ul>
<li>API는 데이터베이스와 함께 프로젝트의 핵심 구조를 형성합니다.  </li>
<li>명확한 설계를 통해 데이터 흐름을 효율적으로 관리할 수 있습니다.  </li>
</ul>
<p>2️⃣ <strong>협업의 가이드라인 제공</strong>  </p>
<ul>
<li>API 명세서를 통해 <strong>프론트엔드, 백엔드, 기획자</strong> 모두가 같은 방향으로 작업할 수 있습니다.  </li>
<li>이를 통해 기능 구현 단계에서 발생할 수 있는 혼란을 최소화할 수 있습니다.  </li>
</ul>
<p>3️⃣ <strong>확장성과 유지보수성 확보</strong>  </p>
<ul>
<li>설계 초기부터 API 구조를 체계적으로 정의하면, 새로운 기능 추가나 변경 시에도 유연하게 대응할 수 있습니다.  </li>
</ul>
<hr>
<h3 id="api-설계에서-가장-중요한-두-가지"><strong>API 설계에서 가장 중요한 두 가지</strong></h3>
<p>1️⃣ <strong>API Endpoint 설계</strong>  </p>
<ul>
<li>각 기능이 어떤 경로(URL)를 통해 호출될지 정의.  </li>
<li>명확하고 직관적인 경로로 설계해야 협업과 유지보수가 쉬워집니다.  </li>
</ul>
<p>2️⃣ <strong>요청(Request)과 응답(Response) 데이터 설계</strong>  </p>
<ul>
<li>클라이언트가 서버에 전달할 데이터와, 서버가 반환할 데이터를 명확히 정의.  </li>
<li>데이터를 어떻게 요청하고, 어떤 형식으로 받을지를 설계합니다.  </li>
</ul>
<hr>
<h3 id="api-설계-결과물-api-명세서"><strong>API 설계 결과물: API 명세서</strong></h3>
<p>API 설계의 결과는 <strong>API 명세서</strong>라는 형태로 문서화됩니다.<br>이 명세서는 <strong>프론트엔드 개발자와 다른 팀원들이 API를 쉽게 이해하고 활용할 수 있도록 돕는 가이드</strong>가 됩니다.  </p>
<p>API 명세서는 마치 <strong>ERD의 API 버전</strong>과도 같습니다.  </p>
<ul>
<li>ERD가 데이터베이스 설계의 기반이라면, API 명세서는 시스템 간 데이터 흐름과 상호작용을 정의합니다.  </li>
</ul>
<hr>
<h3 id="이번-주차-목표">이번 주차 목표</h3>
<p>이번 포스팅에서는 <strong>API 설계의 원칙과 실전 활용 방법</strong>을 배우고, 이를 바탕으로 프로젝트의 기초를 다집니다.  </p>
<h3 id="학습-목표">학습 목표</h3>
<p>📌 <strong>1. API, HTTP 메소드 배우기</strong><br>📌 <strong>2. RESTful API Endpoint 설계 배우기</strong><br>📌 <strong>3. 세부적인 API 설계 배우기</strong>  </p>
<hr>
<h2 id="🌟-1-api-http-메소드-배우기">🌟 1. API, HTTP 메소드 배우기</h2>
<p>API 설계 시 필요한 것은 아래와 같습니다:<br>1️⃣ <strong>API Endpoint의 설계</strong><br>2️⃣ <strong>요청 데이터, 응답 데이터의 설계</strong>  </p>
<p>위의 정보를 문서화하여 <strong>프론트엔드 개발자가 API를 사용하기 쉽게 돕는 문서</strong>를 <strong>API 명세서</strong>라고 부릅니다.<br>📝 API 명세서는 ERD처럼 <strong>개발 초기 단계</strong>에서 큰 틀을 설계해야 합니다.  </p>
<hr>
<h3 id="✏️-1-1-용어-정리">✏️ 1-1. 용어 정리</h3>
<ul>
<li><p><strong>💡 API (Application Programming Interface)</strong>  </p>
<ul>
<li>애플리케이션을 프로그래밍할 때 쉽게 작업할 수 있도록 돕는 도구 또는 인터페이스.  </li>
<li><strong>예시</strong>: 네이버 로그인 API를 사용해 간단히 로그인 기능 구현.</li>
</ul>
</li>
<li><p><strong>💡 REST API</strong>  </p>
<ul>
<li>HTTP 메소드와 자원을 이용해 서로 간에 통신하는 방식.  </li>
<li>REST는 <strong>Representational State Transfer</strong>의 약자로, HTTP를 기반으로 한 웹 서비스 아키텍처.  </li>
</ul>
</li>
<li><p><strong>💡 API Endpoint</strong>  </p>
<ul>
<li>REST API에서 <strong>API 호출을 위한 HTTP 메소드와 URL의 조합</strong>.  </li>
<li><strong>예</strong>: <code>/users</code>, <code>/users/{id}</code>  </li>
<li><strong>구성 요소</strong>: HTTP 메소드 + URL = <strong>API Endpoint</strong>  </li>
</ul>
</li>
<li><p><strong>💡 HTTP 메소드</strong>  </p>
<ul>
<li>REST 통신 시 작업의 종류를 표시하는 방법.  </li>
<li>CRUD 작업에 대응하는 대표적인 5가지 메소드:<br>1️⃣ <strong>GET</strong>: 조회<br>2️⃣ <strong>POST</strong>: 생성 및 데이터 전송<br>3️⃣ <strong>PUT</strong>: 전체 갱신<br>4️⃣ <strong>PATCH</strong>: 부분 갱신<br>5️⃣ <strong>DELETE</strong>: 삭제  </li>
</ul>
</li>
</ul>
<blockquote>
<p>⚠️ <strong>주의</strong>: 모든 작업을 <code>POST</code>로 구현 가능하나, RESTful 설계 원칙을 지키려면 <strong>적합한 메소드</strong>를 선택해야 합니다!  </p>
</blockquote>
<hr>
<h2 id="🌟-2-restful-api-endpoint의-설계-규칙">🌟 2. RESTful API Endpoint의 설계 규칙</h2>
<p>RESTful의 핵심은 &quot;리소스 중심&quot; 설계에 있습니다.
RESTful(Representational State Transfer) 아키텍처 스타일은 HTTP 프로토콜을 기반으로 한 웹 서비스 설계 방식으로, 주로 <strong>리소스(Resource)</strong>를 중심으로 동작합니다.
즉, RESTful API는 리소스를 URI(Uniform Resource Identifier)로 표현하고, 이를 처리하기 위한 HTTP 메소드를 사용합니다.</p>
<hr>
<h3 id="2-1-restful-api-endpoint-설계의-의미">2-1. RESTful API Endpoint 설계의 의미</h3>
<p>API Endpoint 설계는 리소스와 작업을 조합해 사용자가 원하는 기능을 제공하는 경로(URL)를 정의하는 것을 의미합니다.</p>
<p><strong>예를 들어, &quot;게시글의 목록을 조회&quot;하는 API가 필요하다고 가정해 봅시다.</strong></p>
<ul>
<li>리소스: 게시글(<code>posts</code>)</li>
<li>작업: 조회(<code>GET</code>)</li>
<li>엔드포인트 설계:<ul>
<li><strong>GET /posts</strong>: 게시글 목록을 조회</li>
</ul>
</li>
</ul>
<p><strong>또 다른 예: &quot;특정 사용자의 게시글 목록을 조회&quot;하는 API</strong></p>
<ul>
<li>리소스: 게시글(<code>posts</code>), 사용자(<code>users</code>)</li>
<li>작업: 조회(<code>GET</code>)</li>
<li>엔드포인트 설계:<ul>
<li><strong>GET /users/{userId}/posts</strong>: 특정 사용자의 게시글 목록 조회</li>
</ul>
</li>
</ul>
<hr>
<h3 id="2-2-설계-규칙">2-2. 설계 규칙</h3>
<p>RESTful API의 Endpoint는 <strong>아래 규칙</strong>에 따라 설계합니다:  </p>
<p>1️⃣ <strong>URI에 동사를 포함하지 않는다.</strong>  </p>
<ul>
<li>❌ <code>/getUsers</code>  </li>
<li>✅ <code>/users</code>  </li>
<li>작업은 HTTP 메소드로 표현하므로, URI에 동사를 포함할 필요가 없습니다.</li>
</ul>
<p>2️⃣ <strong>단어 구분은 하이픈(<code>-</code>)으로 한다.</strong>  </p>
<ul>
<li>❌ <code>/users_list</code>  </li>
<li>✅ <code>/users-list</code>  </li>
</ul>
<p>3️⃣ <strong>자원(Resource)은 복수형으로 표현한다.</strong>  </p>
<ul>
<li>❌ <code>/user</code>  </li>
<li>✅ <code>/users</code>  </li>
<li>리소스는 여러 개의 데이터를 다룰 수 있으므로 복수형으로 설계합니다.</li>
</ul>
<p>4️⃣ 단일 자원을 표현할 경우 <strong>식별자(<code>id</code>)를 추가로 사용</strong>.  </p>
<ul>
<li>✅ <code>/users/{id}</code>  </li>
<li>특정 데이터를 지칭할 때 고유 식별자(ID)를 URI에 포함해야 합니다.</li>
</ul>
<p>5️⃣ <strong>자원 간 연관 관계는 URI에 명시한다.</strong>  </p>
<ul>
<li>✅ <code>/users/{id}/subjects</code> </li>
<li>계층 구조를 사용하여 자원 간 관계를 표현해야 합니다.</li>
</ul>
<blockquote>
<p>💡 <strong>Tip</strong>: 설계 규칙을 기본적으로 따르되, 프론트엔드 개발자가 <strong>이해하기 쉬운 API</strong>를 설계하는 것이 가장 중요합니다.  </p>
</blockquote>
<h4 id="🧩-자원-간-관계-표현">🧩 자원 간 관계 표현</h4>
<ul>
<li><p><strong>1:1 관계</strong>:  </p>
<ul>
<li>특정 자원만을 명확히 표현.  </li>
<li>✅ <code>/users/{id}/subjects/{subject-id}</code>  </li>
</ul>
</li>
<li><p><strong>1:N 관계</strong>:  </p>
<ul>
<li>계층 관계에서 상위 자원이 먼저 위치.  </li>
<li>✅ <code>/users/{id}/subjects</code>  </li>
</ul>
</li>
<li><p><strong>N:M 관계</strong>:  </p>
<ul>
<li>비즈니스 로직에서 중요한 자원을 앞에 배치.  </li>
</ul>
</li>
</ul>
<hr>
<h3 id="🔑-예시-회원가입-및-로그인">🔑 <strong>예시: 회원가입 및 로그인</strong></h3>
<ul>
<li><p><strong>로그인</strong>:  </p>
<ul>
<li><code>POST /users/login</code>  </li>
<li><strong>예시 요청</strong>:  <pre><code class="language-http">POST https://example.com/users/login</code></pre>
</li>
</ul>
</li>
<li><p><strong>회원가입</strong>:  </p>
<ul>
<li><code>POST /users</code>  </li>
</ul>
</li>
<li><p><strong>회원탈퇴 (Soft Delete)</strong>:  </p>
<ul>
<li><code>PATCH /users</code>  </li>
</ul>
</li>
</ul>
<hr>
<h2 id="🌟-3-세부적인-api-설계-배우기">🌟 3. 세부적인 API 설계 배우기</h2>
<h3 id="3-1-데이터-전달-방식">3-1. 데이터 전달 방식</h3>
<h4 id="1️⃣-path-variable">1️⃣ <strong>Path Variable</strong></h4>
<ul>
<li>특정 자원을 식별할 때 사용.  </li>
<li><strong>예시</strong>:  <pre><code class="language-http">GET /users/articles/{article-id}</code></pre>
</li>
</ul>
<h4 id="2️⃣-query-string">2️⃣ <strong>Query String</strong></h4>
<ul>
<li>검색 및 필터링 시 사용.  </li>
<li><strong>엔드포인트 뒤에 <code>?</code>로 시작</strong>, 여러 값은 <code>&amp;</code>로 연결.  </li>
<li><strong>예시</strong>:  <pre><code class="language-http">GET /users/articles?name=mango&amp;owner=yappi</code></pre>
</li>
</ul>
<h4 id="3️⃣-request-body">3️⃣ <strong>Request Body</strong></h4>
<ul>
<li>데이터를 생성하거나 업데이트할 때 사용.  </li>
<li>JSON 또는 Form-data 형식으로 전송.  </li>
<li><strong>예시</strong>:  <pre><code class="language-json">{
  &quot;name&quot;: &quot;최용욱&quot;,
  &quot;phoneNum&quot;: &quot;010-1111-2222&quot;,
  &quot;nickName&quot;: &quot;ddol&quot;
}</code></pre>
</li>
</ul>
<h4 id="4️⃣-request-header">4️⃣ <strong>Request Header</strong></h4>
<ul>
<li>서버와 클라이언트 간의 메타데이터를 전달.  </li>
<li><strong>예시</strong>:  <pre><code class="language-http">Authorization: Bearer &lt;token&gt;
Content-Type: application/json</code></pre>
</li>
</ul>
<hr>
<h2 id="🌟-4-api-명세서-예시">🌟 4. API 명세서 예시</h2>
<ul>
<li>요즘에는 <strong>노션</strong>을 사용해 작성하는 경우가 많습니다. </li>
</ul>
<p><strong>API 명세서 예시 스크린샷</strong><br><img src="https://velog.velcdn.com/images/funda__mental_/post/b9d15aea-d3e9-4016-8601-40dc1f9b7c62/image.png" alt="API 명세서">  </p>
<p>최종적으로는, SWAGGER을 사용해서 API 명세서를 프론트엔드에게 넘겨주게 되고, 프론트엔드를 이를 확인하면서 협업하게 됩니다. 
<img src="https://velog.velcdn.com/images/funda__mental_/post/d05db662-2b23-44f7-9797-405e92c8f678/image.png" alt=""></p>
<hr>
<h3 id="🔎-참고-html-form-방식의-데이터-전송">🔎 참고: HTML Form 방식의 데이터 전송</h3>
<ul>
<li>POST 요청 시, Form 데이터를 전송하는 방식.  </li>
<li>주로 회원가입, 상품 주문 등에 사용.  </li>
</ul>
<h4 id="📤-http-메시지-구조">📤 <strong>HTTP 메시지 구조</strong></h4>
<p><img src="https://velog.velcdn.com/images/funda__mental_/post/1a56baff-d51c-43c6-98b1-b91dc28b9950/image.png" alt="HTML Form">  </p>
<hr>
<p>이번 시간에는 백엔드 설계에서 매우 중요한 API 설계에 대해 배워봤습니다! 다음 포스팅에는 본격적으로 Spring Boot에 대해 배워보면서 실제로 개발을 해볼 준비를 해봅시다!😊</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 2. 실전 SQL - Query 작성 방법]]></title>
            <link>https://velog.io/@funda__mental_/Chapter-2.-%EC%8B%A4%EC%A0%84-SQL-Query-%EC%9E%91%EC%84%B1-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@funda__mental_/Chapter-2.-%EC%8B%A4%EC%A0%84-SQL-Query-%EC%9E%91%EC%84%B1-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sat, 05 Oct 2024 07:29:02 GMT</pubDate>
            <description><![CDATA[<p>이번에는 실제로 SQL을 사용해서 어떻게 데이터베이스를 만드는 쿼리를 작성해야하는지 알아보겠습니다. </p>
<hr>
<h2 id="목차">목차</h2>
<ol start="0">
<li>기본개념 - Join과 SubQuery 개념</li>
<li>여러가지 요구 사항에 대한 SQL 쿼리 짜는 방법 배우기</li>
<li>offset paging(페이징)과 cusor-based paging 배우기</li>
</ol>
<hr>
<h2 id="0-기본개념---join과-subquery-개념">0. 기본개념 - Join과 SubQuery 개념</h2>
<p>해당 주제는 스터디 워크북에서 sql 문법에 대해 잘 알고 있다고 가정하고 시작했기에, 아래 블로그들을 참고해서 사전공부를 진행했습니다! 
<a href="https://hongong.hanbit.co.kr/sql-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95-joininner-outer-cross-self-join/">SQL 기본 문법: JOIN(INNER, OUTER, CROSS, SELF JOIN)</a>
<a href="https://velog.io/@wijoonwu/JOIN">[SQL] 테이블 JOIN의 개념과 예제</a>
<a href="https://inpa.tistory.com/entry/MYSQL-%F0%9F%93%9A-JOIN-%EC%A1%B0%EC%9D%B8-%EA%B7%B8%EB%A6%BC%EC%9C%BC%EB%A1%9C-%EC%95%8C%EA%B8%B0%EC%89%BD%EA%B2%8C-%EC%A0%95%EB%A6%AC">[MYSQL] 📚 테이블 조인(JOIN) - 그림으로 알기 쉽게 정리</a>
<a href="https://snowple.tistory.com/360">SQL / MySQL 서브쿼리(SubQuery)</a>
<a href="https://inpa.tistory.com/entry/MYSQL-%F0%9F%93%9A-%EC%84%9C%EB%B8%8C%EC%BF%BC%EB%A6%AC-%EC%A0%95%EB%A6%AC">[MYSQL] 📚 서브쿼리 개념 &amp; 문법 💯 정리</a></p>
<h3 id="01-join">0.1. Join</h3>
<p>1) <span style="background-color:#fff5b1"> <strong>join</strong></span>: 서로 다른 각각의 테이블 속 데이터를 동시에 보여주려고 할 때 사용하는 SQL문</p>
<p>2)** join의 종류**</p>
<ul>
<li>inner join: 가장 basic, 양측 모두에 존재하는 것만 조인 (교집합)</li>
<li>outer join<ul>
<li>left join: 좌측 테이블 데이터에 추가로 우측 정보를 조인하는 문법, 좌측 테이블 중 조인 불가능한 것들도 모두 결과(Null)로 채운다.<ul>
<li>right join: LEFT 조인을 뒤집은 개념</li>
<li>full join: 좌측,우측 상관없이 데이터가 있는 것은 모두 가져오고 없는 것은 모두 Null 이 되는 것 (합집합)</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/funda__mental_/post/c8fd4fdc-9516-452b-b3ce-b3aa1a37aa0a/image.png" alt=""></p>
<p>3) <strong>형식</strong></p>
<pre><code>-- inner join
SELECT &lt;열 목록&gt;
FROM &lt;첫 번째 테이블&gt;
    INNER JOIN &lt;두 번째 테이블&gt;
    ON &lt;조인 조건&gt;
[WHERE 검색 조건]

#INNER JOIN을 JOIN이라고만 써도 INNER JOIN으로 인식</code></pre><pre><code>-- outer join
SELECT &lt;열 목록&gt;
FROM &lt;첫 번째 테이블(LEFT 테이블)&gt;
    &lt;LEFT | RIGHT | FULL&gt; OUTER JOIN &lt;두 번째 테이블(RIGHT 테이블)&gt;
     ON &lt;조인 조건&gt;
[WHERE 검색 조건]</code></pre><p>4) <strong>예시 쿼리</strong></p>
<pre><code class="language-sql">-- (inner) join
SELECT
  comments.body,
  users.nickname
FROM
  comments
JOIN users ON
  users.id = comments.user_id
WHERE
  comments.photo_id = 1
;

-- outer left join
SELECT photos.filename, users.nickname
FROM photos
LEFT JOIN users ON users.id = photos.user_id</code></pre>
<h3 id="02-subquery">0.2. Subquery</h3>
<p>1) <span style="background-color:#fff5b1"> <strong>Subquery</strong></span>: 쿼리 안에 포함된 또 다른 쿼리. 주로 데이터 검색, 조건 필터링, 집계 연산 등 특정한 데이터를 추출하거나 처리할 때 사용.</p>
<p>2) <strong>Subquery의 특징 및 종류</strong>  </p>
<ul>
<li><p><strong>특징</strong>:  </p>
<ul>
<li><code>()</code> 안에 작성되며, 메인 쿼리에서 활용됨.  </li>
<li>독립적으로 실행 가능한 쿼리로, 메인 쿼리에서 필요로 하는 데이터를 반환함.  </li>
<li>반환값 형태에 따라 스칼라 값, 단일 행, 다중 행, 다중 열로 나뉨.  </li>
</ul>
</li>
<li><p><strong>종류</strong>:  </p>
<ul>
<li><strong>스칼라 서브쿼리</strong>: 하나의 값을 반환하며, SELECT 문의 필드나 조건에 사용.  </li>
<li><strong>다중 행 서브쿼리</strong>: 여러 행을 반환하며, <code>IN</code>, <code>ANY</code>, <code>ALL</code>과 같은 연산자와 함께 사용.  </li>
<li><strong>다중 열 서브쿼리</strong>: 여러 열을 반환하며, 일반적으로 <code>EXISTS</code>와 함께 사용.  </li>
<li><strong>상관 서브쿼리 (Correlated Subquery)</strong>: 메인 쿼리의 값을 참조하여 동작. 서브쿼리가 메인 쿼리와 반복적으로 실행됨.  </li>
</ul>
</li>
</ul>
<p>3) <strong>형식</strong>  </p>
<pre><code class="language-sql">-- 일반적인 서브쿼리
SELECT &lt;열 목록&gt;
FROM &lt;테이블&gt;
WHERE &lt;열&gt; = (
    SELECT &lt;열&gt;
    FROM &lt;테이블&gt;
    WHERE &lt;조건&gt;
);

-- 다중 행 서브쿼리
SELECT &lt;열 목록&gt;
FROM &lt;테이블&gt;
WHERE &lt;열&gt; IN (
    SELECT &lt;열&gt;
    FROM &lt;테이블&gt;
    WHERE &lt;조건&gt;
);

-- 상관 서브쿼리
SELECT &lt;열 목록&gt;
FROM &lt;테이블&gt; T1
WHERE EXISTS (
    SELECT 1
    FROM &lt;테이블&gt; T2
    WHERE T2.&lt;열&gt; = T1.&lt;열&gt; AND &lt;조건&gt;
);</code></pre>
<p>4) <strong>예시 쿼리</strong>  </p>
<pre><code class="language-sql">-- 1. 스칼라 서브쿼리: 가장 높은 급여를 받는 직원의 이름 출력
SELECT name
FROM employees
WHERE salary = (
    SELECT MAX(salary)
    FROM employees
);

-- 2. 다중 행 서브쿼리: 특정 부서에서 근무하는 직원의 이름 출력
SELECT name
FROM employees
WHERE department_id IN (
    SELECT department_id
    FROM departments
    WHERE location = &#39;New York&#39;
);

-- 3. 상관 서브쿼리: 자신보다 급여를 적게 받는 직원의 수 출력
SELECT name, (
    SELECT COUNT(*)
    FROM employees E2
    WHERE E2.salary &lt; E1.salary
) AS lower_salary_count
FROM employees E1;</code></pre>
<h3 id="03-join-vs-subquery-비교">0.3. Join vs Subquery 비교</h3>
<ul>
<li><strong>Join</strong>은 여러 테이블의 데이터를 결합하여 하나의 결과로 출력하는 데 사용.  </li>
<li><strong>Subquery</strong>는 메인 쿼리에 필요한 데이터를 제공하기 위한 용도로 사용.  </li>
<li>성능 면에서 Join이 일반적으로 효율적이며, Subquery는 간단한 작업에서 가독성을 높이는 데 유리.  </li>
</ul>
<blockquote>
<p><strong>Tip</strong>: Join은 <strong>관계형 데이터 처리</strong>에서 성능과 효율성을 중시할 때 유용하며, Subquery는 <strong>특정 데이터 추출</strong>이나 <strong>조건 필터링</strong>에 적합.  </p>
</blockquote>
<hr>
<p>2) <strong>조인과 서브쿼리의 형식 비교</strong>  </p>
<table>
<thead>
<tr>
<th><strong>구분</strong></th>
<th><strong>조인</strong></th>
<th><strong>서브쿼리</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>작성 방식</strong></td>
<td>여러 테이블을 JOIN 절로 연결하여 하나의 결과 집합으로 결합.</td>
<td>쿼리 내부에 또 다른 쿼리를 포함 (<code>()</code> 안에 작성).</td>
</tr>
<tr>
<td><strong>주요 키워드</strong></td>
<td><code>JOIN</code>, <code>ON</code>, <code>LEFT</code>, <code>RIGHT</code>, <code>FULL</code>, <code>INNER</code> 등.</td>
<td><code>SELECT</code>, <code>WHERE</code>, <code>IN</code>, <code>EXISTS</code>, <code>=</code>, <code>&lt;</code>, <code>&gt;</code> 등.</td>
</tr>
<tr>
<td><strong>사용 위치</strong></td>
<td>FROM 절에서 테이블을 결합하거나 필요한 데이터를 조건에 따라 연결.</td>
<td>메인 쿼리의 SELECT, WHERE, FROM, HAVING 절 등에 사용.</td>
</tr>
<tr>
<td><strong>반환 값</strong></td>
<td>하나의 테이블 형태로 반환되며 모든 연결된 열과 데이터가 결과에 포함됨.</td>
<td>스칼라 값, 단일 행, 다중 행, 다중 열 등.</td>
</tr>
<tr>
<td><strong>종속성</strong></td>
<td><strong>완전히 통합된 실행</strong>으로, 모든 연결된 테이블이 한 번에 처리됨.</td>
<td><strong>독립적 실행 가능</strong>하며 메인 쿼리와 연결되거나, 상관 서브쿼리로 메인 쿼리에 종속될 수 있음.</td>
</tr>
<tr>
<td><strong>복잡성</strong></td>
<td>여러 테이블을 연결하므로 관계가 명확히 정의되지 않으면 혼란스러울 수 있음.</td>
<td>쿼리 안에 쿼리가 포함되어 논리가 중첩되므로 이해와 디버깅이 어려울 수 있음.</td>
</tr>
<tr>
<td><strong>성능</strong></td>
<td>일반적으로 더 효율적이며, 특히 큰 데이터셋이나 복잡한 조건에서도 성능이 우수.</td>
<td>작은 데이터셋이나 간단한 조건에서는 효율적. 하지만 중첩 쿼리가 많아지면 성능 저하 가능.</td>
</tr>
<tr>
<td><strong>예시</strong></td>
<td>아래 참고</td>
<td>아래 참고</td>
</tr>
</tbody></table>
<hr>
<p>3) <strong>조인 형식 예시</strong>  </p>
<pre><code class="language-sql">-- 조인: 두 테이블 연결
SELECT employees.name, departments.department_name
FROM employees
INNER JOIN departments
ON employees.department_id = departments.department_id;

-- 조인: 왼쪽 테이블의 모든 데이터 포함
SELECT products.product_name, orders.order_date
FROM products
LEFT JOIN orders
ON products.product_id = orders.product_id;</code></pre>
<p>4) <strong>서브쿼리 형식 예시</strong>  </p>
<pre><code class="language-sql">-- 서브쿼리: 특정 조건을 만족하는 데이터를 추출
SELECT name
FROM employees
WHERE department_id = (
    SELECT department_id
    FROM departments
    WHERE department_name = &#39;Sales&#39;
);

-- 서브쿼리: 집계된 데이터를 조건으로 사용
SELECT product_id, product_name
FROM products
WHERE price &gt; (
    SELECT AVG(price)
    FROM products
);</code></pre>
<hr>
<p>5) <strong>핵심 차이점 요약</strong>  </p>
<ol>
<li><p><strong>사용 용도</strong>:  </p>
<ul>
<li>조인은 여러 테이블의 데이터를 결합하고, 테이블 간의 관계를 명확히 정의하는 데 유용.  </li>
<li>서브쿼리는 특정 조건을 만족하는 데이터를 추출하거나 필터링에 적합.  </li>
</ul>
</li>
<li><p><strong>성능</strong>:  </p>
<ul>
<li>조인이 일반적으로 더 빠르고 효율적.  </li>
<li>서브쿼리는 직관적이지만 데이터 양이 많으면 비효율적일 수 있음.  </li>
</ul>
</li>
<li><p><strong>가독성</strong>:  </p>
<ul>
<li>조인은 관계형 데이터를 다룰 때 명확성을 높여줌.  </li>
<li>서브쿼리는 간단한 작업에서 더 읽기 쉽고 간단.  </li>
</ul>
</li>
</ol>
<p><em>참고) join과 서브쿼리의 성능 차이의이유</em>
이유: 내부 처리 방식의 차이</p>
<ul>
<li><strong>JOIN</strong>: 데이터베이스는 JOIN을 할 때 <strong>두 테이블을 결합</strong>하며, 조인된 데이터를 한 번에 처리하도록 <strong>쿼리 최적화</strong>를 적용할 수 있다. 이를 통해 <strong>단일 스캔</strong>으로 결과를 가져올 수 있다.</li>
<li><strong>서브쿼리</strong>: 서브쿼리는 외부 쿼리의 <strong>각 행에 대해 독립적으로 실행</strong>되므로, 각 행마다 서브쿼리를 처리하는 오버헤드가 발생. 특히 서브쿼리가 <code>SELECT</code>와 같은 필터링을 포함할 때, <strong>반복적인 조회</strong>로 인해 성능이 느려질 수 있음.</li>
</ul>
<p>⇒ JOIN을 기본으로 사용하고, 서브쿼리는 필요한 경우에만 활용하는 것이 좋다!!</p>
<hr>
<hr>
<h2 id="1-여러가지-요구-사항에-대한-sql-쿼리-짜는-방법">1. 여러가지 요구 사항에 대한 SQL 쿼리 짜는 방법</h2>
<p>이제 실제로 여러 시나리오를 살펴보면서 SQL문을 어떻게 짜는지 살펴보자. </p>
<h3 id="시나리오-1">시나리오 1</h3>
<blockquote>
<p>** 책의 좋아요 개수를 집계하는데, 내가 차단한 사용자의 좋아요는 집계하지 않는 경우**</p>
</blockquote>
<p><em>두 entity에 대한 ERD는 아래와 같다</em>
<img src="https://velog.velcdn.com/images/funda__mental_/post/9545f691-4198-467d-98c5-a023705cc36d/image.png" alt="">
<img src="https://velog.velcdn.com/images/funda__mental_/post/942bdf77-915f-4409-8755-8cd819ceb738/image.png" alt=""></p>
<pre><code class="language-sql">-- 책의 아이디가 3, 내 아이디가 2라고 가정

-- 1) subquery 사용해서 구현
select count(*) from book_like where book_id = 3
    and user_id not in (select target_id from block where owner_id = 2);

-- 2) left join 사용해서 구현
select count(*)
from book_like as bl
left join block as b on bl.user_id = b.target_id and b.owner_id = 2
where bl.book_id = 3 and b.target_id is null;</code></pre>
<p>_#참고사항 - in과 on의 차이 _
sql문을 처음 배우는 나는 등호(=) 사용 대신에 in, on 등을 사용하는 걸 보고 둘의 차이점이 궁금했다. </p>
<ul>
<li><p>in: <strong>서브쿼리</strong>에서 여러 값이 나올 때 사용하는 연산자, 여러 값이나 서브쿼리에서 반환한 다중 결과와 비교가능 / not in 으로도 사용 가능</p>
<pre><code class="language-sql">  SELECT * FROM user 
  WHERE user_id IN (1, 2, 3); -- 여러 값 중 하나에 일치하는 경우

  SELECT * FROM user 
  WHERE region_id IN (SELECT id FROM region WHERE name = &#39;Seoul&#39;); -- 서브쿼리에서 반환된 값 중 일치</code></pre>
</li>
<li><p>on: <strong>JOIN 조건</strong>에서 두 테이블 간의 관계를 정의할 때 사용, 테이블을 결합할 때 특정 컬럼 값이 서로 일치하는지 확인하는 데 사용 </p>
<pre><code class="language-sql">  SELECT u.username, r.name AS region_name 
  FROM user AS u
  JOIN region AS r ON u.region_id = r.id; -- 두 테이블 간의 관계를 정의</code></pre>
</li>
</ul>
<hr>
<h3 id="시나리오-2---nm-관계-경우">시나리오 2 - n:m 관계 경우</h3>
<blockquote>
<p>** 해시태그를 통한 책의 검색인데, N : M 관계로 인해 가운데 매핑 테이블이 추가 된 경우**</p>
</blockquote>
<pre><code class="language-sql">-- 서브쿼리
select * from book where id in 
    (select book_id from book_hash_tag 
            where hash_tag_id  = (select id from hash_tag where name = &#39;mango&#39; ));

-- join
select b.*
from book as b
inner join book_hash_tag as bht on b.id = bht.book_id
inner join hash_tag as ht on bht.hash_tag_id = ht.id -- join을 두 번 진행해야 돼
where ht.name = &#39;mango&#39;;</code></pre>
<hr>
<h3 id="시나리오-3---나열">시나리오 3 - 나열</h3>
<ol>
<li><p><strong>최신 순 조회</strong></p>
<pre><code class="language-sql">select * from book order by created_at desc;</code></pre>
<p>내림차순으로 정렬해야 최신 것부터 조회된다. </p>
</li>
<li><p>*<em>좋아요 개수 순 조회 *</em>
(해당 table에 likes 칼럼이 없는 상황)</p>
</li>
</ol>
<pre><code class="language-sql">select * from book as b 
join (select count(*) as like_count
                 from book_likes
                        group by book_id) as likes on b.id = likes.book_id
order by likes.like_count desc;</code></pre>
<hr>
<h2 id="2-span-stylebackground-colorfff5b1페이징pagingspan">2. <span style="background-color:#fff5b1">페이징(paging)</span></h2>
<p>목록 조회를 시나리오3처럼 하게 되면 db의 모든 book 데이터를 다 가져오는 등의 쿼리를 하니까, 엄청난 렉이 걸려서 이에 대한 해결책이 필요하다!</p>
<p>=&gt; <strong>paging</strong> 필요 - 페이징이란, Db에서 끊어서 데이터 보내도록 쿼리 보내는 것</p>
<p><strong>paging의 종류</strong></p>
<ol>
<li>offset based 페이징<ol start="2">
<li>cursor based 페이징</li>
</ol>
</li>
</ol>
<h3 id="1-offset-based-페이징">1) Offset based 페이징</h3>
<p><img src="https://velog.velcdn.com/images/funda__mental_/post/7773477b-3ccd-4b7c-af26-443367aeb7e6/image.png" alt="">
우리가 자주 보는 이런 목록이 오프셋 페이징이다.</p>
<h4 id="1-1-offset-based-페이징-형식과-내용">1-1. Offset based 페이징 형식과 내용</h4>
<ul>
<li>limit: 한 페이지에서 보여줄 데이터 개수</li>
<li>offset으로 몇 개를 건너뛸지를 정함</li>
</ul>
<pre><code class="language-sql">-- 형식
select * 
from book
order by likes desc
limit y offset(x - 1) * y; -- 보통 1페이지가 첫 페이지이기 때문에 1 빼줌

-- 예시: 2페이지이고 한 페이지에 10개씩 보여주는 경우(x=2, y=10)
select *
from book
order by likes desc
limit 10 offset 10; 

-- 인기순 정렬
select * from book as b
join (select count(*) as like_count
                 from book_likes 
                    group by book_id) as likes on b.id = likes.book_id
order by likes.like_count desc
limit 15 offset (n - 1) * 15; -- 실제로는 n자리에 계산해서 넣어야 해</code></pre>
<h4 id="1-2-offset-based-페이징-단점">1-2. Offset based 페이징 단점</h4>
<ol>
<li>페이지가 뒤로 갈수록 넘어가는 데이터가 많아져 성능 상의 이슈</li>
<li>사용자가 다음 페이지로 넘어갈 때, 게시글이 추가되면 전 페이지에서 보이던 게 다음 페이지에도 보이게 됨.</li>
</ol>
<h4 id="1-3-offset-based-페이징-형식-동적으로-구현하기">1-3. Offset based 페이징 형식 동적으로 구현하기</h4>
<p>=&gt; 변수 활용: <strong>클라이언트 측</strong>에서 <strong>마지막 레코드의 커서 값</strong>(예: <code>created_at</code>이나 <code>id</code>)을 서버로 전송하고, 서버에서 이를 받아 SQL 쿼리의 조건으로 사용</p>
<ol>
<li><p><strong>SQL에서 변수를 사용하는 방법 (Prepared Statements 또는 Dynamic SQL)</strong>
대부분의 프로그래밍 언어에서 <strong>SQL 쿼리에 변수를 넣어</strong> 동적으로 커서 값을 적용 가능하다. Python, Node.js, Java 등과 같은 언어에서는 <strong>Prepared Statements</strong>를 사용하여 안전하게 변수를 SQL 쿼리에 삽입한다.</p>
<p><strong>예시</strong> (Python에서 MySQL 쿼리 실행):<code>cursor_value</code>와 <code>user_id</code> 값을 쿼리에 넣고, 동적으로 결과를 가져오기</p>
<pre><code class="language-python">cursor_value = &#39;2024-10-04 12:00:00&#39;  # 클라이언트로부터 받은 마지막 커서 값

sql = &quot;&quot;&quot;
   SELECT mission.restaurant_id
   FROM mission
   WHERE mission.created_at &lt; 
     AND mission.status = &#39;진행중&#39;
     AND mission.user_id = %s
   ORDER BY mission.created_at DESC
   LIMIT 5;
&quot;&quot;&quot;
cursor.execute(sql, (cursor_value, 3))  # 커서 값과 user_id 값을 쿼리에 동적으로 넣음
results = cursor.fetchall()
</code></pre>
</li>
</ol>
<hr>
<ol start="2">
<li><p><strong>변수를 이용한 동적 SQL 쿼리 (MySQL 변수 사용)</strong>
MySQL에서 쿼리 안에 <strong>변수</strong>를 직접 사용하려면, <strong>MySQL 세션 변수</strong> 또는 프로시저 내에서 <strong>입력 매개변수</strong>를 활용</p>
<p>** 예시** (MySQL 변수 사용): <strong><code>@변수명</code></strong> 형식으로 MySQL 내에서 변수를 정의하고, 이 변수를 SQL 쿼리 안에 넣음</p>
<pre><code class="language-sql">SET @cursor_value = &#39;2024-10-04 12:00:00&#39;;
SET @user_id = 3;

SELECT mission.restaurant_id
FROM mission
WHERE mission.created_at &lt; **@cursor_value**
 AND mission.status = &#39;진행중&#39;
 AND mission.user_id = @user_id
ORDER BY mission.created_at DESC
LIMIT 5;</code></pre>
</li>
</ol>
<hr>
<ol start="3">
<li><strong>클라이언트-서버 구조에서 동적 커서 페이징 구현</strong>
일반적으로 커서 기반 페이징을 구현할 때는 <strong>클라이언트-서버 구조</strong>에서 다음과 같은 방식으로 동작:<ol>
<li><strong>첫 번째 요청</strong>: 클라이언트가 서버에 페이지를 요청할 때는 커서 값이 없으므로, 서버는 기본 쿼리로 처음 5개의 레코드를 반환합니다.</li>
<li><strong>응답 데이터와 커서 값 반환</strong>: 서버는 응답할 때 <strong>마지막으로 반환한 레코드의 커서 값</strong>(예: <code>created_at</code>)을 함께 반환합니다.<pre><code class="language-python">   json
   {
     &quot;data&quot;: [...],
     &quot;next_cursor&quot;: &quot;2024-10-04 12:00:00&quot;
   }</code></pre>
<ol start="3">
<li><strong>다음 페이지 요청 시 커서 값 전송</strong>: 클라이언트는 다음 페이지를 요청할 때 <strong><code>next_cursor</code></strong> 값을 서버에 전달합니다.</li>
<li><strong>서버에서 커서 값으로 쿼리 실행</strong>: 서버는 이 커서 값을 받아, 동적으로 쿼리에 넣어 데이터를 가져옵니다.</li>
</ol>
</li>
</ol>
</li>
</ol>
<h3 id="2-cursor-based-페이징---offset-페이징-보완책">2) Cursor based 페이징 - offset 페이징 보완책</h3>
<p>앞서 말한 offset의 성능이나 여러 버그를 해결할 수 있는 또다른 페이징 방법입니다. </p>
<h4 id="2-1-cursor-based-페이징-형식과-내용">2-1. Cursor based 페이징 형식과 내용</h4>
<p>cusor paing의 경우 이름에서 유추 할 수 있듯이 커서로 무언가를 가르켜 페이징을 하는 방법입니다.</p>
<p>여기서 커서는? <strong><em>마지막으로 조회한 콘텐츠입니다.</em></strong></p>
<blockquote>
<p><strong>마지막으로 조회한 대상 그 다음부터 가져와!</strong></p>
</blockquote>
<p>이렇게 생각하면 됩니다.</p>
<p>예시를 함께 봐봅시다. </p>
<pre><code class="language-sql">-- 예시 1: 마지막으로 조회한 책의 아이디가 4인 경우

-- 원래대로 구현하면, 마지막으로 조회한 책의 아이디를 가져와서
select * from book where book.likes &lt; 
        (select likes from book where id = 4)
            order by likes desc limit 15;

-- 커서페이징으로 구현한 책 목록 조회 쿼리
select * from book where created_at &lt; 
    (select created_at from book where id = 3)
        order by created_at desc limit 15;

-- 커서페이징으로 구현한 인기순 목록 조회 쿼리
select * from book as b
     join (select count(*) as like_count 
                    from book_likes 
                        group by book_id) as likes on b.id = likes.book_id
            where likes.like_count &lt; (select count(*) from book_likes where book_id = 3)
                order by likes.like_count desc limit 15;
</code></pre>
<h4 id="2-2-위의-인기순-정렬에서-생기는-문제점과-해결">2-2. 위의 인기순 정렬에서 생기는 문제점과 해결</h4>
<blockquote>
<p>🔥 예를 들어 <strong>좋아요가 0개인 게시글이 400개</strong>이고,
<strong>공교롭게 마지막으로 조회한 책의 좋아요가 0개</strong>라면,
그리고 <strong>아직 뒤에 조회를 하지 않은 책</strong>이 있다면
=&gt; <strong>다음 페이지에 책이 목록으로 조회가 되지 않음!</strong></p>
</blockquote>
<p><strong><em>인기 순 정렬같이 같은 값이 있을 수 있는 경우, 정렬 기준이 하나 더 있어야 한다는 결론!!</em></strong></p>
<p>해결코드</p>
<pre><code class="language-sql">-- 좋아요 수가 같을 경우 PK값을 정렬 기준을 추가
select * from book as b
     join (select count(*) as like_count 
                        from book_likes
                            group by book_id) as likes on b.id = likes.book_id
            where likes.like_count &lt; (select count(*) from book_likes where book_id = 3)
                order by likes.like_count desc, b.id desc limit 15; 
                -- 좋아요 갯수가 같은 book이 15개가 넘어가면 그 이상의 것은 무시가 됨
</code></pre>
<p>LPAD 함수를 이용한 해결 코드</p>
<pre><code class="language-sql">SELECT b.*,
       CONCAT(LPAD(likes.like_count, 10, &#39;0&#39;), LPAD(b.id, 10, &#39;0&#39;)) AS cursor_value
FROM book AS b
JOIN (SELECT book_id, COUNT(*) AS like_count
      FROM book_likes
      GROUP BY book_id) AS likes ON b.id = likes.book_id
HAVING cursor_value &lt; (SELECT CONCAT(LPAD(like_count_sub.like_count, 10, &#39;0&#39;), LPAD(like_count_sub.book_id, 10, &#39;0&#39;))
                FROM (SELECT book_id, COUNT(*) AS like_count
                      FROM book_likes
                      GROUP BY book_id) AS like_count_sub
                WHERE like_count_sub.book_id = 3) # 여기에 cursor_value 값이 들어가면 됨.
ORDER BY likes.like_count DESC, b.id DESC
LIMIT 15;

--  having 절 없이, where 절로 구현하면
SELECT b.*,
       CONCAT(LPAD(likes.like_count, 10, &#39;0&#39;), LPAD(b.id, 10, &#39;0&#39;)) AS cursor_value
FROM book AS b
JOIN (SELECT book_id, COUNT(*) AS like_count
      FROM book_likes
      GROUP BY book_id) AS likes ON b.id = likes.book_id
WHERE CONCAT(LPAD(likes.like_count, 10, &#39;0&#39;), LPAD(b.id, 10, &#39;0&#39;)) &lt; 
      (SELECT CONCAT(LPAD(like_count_sub.like_count, 10, &#39;0&#39;), LPAD(like_count_sub.book_id, 10, &#39;0&#39;))
       FROM (SELECT book_id, COUNT(*) AS like_count
             FROM book_likes
             GROUP BY book_id) AS like_count_sub
       WHERE like_count_sub.book_id = 3) # 여기에 cursor_value 값이 들어가면 됨.
ORDER BY likes.like_count DESC, b.id DESC
LIMIT 15;
</code></pre>
<ul>
<li><p>LPAD 함수란?: <strong>문자열의 길이를 지정하고, 지정된 길이만큼 왼쪽을 특정 문자로 채워주는</strong> 함수</p>
<p> <code>LPAD</code> 함수의 기본 사용법</p>
<pre><code class="language-sql">  LPAD(문자열, 목표 길이, 채울 문자)</code></pre>
<ul>
<li><p><strong>문자열</strong>: 왼쪽을 채울 대상이 되는 문자열.</p>
</li>
<li><p><strong>목표 길이</strong>: 최종적으로 만들어질 문자열의 전체 길이.</p>
</li>
<li><p><strong>채울 문자</strong>: 목표 길이를 채우기 위해 문자열 왼쪽에 추가할 문자.</p>
<p>예를 들어, <code>LPAD(&#39;123&#39;, 5, &#39;0&#39;)</code>는 결과로 <code>&#39;00123&#39;</code>을 반환</p>
</li>
</ul>
<hr>
<p>  예제 쿼리
  <strong><code>cursor_value</code> 생성</strong>:</p>
<ul>
<li><p><code>CONCAT(LPAD(likes.like_count, 10, &#39;0&#39;), LPAD(b.id, 10, &#39;0&#39;))</code>를 사용하여 <code>cursor_value</code>를 생성.</p>
</li>
<li><p><code>like_count</code>와 <code>book_id</code>를 각각 10자리 숫자 문자열로 변환하고 결합하여 <strong>좋아요 수와 책 ID 기반의 고유한 정렬 기준</strong>을 만듦.</p>
</li>
<li><p><em>페이징 조건*</em>:</p>
</li>
<li><p><code>HAVING cursor_value &lt; (...)</code> 구문에서 <code>cursor_value</code>를 기준으로 커서 페이징을 구현</p>
</li>
<li><p>서브쿼리에서 <code>book_id = 3</code>인 책의 <code>cursor_value</code>를 가져와, 이 값을 기준으로 <strong>이전 책 목록을 15개까지 조회</strong></p>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="3--offset-페이징-vs-cursor-based-페이징">3)  Offset 페이징 vs Cursor based 페이징</h3>
<p>그렇다면 커서 페이징이 어떻게 offset 페이징의 문제점을 극복하는지 서로를 비교하면서 봐보겠습니다. </p>
<h4 id="1-성능-문제-극복"><strong>1. 성능 문제 극복</strong></h4>
<p>Offset 페이징의 주요 문제는 페이지가 뒤로 갈수록 <strong>많은 데이터를 건너뛰기 위해 DB가 불필요한 계산</strong>을 수행해야 한다는 점입니다.  </p>
<ul>
<li><strong>Offset 방식</strong>: <code>OFFSET</code> 값만큼 데이터를 스캔한 후, 해당 범위를 제외하고 필요한 데이터를 반환. 페이지가 뒤로 갈수록 처리 시간이 증가.  </li>
<li><strong>Cursor 방식</strong>: <code>WHERE</code> 조건을 통해 <strong>필요한 데이터만 직접 조회</strong>하므로, 건너뛰는 데이터를 처리하지 않아 효율적.  </li>
</ul>
<h5 id="예시-100만-개의-데이터에서-1만-번째-페이지를-조회"><strong>예시: 100만 개의 데이터에서 1만 번째 페이지를 조회</strong></h5>
<ul>
<li><strong>Offset 방식 쿼리</strong>:<pre><code class="language-sql">SELECT * 
FROM book 
ORDER BY created_at DESC 
LIMIT 10 OFFSET 99990; -- 앞의 99990개를 건너뜀</code></pre>
<ul>
<li>이 경우, DB는 99990개의 데이터를 스캔하고 버린 후 필요한 10개를 반환.  </li>
</ul>
</li>
<li><strong>Cursor 방식 쿼리</strong>:<pre><code class="language-sql">SELECT * 
FROM book 
WHERE created_at &lt; &#39;2024-10-04 12:00:00&#39; -- 마지막 커서 값
ORDER BY created_at DESC 
LIMIT 10;</code></pre>
<ul>
<li><code>WHERE</code> 조건을 사용하여 <strong>필요한 데이터만 조회</strong>. 건너뛰는 데이터 스캔이 없으므로 성능이 훨씬 우수.  </li>
</ul>
</li>
</ul>
<hr>
<h4 id="2-데이터-중복-및-누락-문제-극복"><strong>2. 데이터 중복 및 누락 문제 극복</strong></h4>
<p>Offset 방식은 <strong>데이터가 추가/삭제될 경우 페이지 간 데이터 중복</strong>이 발생할 가능성이 높습니다.  </p>
<ul>
<li><strong>문제 상황</strong>:  
사용자가 1페이지를 보고 2페이지로 넘어가는 사이에 새로운 데이터가 추가되면, 다음 페이지에서 이미 본 데이터가 다시 나타날 수 있음.  </li>
</ul>
<h5 id="예시"><strong>예시</strong></h5>
<ul>
<li><p><strong>Offset 방식</strong>:</p>
<ol>
<li><strong>1페이지 쿼리</strong>:<pre><code class="language-sql">SELECT * 
FROM book 
ORDER BY created_at DESC 
LIMIT 10 OFFSET 0; -- 처음 10개</code></pre>
</li>
<li><strong>2페이지 쿼리</strong>:<pre><code class="language-sql">SELECT * 
FROM book 
ORDER BY created_at DESC 
LIMIT 10 OFFSET 10; -- 다음 10개</code></pre>
<ul>
<li>1페이지와 2페이지 사이에 새로운 데이터가 추가되면, 중복 데이터가 포함될 가능성이 높음.  </li>
</ul>
</li>
</ol>
</li>
<li><p><strong>Cursor 방식</strong>:  </p>
<ol>
<li><strong>1페이지 쿼리</strong>:<pre><code class="language-sql">SELECT * 
FROM book 
ORDER BY created_at DESC 
LIMIT 10;</code></pre>
<ul>
<li>마지막 커서 값: <code>created_at = &#39;2024-10-04 12:00:00&#39;</code>.</li>
</ul>
</li>
<li><strong>2페이지 쿼리</strong>:<pre><code class="language-sql">SELECT * 
FROM book 
WHERE created_at &lt; &#39;2024-10-04 12:00:00&#39; -- 마지막 커서 기준
ORDER BY created_at DESC 
LIMIT 10;</code></pre>
<ul>
<li>새 데이터가 추가되더라도 커서 값 이후의 데이터만 조회하므로 중복 발생 없음.  </li>
</ul>
</li>
</ol>
</li>
</ul>
<hr>
<h4 id="3-안정적인-정렬-보장"><strong>3. 안정적인 정렬 보장</strong></h4>
<p>Cursor 페이징은 일반적으로 <strong>정렬 가능한 고유한 값</strong>(예: <code>created_at</code>, <code>id</code>)을 기반으로 데이터를 조회합니다.  </p>
<ul>
<li><strong>Offset 방식</strong>: 데이터 정렬 순서가 변경될 경우, 페이지 이동 시 정확한 데이터를 보장하기 어려움.  </li>
<li><strong>Cursor 방식</strong>: 특정 정렬 기준(예: <code>created_at</code>)을 유지하며 데이터를 조회하므로 일관성 있는 결과를 보장.  </li>
</ul>
<hr>
<h3 id="결론---offset-기반-페이징-vs-cursor-기반-페이징-비교-표"><strong>결론</strong>  : Offset 기반 페이징 vs Cursor 기반 페이징 비교 표</h3>
<table>
<thead>
<tr>
<th><strong>구분</strong></th>
<th><strong>Offset 기반 페이징</strong></th>
<th><strong>Cursor 기반 페이징</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>사용 방식</strong></td>
<td><code>OFFSET</code>과 <code>LIMIT</code>을 사용하여 건너뛸 데이터와 조회할 데이터 개수를 지정.</td>
<td><code>WHERE</code> 조건과 커서 값을 사용하여 필요한 데이터만 조회.</td>
</tr>
<tr>
<td><strong>성능</strong></td>
<td>페이지가 뒤로 갈수록 건너뛰는 데이터가 많아져 성능 저하 발생.</td>
<td>건너뛰는 데이터 없이 필요한 데이터만 조회하므로 성능 우수.</td>
</tr>
<tr>
<td><strong>데이터 중복 및 누락</strong></td>
<td>데이터 추가/삭제 시 중복 데이터가 발생하거나 누락될 가능성이 있음.</td>
<td>커서 값 기준으로 데이터를 조회하므로 중복 및 누락 없음.</td>
</tr>
<tr>
<td><strong>정렬 안정성</strong></td>
<td>데이터가 정렬 기준에 따라 변경될 경우, 페이지 이동 시 정확한 데이터 보장이 어려움.</td>
<td>커서 값으로 정렬 기준을 유지하며 안정적인 결과를 보장.</td>
</tr>
<tr>
<td><strong>구현 난이도</strong></td>
<td>구현이 간단하며 대부분의 DB에서 지원.</td>
<td>커서 값을 저장 및 관리해야 하므로 약간의 추가 구현이 필요.</td>
</tr>
<tr>
<td><strong>적합한 데이터 크기</strong></td>
<td>소규모 데이터셋이나 정렬이 자주 변경되지 않는 데이터셋에 적합.</td>
<td>대규모 데이터셋이나 실시간 동기화가 필요한 데이터셋에 적합.</td>
</tr>
<tr>
<td><strong>적용 시나리오</strong></td>
<td>간단한 페이지네이션이 필요한 경우 (예: 상품 목록, 기본적인 데이터 리스트).</td>
<td>대규모 소셜 피드, 실시간 채팅 로그, 검색 결과 페이지 등 성능과 데이터 정합성이 중요한 경우.</td>
</tr>
<tr>
<td><strong>쿼리 예시</strong></td>
<td><code>sql SELECT * FROM book ORDER BY created_at DESC LIMIT 10 OFFSET 30;</code></td>
<td><code>sql SELECT * FROM book WHERE created_at &lt; &#39;2024-10-04 12:00:00&#39; ORDER BY created_at DESC LIMIT 10;</code></td>
</tr>
<tr>
<td><strong>장점</strong></td>
<td>- 구현이 간단하며 대부분의 언어와 DB에서 바로 사용 가능.</td>
<td>- 성능이 우수하고, 데이터 중복 및 누락 문제가 발생하지 않음.<br>- 대규모 데이터셋에서 효율적.</td>
</tr>
<tr>
<td><strong>단점</strong></td>
<td>- 성능 저하 문제 발생 (특히 페이지가 뒤로 갈수록).<br>- 데이터 추가/삭제 시 중복 문제.</td>
<td>- 구현이 약간 복잡하며, 커서 값을 저장/관리해야 함.<br>- 정렬 기준이 고유해야 함.</td>
</tr>
</tbody></table>
<hr>
<h4 id="요약"><strong>요약</strong></h4>
<ul>
<li><strong>Offset 기반 페이징</strong>은 간단하고 빠르게 구현할 수 있지만, 대규모 데이터셋이나 실시간 데이터 동기화에는 적합하지 않음.  </li>
<li><strong>Cursor 기반 페이징</strong>은 성능과 데이터 정합성을 보장하며, 대규모 데이터 처리 및 실시간 시스템에서 매우 효과적.  </li>
</ul>
<blockquote>
<p><strong>Tip</strong>: 선택은 데이터 크기, 성능 요구사항, 정렬 기준에 따라 결정해야 하며, 대규모 데이터 처리에서는 <strong>Cursor 기반 페이징</strong>이 권장됩니다.</p>
</blockquote>
<hr>
<h2 id="오늘-배운-것-정리">오늘 배운 것 정리</h2>
<ol>
<li>sql 기본 문법인 join과 subquery</li>
<li>ERD 바탕으로 쿼리 짜는 법(데이터 가져오기, 목록 조회, ~순 나열)</li>
<li>페이징(오프셋, 커서)을 사용해서 목록을 조회하는 방법</li>
</ol>
<p>수고하셨습니다~! 다음 포스팅에는 본격적으로 API에 대해 알아보겠습니다! 🤗</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Chapter 1. Database 설계 ]]></title>
            <link>https://velog.io/@funda__mental_/Chapter-1.-Database-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@funda__mental_/Chapter-1.-Database-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Wed, 02 Oct 2024 08:12:33 GMT</pubDate>
            <description><![CDATA[<p>UMC springboot 파트에 들어가면서 관련한 스터디가 시작됐다. 이에 관한 내용을 간략히 정리하면서 남겨보려고 한다! 첫번째 내용은 백엔드 서버의 핵심인 데이터베이스와 관련된 내용이다. 데이터베이스를 효율적으로 설계하고 서버와 연결하는 것이 백엔드의 핵심이기 때문에 꼼꼼히 살펴보도록 하자. </p>
<hr>
<h3 id="1-주제">1. 주제</h3>
<ul>
<li>SQL과 NoSQL에 대해 이해</li>
<li>데이터베이스 설계 방법 학습</li>
<li>local DB 연결</li>
</ul>
<hr>
<h3 id="2-sql과-nosql">2. SQL과 NoSQL</h3>
<p>먼저 데이터베이스를 만드는 데 가장 기본이 되는 데이터베이스 언어에 대해 살펴봅시다. 언어는 크게 SQL과 NOSQL이 있는데, SQL은 Stands for Structured Query의 약자로 주로 관계형 데이터 베이스에 이용이 되고 NoSQL은 Not Only SQL의 약자로 SQL이 아닌 나머지를 의미합니다. 더 자세히 살펴봅시다. </p>
<ul>
<li><p><strong>SQL</strong>:  관계형 데이터베이스를 사용하는 시스템(RDBMS)에서 데이터를 관리하는 데 사용되는 언어 &lt;- 주로 사용!!</p>
<ul>
<li>입출력 속도보다는 <u>정확도와 입출력이 중요</u>할 때 적합(데이터를 메모리 중심이 아닌, 테이블 구조로 저장하기 때문에 디스크 I/O 작업이 많아지는 데 영향)<ul>
<li>데이터가 일관되고 테이블 간의 관계가 잘 정의된 구조화된 데이터에 좋음</li>
<li>관계를 맺고 있는 데이터가 자주 변경되는 경우 좋음<ul>
<li>NoSQL에서는 관계를 맺고 있는 컬렉션들을 모두 수정해야 하기 때문에 비효율적</li>
</ul>
</li>
<li>우리가 주로 사용하는 MySQL, PostgreSQL ...이 모두 포함</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>NoSQL</strong> = Not Only SQL의 약자로 SQL이 아닌 나머지</p>
<ul>
<li>key-value, document, graph 등 다양한 데이터 구조를 지원<ul>
<li>데이터가 미리 정의된 스키마를 따르지 않고 데이터 요소 간의 관계가 잘 정의되어 있지 않은 반정형 또는 비정형 데이터에 적합</li>
<li>막대한 양의 데이터를 다뤄야 하는 경우 적합</li>
<li>MongoDB, Redis 등 존재
<img src="https://velog.velcdn.com/images/funda__mental_/post/19075840-b282-4569-aa7a-9eadbd024950/image.png" alt=""></li>
</ul>
</li>
</ul>
<p>보편적으로 SQL을 사용하므로, 이를 사용해 앞으로의 내용을 다뤄보자. </p>
</li>
</ul>
<hr>
<h3 id="3-데이터베이스-설계---erd">3. 데이터베이스 설계 - ERD</h3>
<blockquote>
<p>*<em>프로젝트 개발 시작과 동시에 설계하는 게 좋음. *</em>
    - 기획과 디자인이 어느 정도 나와 기능이 정해지면, DB를 먼저 설계하고 코드 작업에 들어가야 합니다.
 *<em>모든 서버 팀원이 인지하는 DB는 동일해야 함. *</em>
     - 자기 마음대로 DB를 설계하고, 작업 후 나중에 합치는 행위는 혼동을 일으키고 비효율적인 중복 작업을 만들어낼 수 있으므로 자제해야합니다!! 😐</p>
</blockquote>
<p>그렇다면, 데이터베이스 설계의 결과물인 ERD에 대해 더 살펴봅시다. </p>
<p><strong>ERD</strong>(Entity-Relationship Diagram)란?</p>
<ul>
<li>데이터베이스에서 엔티티와 관계를 시각적으로 표현한 다이어그램<ul>
<li>엔티티란 뭐냐면... =&gt; 즉, 데이터베이스 테이블 간의 관계를 표현한 다이어그램이라고 생각하면 이해하기 쉽습니다!<ul>
<li>저장할 가치가 있는 데이터 객체</li>
<li>현실 세계의 사물이나 개념 표현한 것</li>
<li>데이터베이스 내에서 테이블로 표현</li>
<li>독립적으로 존재</li>
<li>여러 속성이 있음</li>
<li>기본키를 통해 고유하게 식별 가능</li>
</ul>
</li>
</ul>
</li>
<li>테이블, 속성, 관계를 그림으로 표현하여 데이터 모델을 설계할 때 사용</li>
<li>저는 dbdigram로 그리는 게 제일 편했는데, 주로 ERDcloud를 통해 다같이 작업하는 편입니다. </li>
</ul>
<hr>
<h3 id="참고-sql-문법에-대한-기본-지식">+참고. sql 문법에 대한 기본 지식</h3>
<ul>
<li><p>외래키</p>
<ul>
<li>FK(Foreign Key)</li>
<li>다른 테이블의 기본 키를 참조하는 키</li>
<li>두 테이블 간의 연관 관계를 정의하며, 데이터의 일관성을 유지하는 데 사용</li>
</ul>
</li>
<li><p>기본키</p>
<ul>
<li>PK(Primary Key)</li>
<li>테이블에서 각 행을 유일하게 식별할 수 있는 키</li>
<li>하나의 테이블에는 하나의!! 기본키만!! 존재</li>
<li>중복 X, NULL X</li>
<li>기본키가 존재하지 않을 수도 있기는 한데, 권장되지는 않음</li>
<li>정의하는 방법<ul>
<li>기본 키 정의 방법 (SQL): 기본 키는 테이블을 생성할 때 <strong><code>PRIMARY KEY</code></strong> 제약 조건을 사용하여 정의할 수 있습니다.</li>
</ul>
<ol>
<li><strong>단일 컬럼을 기본 키로 정의</strong>:<pre><code class="language-sql">CREATE TABLE Students (
 student_id INT PRIMARY KEY,  -- student_id가 기본 키로 정의됨
 name VARCHAR(100),
 age INT
);</code></pre>
</li>
</ol>
<ul>
<li>이 경우, <code>student_id</code>는 기본 키로 설정되었고, 중복되거나 NULL 값을 가질 수 없게 됩니다. </li>
</ul>
</li>
</ul>
<ol start="2">
<li><p><strong>복합 키(두 개 이상의 컬럼을 묶어 기본 키로 정의)</strong>:</p>
<pre><code class="language-sql"> CREATE TABLE Enrollment (
     student_id INT,
     course_id INT,
     enrollment_date DATE,
     PRIMARY KEY (student_id, course_id)  -- student_id와 course_id가 복합 기본 키로 정의됨
 );</code></pre>
<ul>
<li>이미 존재하는 테이블에 기본 키 추가:
: 테이블이 이미 만들어진 경우, 기본 키를 추가하려면 <strong>ALTER TABLE</strong>을 사용<pre><code class="language-sql">ALTER TABLE Students
ADD PRIMARY KEY (student_id);</code></pre>
</li>
</ul>
</li>
</ol>
</li>
</ul>
<pre><code> - `AUTO_INCREMENT` 의 사용
      - PRIMARY KEY의 속성 중 하나
      - 삽입되는 레코드에 유니크한 값을 자동적으로 부여
      - 일반적으로 1부터 증가하며 값을 부여하지만, 별도의 다른 값을 부여할 수도 있음

```sql
CREATE TABLE user {
     id int PRIMARY KEY AUTO_INCREMENT
            }</code></pre><pre><code>---
자 이제 ERD를 작성하는 방법에 대해 더 자세히 살펴보도록 하자. 
## 3-2. ERD 작성의 기본 규칙

- 테이블 이름과 칼럼 이름은 모두 소문자, 단어 구분은 대소문자가 아닌 밑줄로 구분해줘야 함
  - SQL 표준에서 테이블 이름과 칼럼 이름의 대소문자를 구분하지 않는 것으로 처리하기 때문
- 기본 키를 위해 index를 따로 두는 것이 편함.
- 각 index를 위한 id를 book_id, member_id가 아닌, 그냥 id로 이름을 짓는 것이 좋음
- 기본 키 타입은 int가 아닌 추후 서비스 확장을 고려해서 bigint로

&lt;span style=&quot;background-color:#F5F5F5&quot;&gt; **기본 entity #1**&lt;/span&gt;
![](https://velog.velcdn.com/images/funda__mental_/post/51e6609c-c8cf-43bf-8188-3a7ce48f1efd/image.png)
_MySQL에서 varchar(20)는 최대 20글자인데, 유니코드 기준 20글자니, 한글 20글자로 생각_
- varchar로 두는 경우는, enum으로 관리하는 경우 (ex. gender: man, woman)
- created_at, 그리고 updated_at을 테이블마다 추가해주는 것이 좋음 - 최신순 정렬 등의 목적
    - &lt;u&gt;Datetime VS Timestamp&lt;/u&gt;
      - datetime(6)에서 (6)은 밀리초 소수점 6자리까지 구분한다는 의미 - 밀리초 소수점까지 고려하지 않으면, 최신순 정렬이 어려울 수 있음 
       -  Timestamp 타입의 값은 현재 시각 → UTC 시각으로 변환되며 Datetime은 변환되지 않음. =&gt; Timestamp이 더 호환성 좋음!

&lt;span style=&quot;background-color:#F5F5F5&quot;&gt; **기본 entity #2: member entity**&lt;/span&gt;
![](https://velog.velcdn.com/images/funda__mental_/post/4a107960-8200-4f5f-b4e4-709a68edec03/image.png)
- member 테이블은 status와 inactive_date를 두는 것이 좋음 
=&gt; **soft delete**를 위함 (꼭 member가 아니어도 다양한 곳에 적용 가능)
   - Hard delete: 회원 탈퇴, 게시글 삭제 등의 연산을 HTTP Method 중 Delete로 바로 삭제를 해 버리는 방법 =&gt; No!
    - 문제가 발생할 수 있음. ex) 회원 탈퇴를 철회하고 다시 돌아오는 회원들이 존재하는 경우처럼 복구하기 어려움
   - Soft delete: 비활성 상태로 두고, 일정 기간동안 비활성인 경우 자동 삭제가 되도록 설계
     - status가 inactivate된 지 얼마 뒤에(inactivate_date로 count), batch 사용해서 삭제
       - batch: 정해진 시간에 자동으로 실행되는 프로세스로, 대개 하루가 끝날 때나 야간과 같이 컴퓨팅 리소스 사용량이 적은 시간에 task를 batch로 처리, batch는 HTTP Method 중 Patch -  레코드 자체를 삭제하는 것이 아니라, 레코드의 특정 필드(ex: is_deleted, deleted_at 등)만 업데이트하는 것이기 때문

## 3-3. 연관 관계
ERD에서 Entity 하나하나를 설계하는 것도 중요하지만, 이들간의 관계를 연결하는 것도 매우 중요합니다! 이에 대해 자세히 살펴봅시다. 

- &lt;span style=&quot;background-color:#fff5b1&quot;&gt;**1:N**&lt;/span&gt;의 경우, N 쪽이 외래키를 가짐
- &lt;span style=&quot;background-color:#fff5b1&quot;&gt;**N:M**&lt;/span&gt;의 경우
  양쪽에 대해 서로 a가 b 여러 개 적용 가능, b가 a 여러 개 적용 가능이면, N:M 관계 
 가운데 매핑 테이블이 양쪽의 기본키(PK)를 외래키(FK)로 가지고 각각 1:N의 관계를 가짐

- 알림과 같이, 알림 중에서 여러 type이 구분되는 경우를 연관관계를 활용해 다양하게 설계 가능!
     ** 1. 슈퍼타입과 서브타입의 구성**
          ![](https://velog.velcdn.com/images/funda__mental_/post/392221a4-51d1-4b1a-9c02-84de99f84091/image.png)
   - **슈퍼 타입**: `알림(alarm)`
     공통적으로 모든 알림이 가져야 할 속성들을 정의 ⇒ 공통적인 알림의 동작과 속성들을 관리
      : `알림 ID`, `제목`, `내용`,`생성 시간`,`읽음 여부`, 등등...
    - **서브 타입 ⇒** 각각의 알림 타입에 특화된 정보를 관리
         - **공지 알림(notice)**: 공지 사항과 관련된 알림.
                - 추가적으로 &quot;공지사항 ID&quot; 같은 공지 사항으로 이동할 때 필요한 정보를 포함.
         - **마케팅 알림(marketing)**: 마케팅과 관련된 알림.
                - 추가적으로 &quot;마케팅 ID&quot; 같은 마케팅 관련 정보를 포함.
    ** 2. 하나의 테이블을 두고 dtype으로 구분**
          ![](https://velog.velcdn.com/images/funda__mental_/post/8af9a505-ce7e-4c93-a12d-05789d22ac46/image.png)
        ![](https://velog.velcdn.com/images/funda__mental_/post/44d602bb-c6b5-429e-98a0-96b690e6d1f1/image.png)


 **  3. 그냥 각각 테이블 다 나누기: 각 type마다 다른 알람 entity를 만드는 방법**

---
## 4. local DB 접속해서 DB 설계하기 
다음으로는, 실제로 local DB에 접속해서 설계한 ERD를 바탕으로 데이터베이스를 만들어보겠습니다. 
&gt; intelliJ로도 접속할 수 있는 방법이 있어서 그 방법을 소개하겠습니다! 
1. Intellij에 접속해서 DataBase로 들어가기
![](https://velog.velcdn.com/images/funda__mental_/post/d4f47a55-25d7-4152-8ff3-1f0a0ddd62fc/image.png)
2. Data Source에서 MySQL을 클릭
![](https://velog.velcdn.com/images/funda__mental_/post/3e9a3b86-ee3a-4dcb-a005-6bb5a57a0262/image.png)
3. mysql에 하는 것처럼 Host, User, Password에 알맞은 값을 넣고 Test Connection을 눌러서 Succeeded가 나오면 성공


----
오늘은 백엔드 서버의 핵심이 DB 설계와 ERD에 대해 배워봤는데요. 다음 시간에는 실제로 어떻게 SQL문을 작성해야하는지 알아봅시다!😊
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[코드트리 조별과제] Python 14. 객체]]></title>
            <link>https://velog.io/@funda__mental_/%EC%BD%94%EB%93%9C%ED%8A%B8%EB%A6%AC-%EC%A1%B0%EB%B3%84%EA%B3%BC%EC%A0%9C-Python-14.-%EA%B0%9D%EC%B2%B4</link>
            <guid>https://velog.io/@funda__mental_/%EC%BD%94%EB%93%9C%ED%8A%B8%EB%A6%AC-%EC%A1%B0%EB%B3%84%EA%B3%BC%EC%A0%9C-Python-14.-%EA%B0%9D%EC%B2%B4</guid>
            <pubDate>Thu, 22 Aug 2024 15:37:24 GMT</pubDate>
            <description><![CDATA[<h2 id="1-객체">1. 객체</h2>
<h3 id="1-클래스">1) 클래스</h3>
<ul>
<li>학생을 지칭하는 새로운 형태를 하나 정의하고, 그 형태를 이루기 위한 요소로 (국어, 영어, 수학)을 정의</li>
<li>형태<pre><code class="language-python">class Student:
  def __init__(self, kor, eng, math = 40):
      self.kor = kor
      self.eng = eng
      self.math = math</code></pre>
</li>
</ul>
<p><code>__init__</code> 함수는 이 Student 클래스의 형태를 정의하는, <strong>생성자(constructor)</strong> 라 불리는 함수입니다. 
self를 항상 첫 번째 인자로 넣어주고, self는 학생을 지칭
인자로 넘어오는 kor, eng, math는 현재 학생의 kor, eng, math라는 값이 되어야 하기 때문에 그 값을 대입해 주는 식으로 코드를 작성하며, 이때 이러한 kor, eng, math 값을 <strong>멤버 변수</strong>라고 부릅니다.</p>
<p>위의 클래스를 이용하면 다음과 같이 학생 정보를 저장하는 코드를 작성해볼 수 있습니다. Student를 마치 함수를 호출 할때처럼 사용하며, <code>__init__</code> 함수에 self 다음에 인자가 정의되어 있는 순서대로 값을 적어주게 되면, 각각의 값을 갖는 하나의 학생 <strong>객체(instance)</strong> 를 만들어내게 됩니다. 이 객체는 <code>kor</code>, <code>eng</code>, <code>math</code> 라는 이름의 멤버 변수를 갖으며, 이 값들을 조회하기 위해서는 <code>객체이름.멤버변수이름</code> 형태로 사용이 가능합니다.</p>
<pre><code class="language-python">student1 = Student(90, 80, 90)
print(student1.kor)  # 90
print(student1.eng)  # 80
print(student1.math) # 90</code></pre>
<p>이처럼 원하는 형태의 Class를 선언하고, 해당 Class를 이용하여 원하는 객체를 만들게 되면, 원하는 형태 단위로 값을 관리할 수 있게 됩니다.</p>
<h3 id="2-tuple">2) tuple</h3>
<ul>
<li>tuple<ul>
<li>새로운 형태를 정의할 때 class를 만들지 않고 진행하는 방법</li>
<li>소괄호로 둘러쌓인 형태로 정의되어 있는 것</li>
<li>리스트와 비슷하지만, 크게 2개의 차이점이 존재 - tuple은 immutable<ul>
<li>튜플에 들어있는 값은 수정할 수 없음</li>
<li>튜플에 추가 원소를 넣거나 기존 원소를 제거하는 것은 불가능</li>
</ul>
</li>
<li>기본값 개념 없음</li>
</ul>
</li>
</ul>
<pre><code class="language-python">def hello():
    return 1, 2

print(hello()) # (1, 2)</code></pre>
<ul>
<li>튜플의 주 된 역할은 패킹과 언패킹<pre><code class="language-python">def hello():
  return 1, 2
</code></pre>
</li>
</ul>
<p>a, b = hello() # a, b = (1, 2) - 튜플 언패킹 (Tuple Unpacking)
print(a, b, a + b)</p>
<pre><code>
- 패킹
    - 즉 여러개의 값을 하나의 변수에 넣을 수 있는 방법이 있을까요?
- 리스트
```python
dt = [1, 2, 3, 4, 5]</code></pre><ul>
<li>튜플<pre><code class="language-python">dt = 1, 2, 3, 4, 5</code></pre>
</li>
</ul>
<p>그렇다면 어떻게 class 대신 tuple을 이용해 원하는 형태를 담을 수 있을까요?</p>
<p>다음과 같이 tuple 형태로 값을 저장하면 됩니다.</p>
<pre><code class="language-python">student1 = (90, 80, 90)</code></pre>
<p>다만, tuple을 이용하는 경우에는 각 값이 의미하는 것이 무엇인지 알기 어렵기 때문에, 값을 이용할 때 되도록이면 unpacking을 통해 꼭 적절한 이름을 붙여준 뒤 사용하는 것을 권장합니다.</p>
<p>다음과 같이 각 원소에 대해 unpacking시 적절한 변수 이름을 사용하여 의미를 부여한 뒤, 사용</p>
<pre><code class="language-python">student1 = (90, 80, 90)
kor, eng, math = student1
print(kor, end, math) # 90 80 90</code></pre>
<p>그렇다면 tuple에 이미 들어있는 값은 어떻게 바꿀 수 있을까요?<br>tuple은 immutable하기 때문에 원소 값을 바꿀 수 없다고 했었습니다. 따라서 tuple에 있는 원소의 값을 의미상 바꾸기 위해서는 다음과 같이 꼭 <strong>새로운 tuple을 생성해줘야만</strong> 합니다. 다음은 수학 점수만 90점으로 바꾼 예입니다.</p>
<pre><code class="language-python">student1 = (0, 0, 0)
kor, eng, math = student1
print(kor, eng, math) # 0 0 0

student1 = (kor, eng, 90)

kor, eng, math = student1
print(kor, eng, math) # 0 0 90</code></pre>
<p>tuple 이용시 만약 수학 점수만 필요한 경우라면, 코드를 어떻게 작성해주는 것이 좋을까요?<br>바로 unpacking시 국어, 영어 점수와 같이 필요 없는 곳에는 <code>_</code>를 넣어 사용하지 않겠다는 의미 부여를 해주는 것이 좋습니다.</p>
<pre><code class="language-python">student1 = (90, 80, 90)
_, _, math = student1 # 수학 점수만 사용하겠다는 의지
print(math) # 90</code></pre>
<h3 id="3-객체를-원소로-하는-리스트">3) 객체를 원소로 하는 리스트</h3>
<h4 id="class를-이용한-객체-리스트">class를 이용한 객체 리스트</h4>
<p>국어, 영어, 수학 점수를 포함한 학생 5명의 정보를 쉽게 표현해볼 수는 없을까요? 이는 class를 이용한 객체 선언과 동시에, 이 각각의 객체를 원소로 갖는 리스트를 이용하여 깔끔하게 정의가 가능합니다.</p>
<pre><code class="language-python">class Student:
    def __init__(self, kor, eng, math):
        self.kor = kor
        self.eng = eng
        self.math = math

students = [
    Student(90, 80, 90), # 첫 번째 학생
    Student(20, 80, 80), # 두 번째 학생
    Student(90, 30, 60), # 세 번째 학생
    Student(60, 10, 50), # 네 번째 학생
    Student(80, 20, 10), # 다섯 번째 학생 
]

student3 = students[2] # 세 번째 학생 정보
print(student3.kor)  # 90
print(student3.eng)  # 30
print(student3.math) # 60</code></pre>
<p>아직 점수 확정되지 않은 5명 학생의 정보를 정의하기 위해서는, class 정의시 각 <strong>점수의 기본값을 설정해주고, 다음과 같이 리스트에 각 객체를 넣어 정의</strong>해 줄 수 있습니다.</p>
<pre><code class="language-python">class Student:
    def __init__(self, kor=0, eng=0, math=0):
        self.kor = kor
        self.eng = eng
        self.math = math

students = []
for _ in range(5):
    students.append(Student()) # 5명의 학생 추가
    # students = [Student() for _ in range(5)] #  [list comprehension]으로 대체가 가능
# 입력 받는 경우
    # for _ in range(5):
        # kor, eng, math = tuple(map(int, input().split()))
        # students.append(Student(kor, eng, math))

student3 = students[2] # 세 번째 학생 정보
print(student3.kor)  # 0
print(student3.eng)  # 0
print(student3.math) # 0</code></pre>
<h4 id="tuple을-이용한-객체-리스트">tuple을 이용한 객체 리스트</h4>
<p>국어, 영어, 수학 점수를 포함한 학생 5명의 정보를 tuple을 이용하면 다음과 같이 표현이 가능합니다.</p>
<pre><code class="language-python">students = [
    (90, 80, 90), # 첫 번째 학생
    (20, 80, 80), # 두 번째 학생
    (90, 30, 60), # 세 번째 학생
    (60, 10, 50), # 네 번째 학생
    (80, 20, 10), # 다섯 번째 학생 
]

student3 = students[2] # 세 번째 학생 정보
kor, eng, math = student3
print(kor)  # 90
print(eng)  # 30
print(math) # 60</code></pre>
<p>만약 5명의 학생 정보를 <strong>입력받아</strong> 리스트를 만드는 경우라면, 다음과 같이 코드 작성이 가능합니다. 지금처럼 list comprehension을 이용하면 더욱 깔끔하게 작성이 가능합니다.</p>
<pre><code class="language-python">students = [
    tuple(map(int, input().split()))
    for _ in range(5)
]

student3 = students[2] # 세 번째 학생 정보
kor, eng, math = student3
print(&quot;student3:&quot;, kor, eng, math)

&gt;&gt; 90 80 90
   20 80 80
   90 30 60
   60 10 50
   80 20 10

student3: 90 30 60</code></pre>
<ul>
<li>도전문제<ul>
<li>미래의 기상을 예측한 데이터가 n개 주어집니다. 각 데이터에는 날짜, 요일, 날씨의 정보가 순서대로 담겨있을 때, 가장 근 시일내에 비가 오는 날을 찾아 출력하는 프로그램을 작성해보세요.</li>
</ul>
</li>
</ul>
<p>단, 데이터에서 주어지는 연도의 범위는 2,000≤Y≤2,100 입니다.</p>
<ul>
<li><p>입력 형식
첫 번째 줄에 정수 n이 주어집니다.
두 번째 줄부터는 n개의 줄에 걸쳐 각 예측 정보에 대한 날짜, 요일, 그리고 날씨에 대한 정보가 공백을 사이에 두고 주어집니다. 날짜는 <code>yyyy-mm-dd</code> 형태로 주어지며 요일, 날씨의 경우 알파벳으로만 이루어져 있는 단어 형태로 주어짐을 가정해도 좋습니다. 비가 오는 날씨의 경우 <code>Rain</code>이라는 이름으로 주어집니다.</p>
</li>
<li><p>1≤n≤100</p>
</li>
<li><p>n 개의 데이터 중에서 비가 오는 날이 적어도 하루 존재합니다.</p>
</li>
<li><p>출력 형식
첫 번째 줄에 가장 근 시일내에 비가 오는 날의 정보를 날짜, 요일, 날씨 순으로 주어진 포맷과 동일하게 출력합니다.</p>
</li>
</ul>
<h4 id="입출력-예제">입출력 예제</h4>
<h4 id="예제1">예제1</h4>
<p>입력:</p>
<pre><code>4
2036-12-27 Sun Snow
2052-08-28 Wed Rain
2043-03-21 Sat Sun
2077-08-19 Thu Rain</code></pre><p>출력:</p>
<pre><code>2052-08-28 Wed Rain</code></pre><ul>
<li>정답<pre><code class="language-python"># 변수 선언 및 입력
n = int(input())

</code></pre>
</li>
</ul>
<h1 id="forecast-정보를-나타내는-클래스-선언">Forecast 정보를 나타내는 클래스 선언</h1>
<p>class Forecast:
    def <strong>init</strong>(self, date, day, weather):
        self.date, self.day, self.weather = date, day, weather</p>
<p>ans = Forecast(&quot;9999-99-99&quot;, &quot;&quot;, &quot;&quot;)</p>
<p>for _ in range(n):
    date, day, weather = tuple(input().split())
    # Forecast 객체를 만들어 줍니다.
    f = Forecast(date, day, weather)
    if weather == &quot;Rain&quot;:
        # 비가 오는 경우 가장 최근인지 확인하고,
        # 가장 최근일 경우 정답을 업데이트합니다.
        if ans.date &gt;= f.date:
            ans = f</p>
<h1 id="결과를-출력합니다">결과를 출력합니다.</h1>
<p>print(ans.date, ans.day, ans.weather)</p>
<p>```</p>
<ul>
<li>틀린 이유<ol>
<li>date가 스프링으로 들어가있는데 그대로 비교가 되는지 몰랐음. </li>
<li>저렇게 객체를 만들 생각을 하지 못함. </li>
</ol>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코드트리 조별과제] Python 13. 정렬]]></title>
            <link>https://velog.io/@funda__mental_/%EC%BD%94%EB%93%9C%ED%8A%B8%EB%A6%AC-%EC%A1%B0%EB%B3%84%EA%B3%BC%EC%A0%9C-Python-13.-%EC%A0%95%EB%A0%AC</link>
            <guid>https://velog.io/@funda__mental_/%EC%BD%94%EB%93%9C%ED%8A%B8%EB%A6%AC-%EC%A1%B0%EB%B3%84%EA%B3%BC%EC%A0%9C-Python-13.-%EC%A0%95%EB%A0%AC</guid>
            <pubDate>Tue, 20 Aug 2024 03:11:49 GMT</pubDate>
            <description><![CDATA[<ul>
<li>오름차순으로 정렬하는 코드 - sort()<ul>
<li>list를 만들어 사용해야 함 =&gt; 문자열은 바로 사용 못함, 각 문자를 원소로 갖는 list로 변환 후 sort 함수를 이용하면 정렬이 가능<pre><code class="language-python">arr = [12, 41, 37, 81, 19, 25, 60, 20]
arr.sort()
print(arr) # [12, 19, 20, 25, 37, 41, 60, 81]
</code></pre>
</li>
</ul>
</li>
</ul>
<h1 id="뒤집는-법--1">뒤집는 법- 1</h1>
<p>arr.sort(reverse=True)
print(arr) # [81, 60, 41, 37, 25, 20, 19, 12]</p>
<h1 id="뒤집는-법---2">뒤집는 법 - 2</h1>
<p>arr = [12, 41, 37, 81, 19, 25, 60, 20]
arr.sort()
arr = arr[::-1] # reversed array
print(arr) #[81, 60, 41, 37, 25, 20, 19, 12]</p>
<pre><code>
- `sorted()`라는 함수
    -  **단, sorted 함수는 정렬된 리스트를 반환하는 함수이기 때문에, 다음과 같이 정렬 이후의 리스트를 변수에 할당을 해줘야만 합니다.**
```python hl:2
arr = [12, 41, 37, 81, 19, 25, 60, 20]
arr = sorted(arr)
print(arr) # [12, 19, 20, 25, 37, 41, 60, 81]</code></pre><ul>
<li>문자열 정렬<pre><code class="language-python">string = &quot;banana&quot;
arr = list(string)
arr.sort()
print(arr) # [&#39;a&#39;, &#39;a&#39;, &#39;a&#39;, &#39;b&#39;, &#39;n&#39;, &#39;n&#39;]
sorted_str = &#39;&#39;.join(arr)
print(sorted_str) # aaabnn</code></pre>
</li>
</ul>
<p><code>sorted()</code> 함수는 어떨까요? sorted 함수의 경우에는, 문자열을 함수 인자로 넣더라도, 이를 성공적으로 정렬해줍니다. 다만, 결과물이 list로 나오는 것을 확인할 수 있습니다.
따라서 sorted 함수를 이용한 이후에 다시 join함수를 이용해 문자열을 만들어주면 됩니다.</p>
<pre><code class="language-python">string = &quot;banana&quot;
sorted_arr = sorted(string)
print(sorted_arr) # [&#39;a&#39;, &#39;a&#39;, &#39;a&#39;, &#39;b&#39;, &#39;n&#39;, &#39;n&#39;]
sorted_str = &#39;&#39;.join(sorted_arr)
print(sorted_str) # aaabnn</code></pre>
<ul>
<li>문자열 리스트 정렬 - 일반 리스트와 동일<pre><code class="language-python">words = [&quot;banana&quot;, &quot;apple&quot;, &quot;cat&quot;, &quot;app&quot;]
words.sort()
print(words) # [&#39;app&#39;, &#39;apple&#39;, &#39;banana&#39;, &#39;cat&#39;]</code></pre>
</li>
</ul>
<p>! 복기: <code>print( , end=&quot; &quot;)</code>- 이렇게 해야 공백으로 받을 수 있어</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코드트리 조별과제] Python 11, 12. 함수, 재귀함수]]></title>
            <link>https://velog.io/@funda__mental_/Python-11-12.-%ED%95%A8%EC%88%98-%EC%9E%AC%EA%B7%80%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@funda__mental_/Python-11-12.-%ED%95%A8%EC%88%98-%EC%9E%AC%EA%B7%80%ED%95%A8%EC%88%98</guid>
            <pubDate>Thu, 15 Aug 2024 03:41:06 GMT</pubDate>
            <description><![CDATA[<h2 id="1-기본-함수-정의-및-형태">1. 기본 함수 정의 및 형태</h2>
<pre><code class="language-python"> def print_5_stars():
    print(&quot;*&quot; * 5)


for _ in range(4):
    print_5_stars()

def print_n_lines(n): # n: 함수의 인자
    코드 작성

def print_rect(n, m):
    코드 작성

def add(a, b):
    return a + b

add(a,b) # 함수 호출

def add(a, b, c=0): # 기본값 설정해주기
    return a + b + c  # 함수는 `return` 을 만나게 되면 그 즉시 종료</code></pre>
<ul>
<li>함수와 함수 혹은 다른 코드와의 줄 간격은 일반적으로 2줄을 띄기</li>
<li>의미 단위로 <code>_</code>를 붙여 구분해주는 <code>snake case</code></li>
</ul>
<ul>
<li>도전문제: 최소공배수 구하는 문제<pre><code class="language-python">k = list(map(int, input().split()))
n, m = k[0], k[1]
</code></pre>
</li>
</ul>
<p>def guess_min(n,m):
    a = 1
    x, y = 0, 0
    i = 2</p>
<p>    while i &lt;= min(n,m):
        x = n % i
        y = m % i
        if x == 0 and y == 0:
            n = n // i
            m = m // i
            a = a<em>i
            # 같은 i로 실행되게 해야 돼
        else:
            i += 1
    a = a</em>n*m
    print(a)
   
guess_min(n,m)</p>
<pre><code>- 모범답안: 최소공배수 = `a*b / (a와b의 최대공약수)`
```python hl:7=8,11
# 변수 선언 및 입력:
n, m = tuple(map(int, input().split()))

# n과 m의 최소공배수를 출력합니다.
def find_lcm(n, m):
    gcd = 0
    for i in range(1, min(n, m) + 1):
        if n % i == 0 and m % i == 0:
            gcd = i

    print(n * m // gcd)


find_lcm(n, m)</code></pre><ul>
<li><p>참고: 그렇게 되는 이유
  ![[Pasted image 20240804233647.png]]</p>
</li>
<li><h3 id="여러수의-최소공배수-접근방법-팁-스포주의">여러수의 최소공배수.. 접근방법 팁 (스포주의)</h3>
</li>
<li><p>기본적인 두 수의 최대공약수, 최소공배수를 구하고</p>
</li>
</ul>
<pre><code>두 수 a, b의 최대공약수 gcd
두 수 a, b의 최소공배수 lcm = (a * b) // gcd</code></pre><ul>
<li>처음에 잘못된 접근방법</li>
</ul>
<pre><code>세 수 a, b, c의 최대공약수 = 두 수 a, b의 최대공약수인 g와 c의 최대공약수 gcd
세 수 a, b, c의 최소공배수 = (a // gcd) * (b // gcd) * (c // gcd) * gcd</code></pre><p>샘플케이스는 통과 하지만 간단하게 <code>arr = [2,3,4]</code> 이런 경우만 생각해봐도<br>리턴값은 <code>12</code>가 나와야하지만, 위의 방식으로 접근하면<code>24</code>라는 잘못된 값이 나오는걸 알 수 있습니다.</p>
<ul>
<li>제대로된 접근방법</li>
</ul>
<pre><code>세 수 a, b, c의 최소공배수 = 두 수 a, b의 최소공배수인 l과 c의 최소공배수 lcm</code></pre><pre><code>    while len(arr) &gt;= 2:
        a = arr.pop()
        b = arr.pop()
        arr.append(lcm(a,b))</code></pre><p>리스트에서 맨 뒤 2개의 수를 꺼내 최소공배수를 구하고, 리스트에 추가해주는 방식을 반복<br>마지막에 arr 리스트에 1개의 값만 있을 경우, 이 값이 n개수의 최소공배수가 됩니다.</p>
<p>단순하게 2개씩 구하면서 넘어가면되는 간단한 문젠데, 저처럼 방법때문에 삽질하시는 분들 계시다면....<br>참고하셔도 좋을것 같네요 ㅎㅎ</p>
<ul>
<li>인자가 2개가 넘어오든, 3개, 4개 몇 개가 넘어오든 넘어온 값들의 합을 반환해주는 함수<ul>
<li><code>*(asterisk)</code>을 사용하면 인자 개수에 상관없이 <code>tuple</code>이라는 형태로 값들을 받아올 수 있음<pre><code class="language-python">def add(*args):
return sum(args)
</code></pre>
</li>
</ul>
</li>
</ul>
<blockquote>
<blockquote>
<p>add(1, 3, 2, 6, 5, 4)
21</p>
</blockquote>
</blockquote>
<pre><code>
- 고난도 문제 함수로 해결하기
    - `단 하나라도` 라는 조건이 들어가는 경우에는, 주로 True / False 값을 갖는 bool 타입의 변수를 하나 선언하여 해결
    - if 조건문으로만 해결하기에는 조건이 상당히 까다로울 때, 함수와 bool type 변수를 통해, return으로 구현

## 2. Call by value / Call by reference / 두 정수 값 교환하기
- Python에서는 변수의 type에 따라 immutable과 mutable한 것으로 나뉨
    - ==[Call by value] immutable한 type: tuple, string 그리고 int, bool 등을 포함한 primitive type==
        - immutable 특성을 갖는 변수가 함수의 인자로 넘어가게 되면, &lt;u&gt;변할 수 없는 특성&lt;/u&gt; 때문에 그 변수가 그대로 넘어가는 것이 아닌, **변수가 갖고 있던 값을 복사**하여 값을 넘겨줌
        - 함수 안에서 어떠한 일이 일어나더라도 전혀 밖에 있는 변수에 영향을 끼치지 않음
        - 함수 안에서의 역할이 밖에 있는 변수에도 영향을 끼치고 싶다면, 아래와 같이 해당 문자열 자체를 반환하는 함수 작성
    - ==[Call by reference] mutable한 type: list, dict 등==
        - mutable한 변수를 함수 인자로 넘기게 되면 해당 변수 자체의 **주소**가 넘어가게 됨
        - 함수 안에서 해당 인자에 주는 모든 변화가 고스란히 함수 호출시 넘겼던 변수에 남게 됨
        -  함수로 값을 넘길때 다음과 같이 slicing을 하게 되면 기존 리스트와 동일한 원소를 갖는 **새로운 리스트**를 만들어 값을 넘겨주면 이거 방지 가능
```python hl:14,17
# n과 m의 값이 바뀌지 않음
def swap(a, b):
    a, b = b, a
    print(a, b)


n, m = 10, 20
swap(n, m)
print(n, m)

# 바뀜
def swap(a, b):
    a, b = b, a
    return a, b

n, m = 10, 20
n, m = swap(n, m)
print(n, m)

&gt;&gt; 20 10</code></pre><pre><code class="language-python">def modify(arr): # arr는 _list와 관련이 없다.
    arr[0] = 10


_list = [1, 2, 3, 4]
modify(_list[:]) # 새로운 리스트를 만들어 넘기기

for elem in _list:
    print(elem, end=&quot; &quot;)

&gt;&gt; 1 2 3 4 # 값에 변화가 없다</code></pre>
<h2 id="3-변수의-영역">3. 변수의 영역</h2>
<h3 id="1-전역-변수global-variable">1) 전역 변수(Global variable)</h3>
<ul>
<li> <strong>global 변수</strong>:  함수 위에 값들을 정의하게 되면, 어디에서도 쓰일 수 있음<ul>
<li>함수 안에서 바로 참조 가능</li>
</ul>
</li>
<li><strong>local 변수</strong>: 함수 안에서 정의한 변수는, 함수를 벗어나면 더 이상 쓰일 수 없음<ul>
<li>함수가 종료되는 순간에 변수도 같이 사라지기 때문에, 다른 곳에서 사용이 불가함</li>
</ul>
</li>
</ul>
<p>modify 함수 안의 _list 변수를 global 변수로 인식하게 하려면, 다음과 같이 함수 안에 <strong>global 표식</strong>을 해줘야 합니다.</p>
<pre><code class="language-python">_list = [1, 2, 3, 4]


def modify():
    global _list # global 변수인 _list를 가져다 쓸 것이다.
    _list = [5, 6, 7, 8]


modify()
for elem in _list:
    print(elem, end=&quot; &quot;)

&gt;&gt; 5 6 7 8

num = 10


def modify():
    global num
    num = 5

modify()
print(num)

&gt;&gt; 5</code></pre>
<ul>
<li>같은 변수 이름을 갖는 경우 <strong>가장 가까이에 있는 변수</strong>를 사용</li>
</ul>
<hr>
<hr>
<h2 id="1-재귀함수-형태">1. 재귀함수 형태</h2>
<p>이 함수는 <strong>print_star(n - 1)</strong> 를 먼저 수행하여 1번째부터 n - 1번째 줄까지의 별을 알맞게 출력한 뒤, n번째 줄을 출력해보는 식</p>
<pre><code class="language-python">def print_star(n):    # 1부터 n번째 줄까지 별을 출력하는 함수
    if n == 0:        # n이 0이라면, 더 이상 진행하지 않고
        return        # 퇴각합니다.

    print_star(n - 1) # 1부터 n - 1번째 줄까지 출력하는 함수
    print(&quot;*&quot; * 5)    # n번째 줄에 해당하는 별 출력


print(&quot;start&quot;)
print_star(3)
print(&quot;end&quot;)
</code></pre>
<ul>
<li><strong>종료 조건</strong>이 무조건 있어야 됨!!!!!!!!!!<ul>
<li>값을 반환하지 않는 함수에서, 해당 함수를 종료하고 싶을 때는 값 없이 <strong>return</strong> 적어주면 됨</li>
</ul>
</li>
</ul>
<pre><code class="language-python">def print_star(n):    # 1부터 n번째 줄까지 별을 출력하는 함수
    if n == 0:        # 종료 조건
        return

    print_star(n - 1) # 1부터 n - 1번째 줄까지 출력하는 함수
    print(&quot;*&quot; * n)    # n번째 줄에는 n개의 별을 출력합니다.</code></pre>
<p>위의 코드를 실행하면 예상과 동일한 결과를 얻을 수 있게 됩니다.</p>
<pre><code class="language-python">print_star(3)

&gt;&gt; *
   **
   ***</code></pre>
<p>하지만 다음과 같이 print 함수와 print_star 함수를 호출하는 부분의 순서가 바뀌어있는 경우라면, 어떤 출력 결과를 받게 될지 예상이 되시나요?</p>
<pre><code class="language-python">def print_star(n):    # 1부터 n번째 줄까지 별을 출력하는 함수
    if n == 0:        # 종료 조건
        return

    print(&quot;*&quot; * n)    # n번째 줄에는 n개의 별을 출력합니다.
    print_star(n - 1) # 1부터 n - 1번째 줄까지 출력하는 함수</code></pre>
<p>바로 뒤집어진 삼각형 모양을 받게 됩니다. 그 이유는 1부터 n - 1번째 줄까지 출력을 진행하기 전에, 먼저 n개의 별을 출력하였기 때문입니다.</p>
<pre><code class="language-python">print_star(3)

&gt;&gt; ***
   **
   *</code></pre>
<p>한 가지 예시를 더 살펴보겠습니다. 다음 코드를 실행하면 어떤 결과가 나오게 될까요?</p>
<pre><code class="language-python">def print_star(n):  
    if n == 0:      
        return

    print(&quot;*&quot; * n)
    print_star(n - 1)
    print(&quot;*&quot; * n)</code></pre>
<p>바로 두 직각삼각형이 위아래로 붙어있는 모양을 받아보게 됩니다. 크게 봤을 때 상상을 해보면, print_star(n - 1)을 실행하기 직전에 n개의 별을 출력하고, n - 1개를 처리한 이후에 다시 n개의 별을 출력하므로 위아래로 n개를 출력하고 계속 안쪽으로 별을 채워넣어가는 모양이 될 것이라 예상할 수 있습니다.</p>
<pre><code class="language-python">print_star(3)

&gt;&gt; ***
   **
   *
   *
   **
   ***</code></pre>
<p>이렇듯 재귀함수에서는 print문을 어디에 정의하는지에 따라 예상과는 아예 다른 결과를 받아보게 됩니다.
-&gt; 너무너무 헷갈림!!!!!!!!!!</p>
<h1 id="2-값을-반환하는-재귀함수">2. 값을 반환하는 재귀함수</h1>
<p> - 값을 반환하는 재귀함수에서의 종료조건은 <strong>지극히 자명한</strong>, <strong>계산 없이도 바로 결과를 알 수 있는 경우</strong>로 설정
     - 값을 return해도 종료조건이 따로 있어야 됨</p>
<ul>
<li><p>팩토리얼 함수</p>
<ul>
<li><p>1부터 n까지의 곱을 반환</p>
<pre><code class="language-python">def fact(n):
if n == 1:
  return 1

return fact(n - 1) * n

</code></pre>
</li>
</ul>
</li>
</ul>
<p>print(fact(3))</p>
<blockquote>
<blockquote>
<p>6</p>
</blockquote>
</blockquote>
<pre><code>
- 점화식
    - An = An-1 + 2 * An-2
```python hl:9
def f(n):
    # 종료 조건
    if n == 1:
        return 2
    if n == 2:
        return 7

    # 점화식
    return f(n - 1) + 2 * f(n - 2)


print(f(4))

&gt;&gt; 25</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Python 09. 문자열]]></title>
            <link>https://velog.io/@funda__mental_/Python-09.-%EB%AC%B8%EC%9E%90%EC%97%B4</link>
            <guid>https://velog.io/@funda__mental_/Python-09.-%EB%AC%B8%EC%9E%90%EC%97%B4</guid>
            <pubDate>Sat, 03 Aug 2024 03:48:37 GMT</pubDate>
            <description><![CDATA[<h2 id="1-문자열-입력받아-출력하기">1. 문자열 입력받아 출력하기</h2>
<ul>
<li><p> <code>input()</code>함수를 이용해서 입력받음</p>
<ul>
<li>중간에 공백 있으면 공백도 인식됨</li>
</ul>
</li>
<li><p>문자열 ≒ 1차원 배열</p>
<pre><code class="language-python">given_input = &quot;banana&quot;
</code></pre>
</li>
</ul>
<p>print(given_input[0])   # b &lt;- 문자열의 첫 번째 문자
print(given_input[1])   # a &lt;- 문자열의 두 번째 문자
print(given_input[2])   # n &lt;- 문자열의 세 번째 문자</p>
<pre><code>
- 문자열의 경우에는 for loop 없이 print 함수만을 이용하여 출력 가능
```python
given_input = &quot;banana&quot;
print(given_input)

&gt;&gt; banana</code></pre><p>하지만 일반 리스트의 경우, 리스트를 그대로 출력하면 리스트 type 형태로 나와서 for loop 필요</p>
<pre><code class="language-python">arr = [1, 2, 3]
print(arr)                   # [1, 2, 3] 그 자체로 출력됨

for elem in arr:             # 1 2 3 이렇게 출력됨
    print(elem, end=&quot; &quot;)     </code></pre>
<ul>
<li>문자열의 길이: <code>print(len(given_input))</code><ul>
<li>[&#39;apple&#39;, &#39;banana&#39;, &#39;candy&#39;]
  여기서의 arr는 1차원 리스트이기 때문에, <code>len(arr)</code>를 출력하면 문자열의 개수 = 3</li>
</ul>
</li>
<li>도전문제: 3개의 문자열이 주어졌을 때, 가장 긴 문자열과 가장 짧은 문자열의 길이 차가 얼마인지 구해보는 프로그램<pre><code class="language-python">a = input()
b= input()
c = input()
</code></pre>
</li>
</ul>
<p>a = len(a)
b= len(b)
c= len(c)</p>
<p>print(max(a,b,c) - min(a,b,c)) # 이런 식으로 max와 min 사용 가능</p>
<pre><code>모범답안
```python
# 문자열을 입력받습니다.
str1 = input()
str2 = input()
str3 = input()

# 문자열의 길이를 구합니다.
len1 = len(str1)
len2 = len(str2)
len3 = len(str3)

# 세 문자열 중 가장 긴 길이와 가장 짧은 길이를 구합니다.
mx = max([len1, len2, len3])
mn = min([len1, len2, len3])

# 문제에서 구하고자 하는 값을 출력합니다.
print(mx - mn)</code></pre><ul>
<li><p><code>max()</code> 함수의 괄호 안에 들어갈 수 있는 값:</p>
<ol>
<li><strong>여러 개의 개별 인자</strong>:<ul>
<li><code>max(1, 3, 2)</code>는 <code>3</code>을 반환합니다.</li>
<li><code>max(&#39;apple&#39;, &#39;banana&#39;, &#39;cherry&#39;)</code>는 <code>cherry</code>를 반환합니다. (사전 순으로 비교)</li>
</ul>
</li>
<li><strong>하나의 iterable (리스트, 튜플 등)</strong>:<ul>
<li><code>max([1, 3, 2])</code>는 <code>3</code>을 반환합니다.</li>
<li><code>max((&#39;apple&#39;, &#39;banana&#39;, &#39;cherry&#39;))</code>는 <code>cherry</code>를 반환합니다. </li>
</ul>
</li>
<li><strong>key 인자를 사용하는 경우</strong>:<ul>
<li><code>max([&#39;apple&#39;, &#39;banana&#39;, &#39;cherry&#39;], key=len)</code>는 가장 긴 문자열인 <code>banana</code>를 반환합니다.</li>
<li><code>max([{&#39;a&#39;: 1}, {&#39;b&#39;: 2, &#39;c&#39;: 3}], key=len)</code>는 가장 많은 키를 가진 딕셔너리 <code>{&#39;b&#39;: 2, &#39;c&#39;: 3}</code>를 반환합니다.</li>
<li></li>
</ul>
</li>
</ol>
</li>
<li><p>python에서 문자열을 정수로 변환하기 위해서는 문자열을 단순히 <code>int()</code> 함수</p>
</li>
<li><p>문자열로 변환하기 위해서는 정수를 단순히 <code>str()</code> 함수</p>
<h2 id="2-문자열-리스트-관리">2. 문자열 리스트 관리</h2>
<pre><code class="language-python">arr = [&quot;banana&quot;, &quot;apple&quot;, &quot;pizza&quot;]
</code></pre>
</li>
</ul>
<p>print(arr[0])      # banana &lt;- 첫 번째 문자열
print(len(arr[0])) # 6      &lt;- 첫 번째 문자열의 길이</p>
<p>print(arr[0][0])   # b &lt;- 첫 번째 문자열의 첫 번째 문자
print(arr[0][1])   # a &lt;- 첫 번째 문자열의 두 번째 문자
print(arr[0][2])   # n &lt;- 첫 번째 문자열의 세 번째 문자</p>
<pre><code>
- 끝에 있는 3개의 문자열만 순서대로 출력하는 코드
```python hl:12
n = 5
# 방법1
arr = []
for _ in range(n):
    given_str = input()
    arr.append(given_str)

# 방법2
arr = [ input() for _ in range(n) ]

# 공통
for string in arr[-3:]:
    print(string) </code></pre><h2 id="3-문자열-붙이기">3. 문자열 붙이기</h2>
<ul>
<li>문자열끼리 + 연산</li>
<li><code>join()</code> 함수: 여러 문자열을 하나로 합치는 과정을 더 쉽게 구현 가능
  join 함수는 각 원소들을 특정 구분값(<code>sep</code>)을 기준으로 합쳐주는 함수.<pre><code class="language-python">sep.join(리스트)
</code></pre>
</li>
</ul>
<p>&#39;,&#39;.join([&#39;1&#39;, &#39;2&#39;, &#39;3&#39;])    -&gt; &#39;1,2,3&#39;
&#39;:&#39;.join([&#39;11&#39;, &#39;22&#39;, &#39;33&#39;]) -&gt; &#39;11:22:33&#39;
&#39;&#39;.join([&#39;a&#39;,&#39;b&#39;,&#39;c&#39;])       -&gt; &#39;abc&#39;</p>
<pre><code>
따라서 3개의 문자열을 합치는 코드를 다음과 같이 작성해볼 수 있습니다.
```python
a, b, c = &quot;apple&quot;, &quot;banana&quot;, &quot;candy&quot;
tot_str = &quot;&quot;.join([a, b, c])
print(tot_str)

&gt;&gt; applebananacandy</code></pre><h2 id="4-문자열-찾기">4. 문자열 찾기</h2>
<ul>
<li>문자열 안에 찾는 거 존재하는지 여부<ul>
<li>for loop 돌려서 찾기</li>
<li><code>in</code> 키워드로 찾기</li>
</ul>
</li>
<li>특정 문자열이 포함된 위치 구하기 <ul>
<li>for loop 돌리기</li>
<li><code>find()</code> 함수 사용<ul>
<li>해당 부분 문자열이 없는 경우에는 -1을, 있는 경우에는 가장 앞에 나오는 부분 문자열의 위치를 반환</li>
</ul>
</li>
<li><code>index()</code> 함수로 찾기<ul>
<li>유의해야 할 점은, 부분 문자열이 없는 경우에는 다음과 같이 ValueError가 발생함<pre><code class="language-python">s = &#39;appleabanana&#39;
</code></pre>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>if &#39;ab&#39; in s:
    print(s.index(&#39;ab&#39;))
else:
    print(-1)</p>
<p>s = &#39;appleabanana&#39;
print(s.find(&#39;bob&#39;))</p>
<pre><code>
## 5. 문자열 내 문자 수정

- 문자열은 `immutable`, 즉 변할 수 없는 타입이기 때문에 내부 문자를 절대 변경할 수 없음
    - =&gt; 2가지 방법
        1. **아예 새로운 문자열을 만들어 주는 것**
            -  바뀌어야 하는 문자 양옆으로 slicing을 적용하여 해당 문자를 제외하고는 그대로 유지가 되도록 새로운 문자열을 만들어주는 방식
        - 2. **각 문자를 원소로 갖는 리스트로 변환**
            - 문자열을 `list()`라는 함수로 감싸면, 각 문자를 원소로 갖는 리스트가 만들어지고, 이러한 문자들로만 이루어진 리스트는 `join()` 함수를 이용하여 다시 문자열로 합쳐줄 수 있습니다.
```python hl:3,9,11
# 방법1
s = &#39;baaana&#39;
s = s[:2] + &#39;n&#39; + s[3:]
print(s)
&gt;&gt; banana

# 방법2
s = &#39;baaana&#39;
arr = list(s)
arr[2] = &#39;n&#39;
s = &#39;&#39;.join(arr)
print(s)

&gt;&gt; &#39;banana&#39;</code></pre><ul>
<li>도전문제: 문자열이 주어지면, 문자열 내에서 두 번째 문자와 같은 문자들을 모두 첫 번째 문자로 바꾸어 출력하는 프로그램<pre><code class="language-python">a = input()
arr = list(a)
m = a[0]
n = a[1]
for i in range(len(arr)): # 이때 그냥 for i in arr 이렇게 하면 바꿔도 반영 안돼!!
    if arr[i] == n:
        arr[i] = m
a = &#39;&#39;.join(arr)
print(a)</code></pre>
</li>
</ul>
<h2 id="6-문자열-내-문자-제거">6. 문자열 내 문자 제거</h2>
<ul>
<li>방법1: slicing<pre><code class="language-python">s = &#39;baknana&#39;
s = s[:2] + s[3:]
print(s)
</code></pre>
</li>
</ul>
<blockquote>
<blockquote>
<p>banana</p>
</blockquote>
</blockquote>
<pre><code>- 방법2: 문자열을 list로 변환한 뒤, pop 함수를 이용해 해당 문자를 제거하고 다시 문자열로 변환
```python hl:3
s = &#39;baknana&#39;
arr = list(s)
arr.pop(2)
s = &#39;&#39;.join(arr)
print(s)

&gt;&gt; banana</code></pre><ul>
<li>도전문제: 문자열 A가 주어졌을 때, 명령에 따라 문자열 A를 민 결과를 출력하는 프로그램을 작성해보세요.
명령은 <code>L</code> 또는 <code>R</code> 로 이루어진 명령 문자열로 주어지고, 명령에 따라 순서대로 다음을 반복합니다.</li>
<li>L일 때는 좌측으로 한 칸 밀고, R일 때는 우측으로 한 칸 미는 것을 진행합니다.<pre><code class="language-python">k = input()
n = input()
</code></pre>
</li>
</ul>
<p>for i in n:
    if i == &#39;L&#39;:
        k = k[+1:] + k[:+1]
    else:
         k = k[-1:] + k[:-1]
print(k)</p>
<pre><code>- 처음에 L과 R의 계산을 한번에 해서 실행하려고 했는데 그러면, 이런 반례(문자열의 개수보다 이동횟수가 많은 경우)에 오류가 생김
    qebh
    LLLLL


## 7. 아스키 코드
-  ASCII: python 언어에서 사용할 수 있는 모든 문자들은 전부 하나의 숫자와 대응
-  아스키 코드 (ASCII)
- ord(): 특정 문자의 아스키 코드 값
```python hl:1
&gt;&gt; print(ord(&#39;A&#39;)) #따옴표 넣어줘야 돼

65</code></pre><ul>
<li> <code>chr()</code> 함수<pre><code class="language-python">&gt;&gt; print(chr(65))
</code></pre>
</li>
</ul>
<p>&#39;A&#39;</p>
<pre><code>
- 알파벳 x 다음 알파벳을 구하는 것:  `chr(ord(x) + 1)`

- 대소문자 변경
    - 1. 아스키 코드 활용
    - 2. `lower()` 함수 사용(&lt;-&gt; `upper()`)
```python hl:6,10
# 방법1
x = &#39;C&#39;
ord(x)                            -&gt; 67
ord(x) - ord(&#39;A&#39;)                 -&gt; 2 (67 - 65)
ord(x) - ord(&#39;A&#39;) + ord(&#39;a&#39;)      -&gt; 99 (2 + 97)
chr(ord(x) - ord(&#39;A&#39;) + ord(&#39;a&#39;)) -&gt; &#39;c&#39;

# 방법2
x = &#39;C&#39;
print(x.lower())

&gt;&gt; &#39;c&#39;
</code></pre><ul>
<li> x가 알파벳인지에 대한 여부는 python에서 <code>isalpha</code>라는 함수를 제공합니다. <ul>
<li>숫자인지에 대한 여부 역시 <code>x.isdigit()</code>이라는 함수</li>
</ul>
</li>
</ul>
<h2 id="8-문자열-비교">8. 문자열 비교</h2>
<ul>
<li>두 문자열이 동일한지 비교할 때 숫자 비교와 마찬가지로 <code>==</code> 연산자를 그대로 사용</li>
<li>python에서의 문자열 끼리는 <code>&lt;</code>, <code>&gt;</code> 연산 가능<ul>
<li>이는 <strong>사전순</strong>으로 앞선 문자열이 누구인가에 대한 비교이며, 값이 더 작을 수록 사전순으로 앞서다는 것</li>
</ul>
</li>
</ul>
<hr>
<h1 id="코드트리-novice-low-python으로-수료">@코드트리 Novice Low python으로 수료!@</h1>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코드트리 조별과제] Python 08. 2차원 배열]]></title>
            <link>https://velog.io/@funda__mental_/%EC%BD%94%EB%93%9C%ED%8A%B8%EB%A6%AC-%EC%A1%B0%EB%B3%84%EA%B3%BC%EC%A0%9C-Python-08.-2%EC%B0%A8%EC%9B%90-%EB%B0%B0%EC%97%B4</link>
            <guid>https://velog.io/@funda__mental_/%EC%BD%94%EB%93%9C%ED%8A%B8%EB%A6%AC-%EC%A1%B0%EB%B3%84%EA%B3%BC%EC%A0%9C-Python-08.-2%EC%B0%A8%EC%9B%90-%EB%B0%B0%EC%97%B4</guid>
            <pubDate>Wed, 31 Jul 2024 03:14:32 GMT</pubDate>
            <description><![CDATA[<h2 id="1-배열-입력-받기">1. 배열 입력 받기</h2>
<p>n개의 줄에 걸쳐 각 n개의 숫자를 공백을 사이에 두고 <strong>입력받는</strong> 코드</p>
<pre><code class="language-python">n = 4
arr_2d = []
for _ in range(n):
    arr_1d = list(map(int, input().split()))
    arr_2d.append(arr_1d)

print(arr_2d)

&gt;&gt; 출력 결과
[[1, 2, 3, 4], [7, 8, 9, 10], [11, 12, 13, 14], [15, 16, 17, 18]]</code></pre>
<ul>
<li>어떤 배열을 정의하고 for loop 안에 append만 사용하는 경우는 <code>list comprehension</code>을 이용할 수 있습니다.<pre><code class="language-python">n = 4
arr_2d = [
  list(map(int, input().split()))
  for _ in range(n)
]
print(arr_2d)</code></pre>
</li>
</ul>
<h2 id="2-배열-원소에-대한-접근">2. 배열 원소에 대한 접근</h2>
<pre><code class="language-python">&quot;&quot;&quot;
i/j  0  1  2  3
0    1  2  3  4
1    7  8  9 10
2   11 12 13 14
3   15 16 17 18
&quot;&quot;&quot;
arr_2d = [[1, 2, 3, 4], [7, 8, 9, 10], [11, 12, 13, 14], [15, 16, 17, 18]]
print(arr_2d[0][0]) # 1행 1열 -&gt; 1
print(arr_2d[1][2]) # 2행 3열 -&gt; 9</code></pre>
<h2 id="3-새로운-2차원-배열-선언">3. 새로운 2차원 배열 선언</h2>
<p>n * m 크기의 격자라면 다음과 같이 초기화가 가능합니다. list comprehension 사용시 <strong>n, m 위치</strong>에 유의합니다.</p>
<pre><code class="language-python">n, m = 4, 5
arr_2d = [
    [0 for _ in range(m)] # m개의 0을 가진 리스트를 생성(열)
    for _ in range(n) # n개의 행을 가진 2차원 리스트를 생성(행)
]
print(arr_2d)

&gt;&gt; 출력 결과
[[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]</code></pre>
<h2 id="4-2차원-배열-출력">4. 2차원 배열 출력</h2>
<pre><code class="language-python">n = 4
arr_2d = [[1, 2, 3, 4], [7, 8, 9, 10], [11, 12, 13, 14], [15, 16, 17, 18]]

#방법 1
for i in range(n): #행
    for j in range(n): #열
        print(arr_2d[i][j], end=&quot; &quot;)
    print()

#방법2
for row in arr_2d: #행
    for elem in row: #열
        print(elem, end=&quot; &quot;)
    print()</code></pre>
<h2 id="5-다양한-활용">5. 다양한 활용</h2>
<ul>
<li>신기한 규칙: 손으로 숫자를 쭉 적다가 내려서 다시 반대 방향으로 적는 것을 반복하는 것<pre><code class="language-python">입력
n = 4
</code></pre>
</li>
</ul>
<p>출력
1 2 3 4
8 7 6 5
9 10 11 12
16 15 14 13</p>
<pre><code>=&gt; 홀수번째 행에 대해서는 우측으로 이동하며 숫자를 적어야 하고, 짝수번째 행에 대해서는 좌측으로 이동하며 숫자를 적는 식으로 코드를 작성하면 됩니다. &amp; num 변수 사용해서 계속 더해줌

```python hl:3-6
# 0으로 채워진 배열 만들기 
n = 4
arr_2d = [
    [0 for _ in range(n)]
    for _ in range(n)
]

num = 1
# 짝수, 홀수에 따른 배열 숫자 입력(대체)
for i in range(n):
    if i % 2 == 0:
        for j in range(n):
            arr_2d[i][j] = num
            num += 1
    else:
        for j in range(n - 1, -1, -1):
            arr_2d[i][j] = num
            num += 1

# 출력
for row in arr_2d:
    for elem in row:
        print(elem, end=&quot; &quot;)
    print()</code></pre><p><em>어디를 기준으로 연속성이 있는지 봐야됨. if 같은 행으로 쭉 가는 경우, 행이 밖 loop 이런 식</em></p>
<h3 id="2차-격자점으로서의-배열">2차 격자점으로서의 배열</h3>
<p>2차 평면 상에 n개의 점이 주어졌을 때, (a, b)가 해당 점들 중 하나인지를 쉽게 판단할 수는 없을까요?</p>
<ul>
<li>방법1: n개의 점들 중 (a, b)가 있는지를 직접 확인<pre><code class="language-python">a, b = 1, 3
exists = False
for _ in range(n):
  r, c = tuple(map(int, input().split()))
  if r == a and c == b:
      exists = True
</code></pre>
</li>
</ul>
<p>print(exists)</p>
<pre><code>
- 방법2: 점들의 위치 (r, c)의 범위가 1에서 10 사이라면 다음과 같이 0으로 초기화된 2차원 배열을 만든 뒤, 각 점들이 주어질때마다 해당 위치에 1로 표시해주는 방식
```python hl:4,9,10
a, b = 1, 3

placed = [
   [0 for _ in range(10)]
   for _ in range(10)
]

for _ in range(n):
   r, c = tuple(map(int, input().split()))
   placed[r - 1][c - 1] = 1

exists = True if placed[a - 1][b - 1] == 1 else False
print(exists)</code></pre><ul>
<li>도전문제 
첫 번째 줄에 격자의 크기 n과 점의 개수 m이 공백을 두고 주어집니다.
두 번째 줄부터 m개의 줄에 걸쳐 각 점의 행 번호와 열 번호가 공백을 사이에 두고 주어집니다.
주어진 m개의 점의 위치는 모두 다름을 가정해도 좋습니다.</li>
<li>1 ≤ n ≤ 10</li>
<li>1 ≤ m ≤ n * n
예제1
입력:<pre><code class="language-python">3 3
1 1
3 2
3 3</code></pre>
출력:<pre><code class="language-python">1 0 0
0 0 0
0 6 9</code></pre>
정답<pre><code class="language-python">k = input().split()
n, m = int(k[0]), int(k[1])
</code></pre>
</li>
</ul>
<p>arr = []  </p>
<p>for _ in range(m):
    arr.append(tuple(map(int, input().split())))</p>
<p>arr2 = [
    [0 for _ in range(n)]
    for _ in range(n)
]</p>
<p>for i in range(1, n+1):
    for j in range(1, n+1):
        if (i, j) in arr: #헷갈림 - tuple이므로 ( , )
            arr2[i-1][j-1] = i*j
for i in range(n):
    for j in range(n):
        print(arr2[i][j], end =&quot; &quot;)</p>
<p>    print()
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python 07. 1차원 배열]]></title>
            <link>https://velog.io/@funda__mental_/Python-07.-1%EC%B0%A8%EC%9B%90-%EB%B0%B0%EC%97%B4</link>
            <guid>https://velog.io/@funda__mental_/Python-07.-1%EC%B0%A8%EC%9B%90-%EB%B0%B0%EC%97%B4</guid>
            <pubDate>Sat, 27 Jul 2024 10:38:42 GMT</pubDate>
            <description><![CDATA[<ul>
<li>한 줄에 공백을 사이에 두고 10개의 숫자가 주어졌을 때, 이 숫자들의 합을 구하는 코드<pre><code class="language-python">arr = input().split()
</code></pre>
</li>
</ul>
<p>sum_val = 0
for i in range(10):
    sum_val += int(arr[i])
print(sum_val)</p>
<p>for elem in arr: 
    print(elem)</p>
<p>#map을 이용하면 리스트에 있는 원소 type을 전부 변환한 이후의 리스트를 반환할 수 있음
arr = list(map(int, input().split()))
print(arr)</p>
<blockquote>
<blockquote>
<p>1 3
[1, 3]</p>
</blockquote>
</blockquote>
<h1 id="2개의-숫자만-입력받는-경우-tuple-사용-가능">2개의 숫자만 입력받는 경우, tuple 사용 가능</h1>
<p>n, m = tuple(map(int, input().split()))
print(n, m)</p>
<blockquote>
<blockquote>
<p>1 3
1 3</p>
</blockquote>
</blockquote>
<pre><code>
- ## Slicing

리스트에 대해 slice(`[]`)를 활용하면 일부 범위, 조건에 해당하는 원소들을 가져올 수 있습니다.
```python hl:1
arr[start:end:step]</code></pre><p>start index에서 시작하여 end index 직전까지 step씩 뛰며 전진합니다.</p>
<pre><code class="language-python">index  0  1  2  3  4
=====================
arr = [1, 2, 3, 4, 5]
print(arr[1:3:1]) # 1번째 index부터 3번째 index 전까지 1씩 증가
print(arr[1:3]) #이렇게 표현해도 동일
print(arr[:3]) #처음부터 받고 싶은 경우
print(arr[3:0:-1]) # 3번째 index부터 0번째 index 전까지 -1씩 감소하면서
print(arr[::-1]) # 전체 원소 뒤집기</code></pre>
<ul>
<li> 10개의 원소를 입력받아 뒤집는 문제는 다음과 같이 코드를 작성<pre><code class="language-python">arr = list(map(int, input().split()))
reversed_arr = arr[::-1]
</code></pre>
</li>
</ul>
<p>for elem in reversed_arr:
    print(elem, end=&quot; &quot;)</p>
<pre><code>

## 리스트 push, pop, len

```python hl:2,4
# 비어있는 리스트
arr = []
혹은
arr = list()</code></pre><ul>
<li><strong>append 함수</strong>를 이용하면, <strong>리스트의 맨 끝에 원소를 추가</strong>할 수 있습니다.<pre><code class="language-python">arr = [3, 5]
arr.append(9) #append 괄호에 들어갈 원소 쓰기
print(arr)
</code></pre>
</li>
</ul>
<blockquote>
<blockquote>
<p>[3, 5, 9]</p>
</blockquote>
</blockquote>
<pre><code>
- **pop 함수**를 이용하면, **리스트의 맨 뒤에 있는 원소를 지울** 수 있습니다.
```python hl:2
arr = [3, 5, 9]
arr.pop()
print(arr)

&gt;&gt; [3, 5]</code></pre><ul>
<li> <code>len()</code> 함수: 해당 리스트에 있는 원소의 개수를 반환해줍니다.</li>
</ul>
<pre><code class="language-python">&gt;&gt; print(len([1, 3, 5]))
3</code></pre>
<ul>
<li>모든 원소의 합을 구하기<pre><code class="language-python">arr = [1, 2, 2, 5]
</code></pre>
</li>
</ul>
<p>n = len(arr)</p>
<p>sum_val = 0
for i in range(n):
    sum_val += arr[i]</p>
<p>print(sum_val)</p>
<blockquote>
<blockquote>
<p>10</p>
</blockquote>
</blockquote>
<pre><code>
이때 len 함수를 이용하지 않고, 다음과 같이 코드를 작성하여 원소를 바로 꺼내는 식으로도 해결이 가능
```python hl:4-5,7
arr = [1, 2, 2, 5]

sum_val = 0
for elem in arr:
    sum_val += elem
#or
sum_val = sum(arr) #sum 함수 사용

print(sum_val)

&gt;&gt; 10</code></pre><ul>
<li><p>sum 함수란?</p>
<ul>
<li>sum(iterable, start) 형태</li>
<li>iterable: 리스트나 튜플 등 숫자로 이루어진 객체</li>
<li>start: 처음에 더해줄 값</li>
<li>그러니까, iterable 내의 값을 다 더해주고 start도 더해주는 거</li>
</ul>
</li>
<li><p>2번째 숫자부터 끝까지의 합을 구하기 위해서는 range를 꼭 사용해야만 할까요?<br>이런 경우에는, slicing을 활용해볼 수 있습니다.</p>
<pre><code class="language-python">arr = [1, 2, 2, 5]
</code></pre>
</li>
</ul>
<p>sum_val = 0
for elem in arr[1:]:
    sum_val += elem
#or
sum_val = sum(arr[1:])</p>
<p>print(sum_val)</p>
<blockquote>
<blockquote>
<p>9</p>
</blockquote>
</blockquote>
<pre><code>
## 음수 index
 arr[-1]의 의미는 끝에서부터 첫 번째 원소, arr[-2]의 의미는 끝에서부터 2번째 원소라는 의미를 갖습니다.
```python
index  0  1  2  3
index -4 -3 -2 -1
==================
arr = [1, 2, 3, 5]</code></pre><p>따라서 가장 끝에 있는 2개의 원소를 더해 출력하는 코드는 다음과 같이 작성해볼 수 있습니다.</p>
<pre><code class="language-python">arr = list(map(int, input().split()))

print(arr[-1] + arr[-2])</code></pre>
<h2 id="list-comprehension">list comprehension</h2>
<p>선언과 동시에 for loop으로부터 나온 원소를 원하는 값으로 변경해줄 수 있습니다</p>
<pre><code class="language-python">[(append 안에 들어갈 내용) (for loop)]

# 조건문을 포함하는 list comprehension
[(append 안에 들어갈 내용) (for loop) &lt;조건문&gt;]</code></pre>
<pre><code class="language-python"># 리스트 arr의 각 원소들을 전부 2배씩 해주는 코드
arr = [1, 2, 3, 5]
new_arr = [elem * 2 for elem in arr]

# 홀수의 제곱수 배열
list_ = [i ** 2 for i in range(1, 10) if i % 2 == 1]</code></pre>
<h4 id="변수가-두-개-이상인-list-comprehension">변수가 두 개 이상인 list comprehension</h4>
<pre><code class="language-python">list_ = [(i + j) for i in range(3) for j in range(3)]</code></pre>
<p>해당 코드의 결과는 다음과 같습니다.</p>
<pre><code class="language-python">[0, 1, 2, 1, 2, 3, 2, 3, 4]</code></pre>
<p> for 문을 사용해 표현하면 다음과 같습니다.</p>
<pre><code class="language-python">list_ = []
for i in range(3):
    for j in range(3):
        list_.append(i + j)</code></pre>
<h4 id="피보나치-수열">피보나치 수열</h4>
<pre><code class="language-python">arr = [0, 1, 1]

# 3번째 항부터 10번째 항까지 추가 - 배열이 이미 있어도 append로 그냥 넣을 수 있음 
for i in range(3, 11): 
    arr.append(arr[-1] + arr[-2])

# 10번째 항 출력
print(arr[10])

&gt;&gt; 55

# or - p: 전항, pp: 전전항

pp, p = 1, 1

for _ in range(3, 11):
    pp, p = p, pp + p

print(p)

&gt;&gt; 55</code></pre>
<h4 id="counting-배열">Counting 배열</h4>
<p>전부 0으로 초기화된 크기가 7인 배열이 필요합니다. (1부터 6까지를 각각 index로 사용한다고 했을 때)</p>
<pre><code class="language-python"># 숫자 별 출현 횟수 1  2  3  4  5  6
count_arr = [0, 0, 0, 0, 0, 0, 0]

# 혹은 다음과 같이도 정의가 가능합니다.
count_arr = [0] * 7

# list comprehension을 이용할수도 있습니다.
count_arr = [0 for _ in range(7)]</code></pre>
<p>10번 던져 나온 숫자 목록을 arr라 한다면, count_arr의 해당 숫자 index에 1씩 증가시키는 식으로 진행해볼 수 있습니다.</p>
<pre><code class="language-python">count_arr = [0] * 7

# 개수 세기
arr = list(map(int, input().split()))
for elem in arr:
    count_arr[elem] += 1

# 개수 출력
for i in range(1, 7):
    cnt = count_arr[i]
    print(f&quot;숫자 {i} - {cnt}번&quot;)</code></pre>
<h3 id="탐색---문자를-원소로-갖는-배열">탐색 - 문자를 원소로 갖는 배열</h3>
<ul>
<li> <code>enumerate()</code> 함수<ul>
<li>원소와 index가 동시에 필요한 경우</li>
<li>enumerate로 list를 감싸면, 원소로 (index, 원소)를 동시에 받으며 진행할 수 있음</li>
<li>형식: <code>enumerate(순서가 있는 객체, start=0)</code></li>
</ul>
</li>
</ul>
<pre><code class="language-python">word = [&#39;A&#39;, &#39;P&#39;, &#39;P&#39;, &#39;L&#39;, &#39;E&#39;]

# 해당 문자를 찾지 못했다면 -1
idx = -1

# 문자 탐색
for i, char in enumerate(word):
    if char == &#39;L&#39;:
        idx = i

# 문자가 존재하지 않는 경우
if idx == -1:
    print(&quot;not exist&quot;)
else:
    print(idx)

&gt;&gt; 3</code></pre>
<ul>
<li>특정 원소가 리스트 안에 있는지, 없는지에 대해서:  <code>in</code>, <code>not in</code> 이라는 키워드<pre><code class="language-python">word = [&#39;A&#39;, &#39;P&#39;, &#39;P&#39;, &#39;L&#39;, &#39;E&#39;]
</code></pre>
</li>
</ul>
<p>if &#39;L&#39; in word:
    print(&#39;L is in list&#39;)
if &#39;L&#39; not in word:
    print(&#39;L is not in list&#39;)</p>
<blockquote>
<blockquote>
<p>L is in list</p>
</blockquote>
</blockquote>
<pre><code>
- 이미 존재하는 원소가 list의 &lt;u&gt;어느 index에 있는지&lt;/u&gt;는 `.index()` 함수
    - 존재하지 않는 원소에 대해 index 함수를 이용하면 **Value Error**
    - index 함수 이용시에는 꼭 먼저 해당 원소가 리스트에 포함되는지를 확인한 이후에 진행
    - 만약 문자가 여러 개 존재한다면, 그 중 가장 앞에 있는 index를 반환합니다.
```python hl:3-4
word = [&#39;A&#39;, &#39;P&#39;, &#39;P&#39;, &#39;L&#39;, &#39;E&#39;]

if &#39;K&#39; in word:
    print(word.index(&#39;K&#39;))</code></pre><ul>
<li>특정 원소의 개수<ul>
<li><code>count()</code>라는 함수를 사용해볼 수도 있습니다.<pre><code class="language-python">word = [&#39;A&#39;, &#39;P&#39;, &#39;P&#39;, &#39;L&#39;, &#39;E&#39;]
</code></pre>
</li>
</ul>
</li>
</ul>
<p>cnt = word.count(&#39;P&#39;)
print(cnt)</p>
<blockquote>
<blockquote>
<p>2</p>
</blockquote>
</blockquote>
<pre><code>
### Min/Max
- 최대: `max(arr)`
```python hl:4
arr = [-1, -5, -2, -5, -3, -9]
# 첫번째 원소를 max로 설정하고 두번째 값부터 비교하면서 알아내기
max_val = arr[0]
for elem in arr[1:]:
    if elem &gt; max_val:
        max_val = elem

print(max_val)</code></pre><ul>
<li>최소: <code>min(arr)</code></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python 05, 06. 단순 반복문, 다중 반복문]]></title>
            <link>https://velog.io/@funda__mental_/Python-05-06.-%EB%8B%A8%EC%88%9C-%EB%B0%98%EB%B3%B5%EB%AC%B8-%EB%8B%A4%EC%A4%91-%EB%B0%98%EB%B3%B5%EB%AC%B8</link>
            <guid>https://velog.io/@funda__mental_/Python-05-06.-%EB%8B%A8%EC%88%9C-%EB%B0%98%EB%B3%B5%EB%AC%B8-%EB%8B%A4%EC%A4%91-%EB%B0%98%EB%B3%B5%EB%AC%B8</guid>
            <pubDate>Fri, 26 Jul 2024 02:39:21 GMT</pubDate>
            <description><![CDATA[<h3 id="for">for</h3>
<ul>
<li>증가 시<ul>
<li>i = a부터 <strong>b - 1</strong>까지</li>
</ul>
</li>
<li>감소 시<ul>
<li>i = a부터 b+1까지 </li>
</ul>
</li>
<li><strong>b에 등호가 들어가지 않음</strong></li>
<li>for문 뒤에는 꼭 <code>:</code>를 </li>
<li>indentation으로 분리</li>
</ul>
<pre><code class="language-python">for i in range(4): # i = 0 ~ 3

for i in range(a, b):
    코드 작성
    이 위치에 i값이 a부터 b - 1까지 1씩 증가하며 들어옴

for i in range(5, 11, 2): print(i) #2씩 증가 5,7,9

`range(a, b, -1)`는 a부터 b + 1까지 1씩 감소

for _ in range(5): #변수가 별로 중요하지 않은 경우
    print(1, end=&quot;&quot;) 
</code></pre>
<ul>
<li>도전 문제<ul>
<li>n을 입력받아, 1부터 n까지 공백을 두고 출력하는 프로그램을 작성해보세요.<pre><code class="language-python"># 변수 선언, 입력
n = int(input())
</code></pre>
</li>
</ul>
</li>
</ul>
<h1 id="출력">출력</h1>
<p>for i in range(1, n + 1):
    print(i, end=&quot; &quot;) #이렇게 하면 \n 자동 실행되지 않아. </p>
<pre><code>
### while문
```python
i = 5           &lt;- 시작 값

while i &lt;= 10:  &lt;- 계속 반복할 조건
    print(i)    &lt;- 원하는 코드 작성
    i += 1      &lt;- 원하는 증감값 설정

print(&quot;Done&quot;)

while True: #계속 반복</code></pre><h3 id="continue문">continue문</h3>
<ul>
<li><p>for, while 안에서만 사용이 가능</p>
</li>
<li><p>조건이 만족하는 경우에는 <strong>아래 코드를 더 이상 실행하지 않고</strong> 다시 위로 올라가 그 다음 for loop을 실행</p>
</li>
<li><p><strong>조건문과 같이 쓰여야 함</strong></p>
<pre><code class="language-python">for i in range(5, 11):
  코드1
  if i % 2 == 0:
      코드2
      continue
  코드3
#이 경우 i = 짝수라면, 코드3이 실행되지 않고 바로 다시 for문</code></pre>
</li>
<li><p>도전문제</p>
<ul>
<li>조건에 부합하는 수
  정수 a 를 입력받아 1부터 a까지의 수 중 다음 조건을 모두 만족하지 않는 수들을 모두 출력하는 프로그램을 작성해보세요.<ul>
<li>짝수이면서 4의 배수가 아닌 수</li>
<li>8로 나눈 몫이 짝수인 수</li>
<li>7로 나눈 나머지가 4보다 작은 수
입력 형식: 첫 번째 줄에 정수 a가 공백을 두고 주어집니다 1 ≤ a ≤ 100
출력 형식: 첫 번째 줄에 1부터 a까지의 수 중 문제의 조건을 모두 만족하지 않는 수들을 공백을 사이에 두고 오름차순으로 출력합니다.<pre><code class="language-python"># 변수 선언, 입력
a = int(input())
</code></pre>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h1 id="조건을-모두-만족하지-않는-수들만-출력합니다">조건을 모두 만족하지 않는 수들만 출력합니다.</h1>
<p>for i in range(1, a + 1):
    if(i % 2 == 0 and i % 4 != 0) or (i // 8) % 2 == 0 or (i % 7) &lt; 4:
        continue
    print(i, end=&quot; &quot;)</p>
<pre><code>
### break문
- for, while 안에서만 사용
- **가장 가까이에 있는 &lt;u&gt;for loop&lt;/u&gt;을 완전히 탈출**
- **조건문과 같이 쓰여야 함**
다음 코드에서는 i가 6, 7, 8, 9인 경우에는 순서대로 `코드1` -&gt; `코드3`이 계속 수행되다가, i가 10이 되는 순간에 `코드1` -&gt; `코드2`가 수행되며 for loop을 탈출하여 바로 `코드4`가 수행
```python hl:5,8,10
for i in range(6, 101):
    코드1
    if i % 5 == 0:
        코드2
        break
    코드3

코드4

while True:
    n = int(input())
    if n &gt;= 10:
        코드1
        break
    코드2

코드3
</code></pre><hr>
<blockquote>
<p><strong>모양의 규칙부터 찾아야 돼!!!</strong></p>
<ul>
<li><strong>행이 1 증가함에 따라 변화하는 별의 개수</strong>에 집중</li>
</ul>
</blockquote>
<p>3 * 5 크기의 격자 =  3개의 행과 5개의 열 = 가로 5, 세로 3</p>
<pre><code class="language-python">for _ in range(3):
    for _ in range(5):
        print(1, end=&quot;&quot;)
    print()</code></pre>
<ul>
<li>직각 삼각형<ul>
<li>규칙: 행이 1씩 증가함에 따라 열도 1씩 증가함<pre><code class="language-python">for i in range(4):         -&gt; i는 0부터 3까지 돌면 됩니다.
for j in range(i + 1): -&gt; j는 0부터 i 까지 돌면 됩니다. 
  print(&quot;*&quot;, end=&quot;&quot;)
print()
</code></pre>
</li>
</ul>
</li>
</ul>
<pre><code>
- 행에 대하여 대칭인 경우</code></pre><hr>
<p>**
*
**</p>
<hr>
<pre><code>```python hl:2,6-7
#감소하는 경우
for i in range(3): #i=0,1,2
    for j in range(3 - i): #j=3,2,1
        print(&quot;*&quot;, end=&quot;&quot;)
    print()
for i in range(1, -1, -1): #i=1,0
    for j in range(3 - i): #j=2,3
        print(&quot;*&quot;, end=&quot;&quot;)
    print()

#증가하는 경우
*
* *
*

# 변수 선언 및 입력
n = int(input())

# 길이가 n인 직각삼각형을 출력합니다.
for i in range(n):
    for j in range(i+1):
        print(&quot;*&quot;, end=&quot; &quot;)
    print()

# 길이가 n-1인 직각삼각형을 뒤집어 출력합니다.
for i in range(n-2, -1, -1):
    for _ in range(i+1):
        print(&quot;*&quot;, end=&quot; &quot;)
    print()</code></pre><ul>
<li>열에 대해 대칭인 경우 - 앞에 공백이 붙는 경우<ul>
<li>행에 따른 공백의 개수를 변화를 파악 후, 별 출력 전에 공백 출력</li>
<li>공백의 개수 = 격자의 개수 -별의 개수</li>
<li>뒤에 있는 공백은 출력 X</li>
</ul>
</li>
<li>실력체크: 가로 세로 2n-1 크기에 해당하는 격자에 체크 다이아몬드 모양을 그리는 코드를 작성해보세요.
규칙은 다음과 같습니다.</li>
<li>n = 3 일 때<pre><code>*
* *</code></pre></li>
</ul>
<hr>
<ul>
<li>*
*<pre><code></code></pre></li>
</ul>
<pre><code class="language-python"># 변수 선언 및 입력
n = int(input())

# 모양에 맞게 위쪽 별을 출력합니다.
for i in range(n):
    for _ in range(n - i - 1):
        print(&quot; &quot;, end=&quot;&quot;)
    for _ in range(i + 1):
        print(&quot;* &quot;, end=&quot;&quot;)
    print()

# 모양에 맞게 아래쪽 별을 출력합니다.
for i in range(n-2, -1, -1):
    for _ in range(n - i - 1):
        print(&quot; &quot;, end=&quot;&quot;)
    for _ in range(i + 1):
        print(&quot;* &quot;, end=&quot;&quot;)
    print()</code></pre>
<ul>
<li>행에 따라 모양이 다른 경우<ul>
<li>if 문을 통해서 짝수, 홀수일 때마다 다르게 출력</li>
</ul>
</li>
</ul>
<h3 id="아스키-코드-ascii">아스키 코드 (ASCII)</h3>
<ul>
<li>ord(): 특정 문자의 아스키 코드 값<pre><code class="language-python">&gt;&gt; print(ord(&#39;A&#39;)) #따옴표 넣어줘야 돼
</code></pre>
</li>
</ul>
<p>65</p>
<pre><code>-  `chr()` 함수
```python hl:1
&gt;&gt; print(chr(65))

&#39;A&#39;</code></pre><ul>
<li><p>알파벳 x 다음 알파벳을 구하는 것:  <code>chr(ord(x) + 1)</code></p>
</li>
<li><p>n번 반복</p>
<ul>
<li>실력체크 : n번에 걸쳐 두 정수 a, b가 주어졌을 때, a부터 b까지의 짝수를 모두 합한 결과를 출력하는 프로그램을 작성해보세요.<ul>
<li>입력 형식
첫 번째 줄에 테스트케이스의 수 n이 주어집니다.
두 번째 줄 부터 n개의 줄에 걸쳐 한 줄에 두 정수 a, b가 공백을 두고 주어집니다.</li>
<li>1 ≤ n ≤ 10</li>
<li>1 ≤ a ≤ b ≤ 100<ul>
<li>출력 형식
첫 번째 줄부터 n개의 줄에 걸쳐, 각 줄에 각 테스트 케이스마다 a이상 b이하의 짝수들의 합을 출력합니다.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<pre><code class="language-python"># 변수 선언 및 입력
n = int(input())

#n회 반복합니다.
for i in range(n):
    # 변수 선언, 입력
    inp = input()
    arr = inp.split()
    a, b = int(arr[0]), int(arr[1])

    ans = 0

    # a부터 b까지 짝수를 전부 더한 값을 출력합니다.
    for j in range(a, b + 1): #for문 하나만 써서 if 절로 나누면 됨
        if j % 2 == 0:
            ans += j

    print(ans)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코드트리 조별과제] Python 03. 연산자, 04. 조건문]]></title>
            <link>https://velog.io/@funda__mental_/Python-03.-%EC%97%B0%EC%82%B0%EC%9E%90-04.-%EC%A1%B0%EA%B1%B4%EB%AC%B8</link>
            <guid>https://velog.io/@funda__mental_/Python-03.-%EC%97%B0%EC%82%B0%EC%9E%90-04.-%EC%A1%B0%EA%B1%B4%EB%AC%B8</guid>
            <pubDate>Wed, 24 Jul 2024 08:25:45 GMT</pubDate>
            <description><![CDATA[<h2 id="03연산자">03.연산자</h2>
<p>ab을 계산하기 위해서는 <code>**</code> 연산 =&gt;</p>
<pre><code class="language-python">a**b
a !=b
a += b #a = a + 5</code></pre>
<ul>
<li>증감연산자 파이썬에서는 적용 X<ul>
<li>i++ 이런 거 X =&gt; i += 1 이렇게 써야 돼 </li>
</ul>
</li>
</ul>
<hr>
<ul>
<li>if : 사용<ul>
<li>indentation으로 범위 표시</li>
</ul>
</li>
</ul>
<pre><code class="language-python">if 조건:
    여기에 조건이 참일 경우에만 수행되는 코드 작성
    여기에 써도 됨
else:
    여기에 조건이 거짓일 경우에만 수행되는 코드 작성

이 위치에 있는 코드는 조건과 무관하게 항상 수행됩니다.
</code></pre>
<hr>
<h2 id="04-조건문">04. 조건문</h2>
<h3 id="삼항연산자">삼항연산자</h3>
<h5 id="기존-코드">기존 코드</h5>
<pre><code class="language-python">if 조건:
    a = v1
else:
    a = v2</code></pre>
<h5 id="삼항연산자를-이용한-코드">삼항연산자를 이용한 코드</h5>
<pre><code class="language-python">a = v1 if 조건 else v2</code></pre>
<ul>
<li><code>2 + 6</code>은 항상 계산되기를 원하고, 삼항 연산자에 따라 5또는 6이 선택되기를 원했다면,</li>
</ul>
<pre><code class="language-python">n = 8
a = 2 + 6 + (5 if n &gt; 10 else 6)
print(a)</code></pre>
<ul>
<li><p>도전 문제</p>
<ul>
<li><p>정수 a가 주어졌을 때, a가 1이라면 <code>t</code> 를 출력하고, 1이 아니라면 <code>f</code> 를 출력하는 프로그램 삼항연산자를 사용하여 작성해보세요.</p>
<p>입력 형식
첫 번째 줄에 정수 a가 주어집니다.</p>
</li>
<li><p>0 ≤ a ≤ 100</p>
<p>출력 형식
첫 번째 줄에 a가 1이라면 <code>t</code> 를 출력하고, 1이 아니라면 <code>f</code> 를 출력합니다</p>
<pre><code class="language-python">a = int(input())    
print((&#39;t&#39; if a == 1 else &#39;f&#39;))</code></pre>
</li>
</ul>
</li>
</ul>
<h3 id="if-elif-else-조건문">if elif else 조건문</h3>
<h3 id="비교-연산자">비교 연산자</h3>
<p>비교 연산자는 식이 옳은지 틀린지에 따라 참(True) 혹은 거짓(False) 값을 반환
  &lt; &gt; ==</p>
<h3 id="and">and</h3>
<pre><code class="language-python">a = int(input())

if a % 2 == 0 and a &gt;= 10: # 괄호 안 쳐도 됨
    print(&quot;special&quot;)
else:
    print(&quot;normal&quot;)</code></pre>
<h3 id="중첩-조건문">중첩 조건문</h3>
<pre><code class="language-python">if a % 2 == 1:
    if a &gt;= 10:
        print(&#39;A&#39;)
    else:
        print(&#39;B&#39;)
else:
    if a &gt;= 15:
        print(&#39;C&#39;)
    else:
        print(&#39;D&#39;)
</code></pre>
]]></description>
        </item>
    </channel>
</rss>