<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>myhousemouse 님의 블로그</title>
    <link>https://myhousemouse.tistory.com/</link>
    <description>myhousemouse 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Sat, 27 Jun 2026 11:44:21 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>myhousemouse</managingEditor>
    <item>
      <title>PM 회고: 처음 해본 기획, 오래 가져간 프로젝트</title>
      <link>https://myhousemouse.tistory.com/9</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ject_logo_black.svg&quot; data-origin-width=&quot;351&quot; data-origin-height=&quot;80&quot;&gt;&lt;a href=&quot;https://ject.kr/&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPl6Xt/dJMcabxPV2n/zdWGNDS8Xcoane8UCcs10k/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPl6Xt%2FdJMcabxPV2n%2FzdWGNDS8Xcoane8UCcs10k%2Ftfile.svg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;351&quot; height=&quot;80&quot; data-filename=&quot;ject_logo_black.svg&quot; data-origin-width=&quot;351&quot; data-origin-height=&quot;80&quot;/&gt;&lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;젝트는&amp;nbsp;기획자,&amp;nbsp;디자이너,&amp;nbsp;개발자가&amp;nbsp;한&amp;nbsp;팀이&amp;nbsp;되어&amp;nbsp;실제&amp;nbsp;서비스를&amp;nbsp;만들어보는&amp;nbsp;IT&amp;nbsp;사이드&amp;nbsp;프로젝트&amp;nbsp;동아리입니다.&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;아이디어 기획부터 MVP 개발, 사용자 테스트, 마케팅까지 경험할 수 있고, 협업이 처음인 사람도 운영진과 팀원 들의 도움을 받으며&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;같이 성장해나가는 IT 동아리입니다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;[공식 사이트 링크: &lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://ject.kr&quot;&gt;https://ject.kr&lt;/a&gt;]&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 활동은 젝트에서 진행한 프로젝트입니다&lt;/span&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;처음이라 들어왔고, 끝까지 해보며 배웠다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PM 직무는 이번에 처음 맡아봤다. 처음부터 잘할 수 있을 거라 생각했다기보다는, 직접 해보면서 배우고 싶다는 마음이 컸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;막상 프로젝트를 시작해보니 PM이 해야 하는 일은 생각보다 넓었다. 문제를 정의하고, 기능을 정하고, 팀원들의 이해를 맞추고, 일정 안에서 계속 선택해야 했다. 특히 하나의 프로젝트를 길게 가져가다 보니 초반 기획이 얼마나 중요한지 체감하게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 정한 방향이 흐릿하면 뒤로 갈수록 흔들렸다. 그래서 기획은 멋진 아이디어를 많이 내는 일보다, 우리가 무엇을 할지와 무엇을 하지 않을지를 분명히 정하는 일에 가깝다고 느꼈다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;많이 만드는 것보다, 정확히 남기는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;젝트 안에서도 여러 팀이 방향을 여러 번 바꾸는 것을 보며, 기획자로서 가장 어려운 지점은 &amp;lsquo;탄탄한 기획&amp;rsquo;과 &amp;lsquo;MVP 설정&amp;rsquo; 사이에 있다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 좋은 기능을 많이 넣고 싶어진다. 하지만 프로젝트 기간과 팀 리소스는 정해져 있다. 결국 중요한 건 우리가 검증하고 싶은 핵심이 무엇인지, 지금 단계에서 꼭 필요한 기능이 무엇인지 판단하는 일이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 팀도 개발을 진행하면서 데이터셋 확보와 정제의 어려움을 마주했다. 원래는 크리에이터와 마케터의 연결고리를 만드는 기능을 주요 페인포인트와 맞닿은 기능으로 보고 있었지만, 데이터셋을 안정적으로 다루기 어렵다고 판단해 후순위로 미뤘다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결정을 하면서 MVP를 정한다는 건 단순히 기능을 줄이는 일이 아니라는 걸 배웠다. 프로젝트의 중심을 남기기 위해, 지금 할 수 없는 것을 인정하는 과정이기도 했다. 다만 그 기능을 어떻게 다시 풀어낼 수 있을지, 보조 기능이나 다른 접근으로 해결할 수 있을지는 앞으로 더 고민해보고 싶은 과제로 남았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 경험을 통해 PM은 더 많이 만드는 사람이 아니라, 더 정확하게 남기는 사람이어야 한다는 걸 배웠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비대면 협업, 저절로 굴러가지 않았다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;젝트는 대면 일정이 약 4-5번 정도라 대부분의 협업이 비대면으로 진행됐다. 그래서 마일스톤 관리와 회의 진행이 체계적으로 되어 있어야 프로젝트가 움직인다는 걸 알게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비대면에서는 모두가 같은 화면을 보고 있다고 해서 같은 생각을 하고 있는 건 아니었다. 회의 전에 무엇을 결정할지 정리하고, 회의 후에는 결정 사항과 다음 액션을 남겨야 했다. 그 과정이 흐려지면 일정도, 역할도, 우선순위도 함께 흐려졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jira를 처음 사용해본 것도 기억에 남는다. 비대면으로 프로젝트를 관리해야 하다 보니 티켓 단위로 업무를 나누고 확인하는 방식이 생각보다 유용했다. 회의 중 결정 사항이 계속 바뀌는 상황도 많았는데, 그때마다 메모하고 다시 정리해 전달하는 습관이 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 팀장과 PM의 역할이 얼마나 중요한지 크게 느꼈다. 팀이 바쁘고 각자의 속도가 다를수록, 누군가는 현재 위치와 다음 액션을 계속 잡아줘야 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발자 언어와 디자이너 언어, 그 사이에서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PM으로 일하면서 가장 많이 배운 습관 중 하나는 팀원들의 싱크를 맞추는 일이었다. 단순히 내용을 전달하는 것만으로는 부족했다. 누구에게 전달하느냐에 따라 용어와 설명 방식도 달라져야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자의 말을 디자이너가 이해할 수 있게 풀어주고, 디자이너의 의도를 개발자가 구현 가능한 방식으로 이해할 수 있게 이어주는 일이 필요했다. 때로는 개발자 언어를 번역하는 일처럼 느껴지기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 과정에서 PM은 가운데에서 말을 옮기는 사람이 아니라, 서로 다른 관점을 같은 방향으로 맞추는 사람이라는 생각이 들었다. 같은 내용을 공유하더라도 모두가 같은 방식으로 이해하지 않는다는 점을 계속 의식하게 됐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사이드 프로젝트였지만, 협업은 진짜였다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;젝트는 대부분 사이드 프로젝트의 성격이 강했다. 협업 프로젝트가 처음인 사람도 있었고, 직장인처럼 다른 일정과 병행하는 사람도 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 이 환경이 좋았던 이유는 혼자 부딪히는 구조가 아니었기 때문이다. 협업이 처음이어도 경험이 있는 팀원들에게 도움을 받을 수 있었고, 운영진분들도 단순히 동아리를 운영하는 데 그치지 않고 각 팀의 멘토처럼 함께 고민해주셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원들에게도 많이 배웠다. 프로젝트 중간에 어려움이 생겨도 각자의 책임을 묵묵히 진행해주는 모습이 고마웠다. 잘 모르는 부분을 알려주려 하고, 다른 사람의 의견을 쉽게 깎아내리지 않는 태도도 인상 깊었다. 함께 일하는 방식 자체에서 배운 점이 많았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아이디어에서 인스타 운영까지 가봤다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;젝트에서 특히 흥미로웠던 점은 자율성이 넓다는 점이었다. 우리 팀은 프로젝트 기간이 늘어났을 때 마케팅 TF팀을 꾸렸고, 한 달 넘게 마케팅만 별도로 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 그중 인스타그램 운영을 맡았다. 인스타를 잘 아는 편은 아니었지만, 직접 해보니 생각보다 정해야 할 것이 많았다. 아이데이션부터 콘텐츠 레이아웃, 톤앤매너, 업로드 흐름까지 하나씩 잡아야 했다. 어떻게 하면 더 많은 사람에게 닿을 수 있을지, 어떤 메시지가 바이럴될 수 있을지 고민하는 것도 쉽지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 4-5주 동안 아이디어를 기획하고 끝내는 것이 아니라, 서비스를 어떻게 알릴지까지 고민해볼 수 있었다는 점이 새로웠다. 실제 프로젝트는 기획에서 끝나는 것이 아니라 사용자에게 닿아야 완성된다는 점을 배웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다들 완벽히 아는 상태에서 시작한 것은 아니었다. 그래도 각자 할 수 있는 만큼 찾아보고, 시도하고, 조율하면서 프로젝트를 끌고 갔다. 그 과정이 힘들기도 했지만, 열정만으로도 팀이 움직일 수 있다는 점이 재미있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 사용자를 만나니, 기획이 다시 보였다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UT 세션이 있어 실제 사용자를 만나볼 수 있었던 점도 매력적이었다. 기획자가 상상한 흐름과 사용자가 실제로 쓰는 흐름은 달랐ㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 서비스가 정보성 플랫폼이다 보니, 사용자가 너무 많은 정보에 버거워하는 지점도 있었다. 반면 AI 요약 같은 기능에는 긍정적인 반응을 보여주어 조금의 자신감도 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경험을 통해 기획은 머릿속에서 완성되는 것이 아니라는 걸 느꼈다. 실제 사용자가 어디에서 막히는지, 어떤 기능에 반응하는지 봐야 비로소 다음 방향을 잡을 수 있었다. 사용자 반응을 직접 보는 일은 PM으로서 꼭 필요한 배움이었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결국 어려웠던 건 사람과 운영이었다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아쉬운 점도 있었다. 사이드 프로젝트 특성상 젝트에 온전히 집중할 수 있는 사람이 많지는 않았다. 각자의 일과 일정이 있다 보니 참여도와 속도에 차이가 생겼고, 인원 변동도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 팀장 입장에서는 운영 스트레스가 컸을 것 같다고 느꼈다. 10명이라는 비교적 많은 인원을 이끌어본다는 건 분명 큰 경험이지만, 동시에 쉬운 일이 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인원 변동이 있을 때마다 온보딩을 다시 진행해야 했고, 새로 합류한 팀원이 현재 프로젝트 상황에 빠르게 얼라인되도록 돕는 것도 어려웠다. 문서가 있어도 맥락까지 전달하는 데는 시간이 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경험을 통해 PM에게 필요한 역량은 기획력만이 아니라는 걸 알게 됐다. 일정을 보는 힘, 사람을 살피는 힘, 팀의 맥락을 유지하는 힘도 중요했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음엔 더 탄탄한 기획으로 시작하고 싶다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에 다시 PM을 맡는다면 더 탄탄한 기획을 해보고 싶다. 문제의 백그라운드와 사용자 맥락을 더 깊게 이해하고, 페인포인트를 어떤 구조로 풀어갈지 초반에 더 치열하게 고민하고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 처음이라 부딪히며 배운 부분이 많았다. 그래서 앞으로는 기획의 근거를 더 잘 쌓고, 회의 아젠다와 마일스톤, 온보딩 문서도 더 체계적으로 준비해보고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획은 처음에 한 번 정하고 끝나는 문서가 아니었다. 팀이 계속 같은 방향을 볼 수 있도록 업데이트하고, 정리하고, 선택하는 과정이었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PM은 팀이 길을 잃지 않게 하는 사람&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;젝트에서의 경험은 PM이라는 역할을 더 현실적으로 이해하게 해줬다. 좋은 아이디어보다 중요한 건 팀이 실제로 만들 수 있는 방향을 정하는 것. 기능을 많이 넣는 것보다 중요한 건 핵심을 끝까지 지키는 것. 그리고 협업에서 중요한 건 완벽한 사람이 모이는 것이 아니라, 부족해도 계속 맞춰가는 구조를 만드는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 PM을 맡으며 많이 헤맸지만, 그만큼 많이 배웠다. 이번 프로젝트를 통해 PM은 프로젝트를 앞에서 끌고 가는 사람인 동시에, 팀이 길을 잃지 않도록 계속 기준을 잡아주는 사람이라는 걸 배웠다.&lt;/p&gt;</description>
      <category>it동아리</category>
      <category>PM</category>
      <category>사이드프로젝트</category>
      <category>사프</category>
      <category>연합동아리</category>
      <category>젝트</category>
      <category>젝트동아리</category>
      <author>myhousemouse</author>
      <guid isPermaLink="true">https://myhousemouse.tistory.com/9</guid>
      <comments>https://myhousemouse.tistory.com/9#entry9comment</comments>
      <pubDate>Sat, 20 Jun 2026 23:30:47 +0900</pubDate>
    </item>
    <item>
      <title>백엔드 스터디 후기</title>
      <link>https://myhousemouse.tistory.com/8</link>
      <description>&lt;h3 id=&quot;-기본-정보&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  기본 정보&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;주차/주제&lt;/b&gt;: 8주차 - 예외 처리와 데이터 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-학습-내용-요약&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  학습 내용 요약&lt;/h3&gt;
&lt;h2 id=&quot;spring-boot의-기본-예외-처리-방식-조사하기&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Spring Boot의 기본 예외 처리 방식 조사하기&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;public Article findById(long id) {
        return blogRepository.findById(id)
                .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;not found : &quot; + id));
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;id를 입력받아, 특정 블로그 글을 찾은 다음, IllegalArgumentException 예외로 &amp;rarr; not found${id} 에러메시지 보냄&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;{ 
&quot;timestamp&quot;: 2023-04-16T07:28:34. 039+00: 00&quot;, # 예외 발생 시간
&quot;status&quot;: 500, # HTTP 상태 코드
&quot;error&quot;: &quot;Internal Server Error&quot; , # 예외 유형
&quot;path&quot;: &quot;/api/articles/123&quot; # 예외가 발생한 요청 경로 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예외 발생&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨트롤러나 서비스 레이어에서 예외(런타임 예외, 검증 실패 등) 발생&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1164&quot; data-origin-height=&quot;644&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uGAEw/dJMb99NeAQM/8XwEoy2bByZ9mE727iAE10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uGAEw/dJMb99NeAQM/8XwEoy2bByZ9mE727iAE10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uGAEw/dJMb99NeAQM/8XwEoy2bByZ9mE727iAE10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuGAEw%2FdJMb99NeAQM%2F8XwEoy2bByZ9mE727iAE10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1164&quot; height=&quot;644&quot; data-origin-width=&quot;1164&quot; data-origin-height=&quot;644&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;handlerexceptionresolver-체인&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;HandlerExceptionResolver 체인&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예외를 처리하는 인터페이스&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ExceptionHandlerExceptionResolver (우선순위: 높음)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;@ExceptionHandler&lt;/b&gt;&amp;nbsp;어노테이션이 달린 메서드를 찾아 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@ControllerAdvice&lt;/b&gt;&amp;nbsp;클래스 처리&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;ResponseStatusExceptionResolver (우선순위: 중간)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;@ResponseStatus&lt;/b&gt;&amp;nbsp;어노테이션이나&amp;nbsp;&lt;b&gt;ResponseStatusException&lt;/b&gt;을 처리&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;DefaultHandlerExceptionResolver (우선순위: 낮음)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Spring MVC 내부에서 발생하는 예외를 적절한 HTTP 상태 코드로 변환&lt;/li&gt;
&lt;li&gt;예외 타입에 따라 적절한 HTTP 상태 코드(400, 405 등)를 설정&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@ExceptionHandler&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;컨트롤러 내 특정 예외를 잡아서 처리&lt;/li&gt;
&lt;li&gt;이는 전역으로 사용할 수 없다 &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;@RestControllerAdvice(@ControllerAdvice) 을 사용해야 함&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@ControllerAdvice/RestControllerAdvice&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;애플리케이션 전역에서 예외를 한 곳에서 처리&lt;/li&gt;
&lt;li&gt;컨트롤러마다&lt;span&gt;&amp;nbsp;&lt;/span&gt;@ExceptionHandler&lt;span&gt;&amp;nbsp;&lt;/span&gt;쓸 필요 없음&lt;/li&gt;
&lt;li&gt;@RestControllerAdvice&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;에러 응답을&amp;nbsp;JSON&lt;span&gt;&amp;nbsp;&lt;/span&gt;으로&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@ResponseStatus&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;특정 예외 클래스에 붙여서 그 예외가 발생하면 상태 코드(404, 400 등)를 지정&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ResponseStatusException&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;코드 중간에서 바로 상태 코드와 메시지를 던질 수 있는 예외&lt;/li&gt;
&lt;li&gt;@ResponseStatus보다 메시지를 동적으로 지정 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;defaulterrorattributes&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;DefaultErrorAttributes&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;: 예외 발생 시&amp;nbsp;&lt;b&gt;에러 정보 수집&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;무엇을 하는가?&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예외가 어느 단계에서도 응답으로 커밋(commit)되지 않고,&lt;/li&gt;
&lt;li&gt;최종적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;/error로 포워딩&lt;/b&gt;되었을 때,&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Map&amp;lt;String,Object&amp;gt;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;형태로
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;timestamp,&lt;span&gt;&amp;nbsp;&lt;/span&gt;status,&lt;span&gt;&amp;nbsp;&lt;/span&gt;error,&lt;span&gt;&amp;nbsp;&lt;/span&gt;message,&lt;span&gt;&amp;nbsp;&lt;/span&gt;path&lt;span&gt;&amp;nbsp;&lt;/span&gt;등의 공통 속성을 담아 제공합니다&lt;/li&gt;
&lt;li&gt;에러 속성을 저장하며 직접 예외를 처리하지는 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장 포인트&lt;/b&gt;:&amp;nbsp;&lt;b&gt;getErrorAttributes()&lt;/b&gt;&amp;nbsp;메서드 오버라이드를 통해 커스텀 필드 추가 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;scala&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {
    @Override
    public Map&amp;lt;String, Object&amp;gt; getErrorAttributes(...) {
        Map&amp;lt;String, Object&amp;gt; attrs = super.getErrorAttributes(...);
        attrs.put(&quot;traceId&quot;, UUID.randomUUID().toString());
        return attrs;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DefaultErrorAttributes가 예외 정보 수집 (getErrorAttributes())&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DefaultErrorAttribute - 스프링부트 기본 제공. 에러 &quot;데이터&quot;를 만듬&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에러 발생 시, 에러에 대한 다양한 속성(메시지, 상태코드, 예외 등)을 Map 형태로 생성해주는 역할을 합니다.&lt;/li&gt;
&lt;li&gt;즉, 에러 응답에 포함될 데이터(에러 정보)를 구성합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ErrorAttribute에 추가 정보를 담아 구현하여 빈으로 등록 &amp;rarr; ErrorAttribute에 맞춰 에러메시지 제작&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ex) DefaultErrorAttribute &amp;rarr; customValue라는 키를 등록&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;BasicErrorController &amp;rarr; 그 데이터를 &quot;응답&quot;으로 만들어 반환&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제로 /error 엔드포인트를 처리하는 컨트롤러&lt;/li&gt;
&lt;li&gt;내부적으로 DefaultErrorAttributes를 사용해 에러 정보를 받아와서, JSON 또는 HTML로 응답&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;basicerrorcontroller&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;BasicErrorController&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;: 스프링 기본 예외처리 컨트롤러&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;WAS가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;/error&lt;/b&gt;&amp;nbsp;경로 요청 처리 &amp;rarr;&amp;nbsp;&lt;b&gt;HTML/JSON 응답 생성&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저 &amp;rarr; HTML 에러 페이지(Whitelabel)를 보여줌&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;588&quot; data-origin-height=&quot;150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qtTK5/dJMcabj4bu1/64z5SNVMkiN5Yk4FHv6v9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qtTK5/dJMcabj4bu1/64z5SNVMkiN5Yk4FHv6v9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qtTK5/dJMcabj4bu1/64z5SNVMkiN5Yk4FHv6v9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqtTK5%2FdJMcabj4bu1%2F64z5SNVMkiN5Yk4FHv6v9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;588&quot; height=&quot;150&quot; data-origin-width=&quot;588&quot; data-origin-height=&quot;150&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;li&gt;API &amp;rarr; { status:500, error:&quot;Internal Server Error&quot;, &amp;hellip; } 같은 기본 JSON을 돌려줌&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;404(리소스 없음)도 500(서버 에러)로 나옴&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에러 메시지&amp;middot;코드가 너무 일반적&lt;/b&gt;이라 &amp;ldquo;무슨 문제가 있었는지&amp;rdquo; 알기 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dart&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Controller
// 1) Spring 환경 내에 server.error.path 혹은 
// error.pth로 등록된 property의 값을 넣거나 없는 경우에는 /error을 사용한다.
@RequestMapping(&quot;${server.error.path:${error.path:/error}}&quot;)
public class BasicErrorController extends AbstractErrorController {

    // 2) HTML 형식의 에러 페이지 요청 처리
    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request,
                                  HttpServletResponse response) {
        // (2-1) 요청에서 HTTP 상태 코드(404, 500 등)를 꺼내고
        HttpStatus status = getStatus(request);

        // (2-2) 에러 정보를 Map 형태로 가져옵니다.
        //      기본적으로 timestamp, status, error, message, path 등이 담겨있음
        Map&amp;lt;String, Object&amp;gt; model = getErrorAttributes(request, false);

        // (2-3) 실제 응답 상태 코드를 설정한 뒤
        response.setStatus(status.value());

        // (2-4) &quot;error&quot;라는 이름의 뷰(HTML)를 렌더링하며,
        //       model에 담긴 에러 정보를 화면에 뿌려줍니다.
        return new ModelAndView(&quot;error&quot;, model);
    }

    // 3) JSON&amp;middot;API 형식의 에러 응답 처리
    @RequestMapping
    public ResponseEntity&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt; errorApi(HttpServletRequest request) {
        // (3-1) 에러 정보를 Map 형태로 가져옵니다.
        Map&amp;lt;String, Object&amp;gt; body = getErrorAttributes(request, false);

        // (3-2) 상태 코드를 꺼내서
        HttpStatus status = getStatus(request);

        // (3-3) body(Map)를 JSON으로 자동 변환해 리턴
        return new ResponseEntity&amp;lt;&amp;gt;(body, status);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;getErrorAttributes &amp;larr; 부모 클래스인 AbstractErrorController&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;public class BasicErrorContorller extends **AbstractErrorController**&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;public abstract class AbstractErrorController implements ErrorController {
  private final ErrorAttributes errorAttributes;
    
  protected Map&amp;lt;String, Object&amp;gt; getErrorAttributes(HttpServletRequest request,
    boolean includeStackTrace) {

    WebRequest webRequest = new ServletWebRequest(request);
    return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;예외처리-적용&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;예외처리 적용&lt;/h3&gt;
&lt;h2 id=&quot;errorcode-enum&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;ErrorCode (Enum)&lt;/h2&gt;
&lt;blockquote style=&quot;background-color: #f8f9fa; color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 별, Http Status code와 에러 메시지를 정의하는 파일이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Getter
public enum ErrorCode {
    INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, &quot;E1&quot;, &quot;올바르지 않은 입력값입니다.&quot;),
    METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, &quot;E2&quot;, &quot;잘못된 HTTP 메서드를 호출했습니다.&quot;),
    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, &quot;E3&quot;, &quot;서버 에러가 발생했습니다.&quot;),
    NOT_FOUND(HttpStatus.NOT_FOUND, &quot;E4&quot;, &quot;존재하지 않는 엔티티입니다.&quot;),

    ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, &quot;A1&quot;, &quot;존재하지 않는 아티클입니다.&quot;);

    private final String message;

    private final String code;
    private final HttpStatus status;

    ErrorCode(final HttpStatus status, final String code, final String message) {
        this.status = status;
        this.code = code;
        this.message = message;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;errorresponse&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;ErrorResponse&lt;/h2&gt;
&lt;blockquote style=&quot;background-color: #f8f9fa; color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트에게 전달할 에러의 응답 형태를 정의하는 클래스다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ErrorResponse {

    private String message;
    private String code;

    private ErrorResponse(final ErrorCode code) {
        this.message = code.getMessage();
        this.code = code.getCode();
    }

    public ErrorResponse(final ErrorCode code, final String message) {
        this.message = message;
        this.code = code.getCode();
    }

    public static ErrorResponse of(final ErrorCode code) {
        return new ErrorResponse(code);
    }

    public static ErrorResponse of(final ErrorCode code, final String message) {
        return new ErrorResponse(code, message);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;globalexceptionhandler&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;GlobalExceptionHandler&lt;/h2&gt;
&lt;blockquote style=&quot;background-color: #f8f9fa; color: #212529; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 언급한&amp;nbsp;@RestControllerAdvice,&amp;nbsp;@ExceptionHandler를 사용해 전역 예외 처리를 담당하는 Class이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Slf4j
@ControllerAdvice // 모든 컨트롤러에서 발생하는 예외를 잡아서 처리
public class GlobalExceptionHandler {

    // 지원하지 않은 HTTP method 호출 할 경우 발생
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class) // HttpRequestMethodNotSupportedException 예외를 잡아서 처리
    protected ResponseEntity&amp;lt;ErrorResponse&amp;gt; handle(HttpRequestMethodNotSupportedException e) {
        log.error(&quot;HttpRequestMethodNotSupportedException&quot;, e);
        return createErrorResponseEntity(ErrorCode.METHOD_NOT_ALLOWED);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    protected ResponseEntity&amp;lt;ErrorResponse&amp;gt; handle(MethodArgumentNotValidException e) {
        log.error(&quot;MethodArgumentNotValidException&quot;, e);
        return createErrorResponseEntity(ErrorCode.INVALID_INPUT_VALUE);
    }

    @ExceptionHandler(BusinessBaseException.class)
    protected ResponseEntity&amp;lt;ErrorResponse&amp;gt; handle(BusinessBaseException e) {
        log.error(&quot;BusinessException&quot;, e);
        return createErrorResponseEntity(e.getErrorCode());
    }

    @ExceptionHandler(Exception.class)
    protected ResponseEntity&amp;lt;ErrorResponse&amp;gt; handle(Exception e) {
        e.printStackTrace();
        log.error(&quot;Exception&quot;, e);
        return createErrorResponseEntity(ErrorCode.INTERNAL_SERVER_ERROR);
    }

    private ResponseEntity&amp;lt;ErrorResponse&amp;gt; createErrorResponseEntity(ErrorCode errorCode) {
        return new ResponseEntity&amp;lt;&amp;gt;(
                ErrorResponse.of(errorCode),
                errorCode.getStatus());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccQID4/dJMcabj4bu3/py23WaXunsPKjTXQA8OPj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccQID4/dJMcabj4bu3/py23WaXunsPKjTXQA8OPj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccQID4/dJMcabj4bu3/py23WaXunsPKjTXQA8OPj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccQID4%2FdJMcabj4bu3%2Fpy23WaXunsPKjTXQA8OPj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;998&quot; height=&quot;464&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;464&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;scala&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;public class BusinessBaseException extends RuntimeException {

    private final ErrorCode errorCode;

    public BusinessBaseException(String message, ErrorCode errorCode) {
        super(message);
        this.errorCode = errorCode;
    }

    public BusinessBaseException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
    }

    public ErrorCode getErrorCode() {
        return errorCode;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;입력값-검증이-필요한-상황-3가지-생각해오기&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;입력값 검증이 필요한 상황 3가지 생각해오기&lt;/h2&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;입력값 검증 : 사용자가 요청을 보냈을때 올바른 값인지 유효성검사를 하는 과정&lt;/p&gt;
&lt;h3 id=&quot;1-사용자-회원가입-정보-검증&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 사용자 회원가입 정보 검증&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;검증 요구사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이메일: 유효한 형식 필수&lt;/li&gt;
&lt;li&gt;비밀번호: 8~20자, 영문/숫자/특수문자 조합&lt;/li&gt;
&lt;li&gt;나이: 14세 이상 100세 이하&lt;/li&gt;
&lt;li&gt;전화번호: 010-XXXX-XXXX 형식&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mOVFG/dJMcabj4bu6/jAOPx8PbkfO7aKfKsLDC5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mOVFG/dJMcabj4bu6/jAOPx8PbkfO7aKfKsLDC5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mOVFG/dJMcabj4bu6/jAOPx8PbkfO7aKfKsLDC5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmOVFG%2FdJMcabj4bu6%2FjAOPx8PbkfO7aKfKsLDC5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;675&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;public class SignUpRequest {
    @NotBlank(message = &quot;이메일을 입력해주세요&quot;)
    @Email(message = &quot;유효한 이메일 형식이 아닙니다&quot;)
    private String email;

    @NotBlank(message = &quot;비밀번호를 입력해주세요&quot;)
    @Pattern(regexp = &quot;^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&amp;amp;])[A-Za-z\\d@$!%*#?&amp;amp;]{8,20}$&quot;,
             message = &quot;8~20자 영문/숫자/특수문자 조합 필요&quot;)
    private String password;

    @Min(value = 14, message = &quot;14세 이상만 가입 가능합니다&quot;)
    @Max(value = 100, message = &quot;유효하지 않은 나이입니다&quot;)
    private int age;

    @Pattern(regexp = &quot;^010-\\d{4}-\\d{4}$&quot;, 
             message = &quot;010-XXXX-XXXX 형식으로 입력해주세요&quot;)
    private String phone;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@Valid&amp;nbsp;어노테이션이 DTO의 검증 규칙을 자동 적용&lt;/p&gt;
&lt;h3 id=&quot;2-상품-주문-정보-검증&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 상품 주문 정보 검증&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;검증 요구사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;상품명&lt;/b&gt;: 필수 입력 항목 (&lt;b&gt;@NotBlank&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가격&lt;/b&gt;: 1,000원 ~ 1,000,000원 (&lt;b&gt;@Min&lt;/b&gt;,&amp;nbsp;&lt;b&gt;@Max&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수량&lt;/b&gt;: 1개 ~ 9,999개 (&lt;b&gt;@Min&lt;/b&gt;,&amp;nbsp;&lt;b&gt;@Max&lt;/b&gt;)&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcE6VL/dJMcabYEBH9/nhbNh0FhLyp2WKOobp8vuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcE6VL/dJMcabYEBH9/nhbNh0FhLyp2WKOobp8vuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcE6VL/dJMcabYEBH9/nhbNh0FhLyp2WKOobp8vuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcE6VL%2FdJMcabYEBH9%2FnhbNh0FhLyp2WKOobp8vuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;596&quot; height=&quot;458&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;import javax.validation.constraints.*;

@Data // Lombok 사용 (Getter/Setter 자동 생성)
public class ProductOrderRequest {

    @NotBlank(message = &quot;상품명은 필수 입력 항목입니다&quot;)
    @Size(max = 100, message = &quot;상품명은 최대 100자까지 입력 가능합니다&quot;)
    private String productName;

    @NotNull(message = &quot;가격은 필수 입력 항목입니다&quot;)
    @Min(value = 1000, message = &quot;가격은 최소 {value}원 이상이어야 합니다&quot;)
    @Max(value = 1000000, message = &quot;가격은 최대 {value}원 이하여야 합니다&quot;)
    private Integer price;

    @NotNull(message = &quot;수량은 필수 입력 항목입니다&quot;)
    @Min(value = 1, message = &quot;수량은 최소 {value}개 이상이어야 합니다&quot;)
    @Max(value = 9999, message = &quot;수량은 최대 {value}개 이하여야 합니다&quot;)
    private Integer quantity;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-파일-업로드-검증&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 파일 업로드 검증&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;검증 요구사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 타입: JPG/PNG/PDF만 허용&lt;/li&gt;
&lt;li&gt;파일 크기: 최대 5MB&lt;/li&gt;
&lt;li&gt;파일명: 특수문자 포함 금지&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자바 빈 밸리데이션&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(&lt;b&gt;Java Bean Validation)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 검증 규칙 간편하게 사용 가능&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;/**
* 문자열을 다룰 때 사용
*/
@NotNull // null 허용하지 않음
@NotEmpty // null, 빈 문자열(공백) 또는 공백만으로 채워진 문자열 허용하지 않음
@NotBlank // null, 빈 문자열(공백) 허용하지 않음
@Size(min=?, max=?) // 최소 길이, 최대 길이 제한
@Null // null만 가능

/**
* 숫자를 다룰 때 사용
*/
@Positive // 양수만 허용
@PositiveOrZero // 양수와 0만 허용
@Negative // 음수만 허용
@NegativeOrZero // 음수와 0만 허용
@Min(?) // 최솟값 제한
@Max(?) // 최댓값 제한

/**
* 정규식 관련
*/
@Email // 이메일 형식만 허용
@Pattern(regexp=&quot;?&quot;) // 직접 작성한 정규식에 맞는 문자열만 허용&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;활용 가능&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;회원가입 폼 검증&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;API 입력 파라미터 검증&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터베이스 저장 전 검증&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일 업로드 메타데이터 검증&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;-새롭게-알게-된-점--어려웠던-부분&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  새롭게 알게 된 점 &amp;amp; 어려웠던 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어려웠던 부분:&lt;br /&gt;- 학습 과정에서 깊이 있는 이해와 실습을 충분히 하지 못한 점이 아쉬움.&lt;br /&gt;- Spring의 기초 개념을 더 공부한 후에 다시 정리하면 더 정확한 이해가 가능할 것 같음.&lt;br /&gt;- 개념 이해에만 시간을 투자하다 보니 실습을 진행하지 못했는데, 향후 실습을 통해 경험을 쌓을 예정&lt;/li&gt;
&lt;li&gt;해결 방법:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기초 spring 개념 학습 후 재학습&lt;/li&gt;
&lt;li&gt;실제 실습 프로젝트에서 사용해보고 싶음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-과제실습-결과-있을-시&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  과제/실습 결과 (있을 시)&lt;/h3&gt;
&lt;pre class=&quot;scala&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;public enum ErrorCode {
    USER_NOT_FOUND(404, &quot;USR_001&quot;, &quot;사용자를 찾을 수 없습니다&quot;);
    // &amp;hellip;생성자&amp;middot;getter 생략
}

// 2) 베이스 예외
public abstract class ApplicationException extends RuntimeException {
    private final ErrorCode code; // 에러코드 Enum 

    public ApplicationException(ErrorCode code) {
        super(code.getMessage());
        this.code = code;          
    }
    public int getStatus() { return code.getStatus(); }
    public String getCode()   { return code.getCode(); }
}

// 3) **커스텀 예외 클래스 (빈칸 채우기)**  =&amp;gt; 사용자 찾지 못하는 예외 User_Not_Found 활용
public class UserNotFoundException extends ApplicationException {  
    public UserNotFoundException(Long id) {  // id 형식은 Long -&amp;gt; 
        super(ErrorCode.USER_NOT_FOUND);  
    }  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;-스터디-피드백&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  스터디 피드백&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;면접 CS 질문&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커스텀 예외를 만들 때 extends RuntimeException을 권장하는 이유는?&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DTO 레벨 검증과 서비스 레벨 검증, 어디에 어떤 검증을 두어야 할까?&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;입력값 검증만으로 방어할 수 없는 공격(예: SQL Injection, XSS)은? 추가로 어떤 조치를 해야 하나?&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 각각 질문에 대해서 자신만의 답변 생각해보기.&lt;/p&gt;</description>
      <author>myhousemouse</author>
      <guid isPermaLink="true">https://myhousemouse.tistory.com/8</guid>
      <comments>https://myhousemouse.tistory.com/8#entry8comment</comments>
      <pubDate>Mon, 25 May 2026 19:51:26 +0900</pubDate>
    </item>
    <item>
      <title>백엔드 스터디 후기</title>
      <link>https://myhousemouse.tistory.com/7</link>
      <description>&lt;h3 id=&quot;-기본-정보&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기본 정보&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;주차/주제&lt;/b&gt;: 7주차 - 인증과 보안 기초&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-학습-내용-요약&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  학습 내용 요약&lt;/h3&gt;
&lt;h3 id=&quot;1-인증authentication과-인가authorization의-차이점&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. 인증(Authentication)과 인가(Authorization)의 차이점&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;A. 인증(Authentication) : 사용자의 신원을 입증하는과정&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;: 사용자가 로그인 화면에서 아이디와 비밀번호를 입력하여 신원을 증명하는 과정&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스프링 시큐리티의 예&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;UsernamePasswordAuthenticationFilter: 사용자가 입력한 아이디와 비밀번호를 통해 인증 과정 처리&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;B. 인가(Authorization) : 사이트 특정부분에 접근할 수 있는지 파악 (사용자의 권한을 파악)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관리자 권한이 있는 사용자만 관리자 페이지에 접근 가능, 일반 사용자는 접근할 수 없도록 접근을 제어&lt;/li&gt;
&lt;li&gt;파일공유시스템&amp;rarr; 내가 접근가능한 폴더만 사용가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스프링 시큐리티의 예&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;FilterSecurityInterceptor: 사용자가 요청한 자원에 접근할 권한이 있는지 검사하여 접근 제어를 처리&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인증&lt;/b&gt;은 집 현관문을 열 때 &quot;이 집 주인이 맞나요?&quot;라고 물어보는 것&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인가&lt;/b&gt;는 집 안의 각 방을 들어갈 때 &quot;내가 이 방에 들어갈 수 있나요?&quot;라고 묻는 것과 같음&lt;/li&gt;
&lt;li&gt;항상&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;인증 &amp;rarr; 인가&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;순서로 진행(인증이 완료되지 않으면 인가 로직 자체가 동작하지 않음)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rArr; 스프링 시큐리티로 쉽게 구현 가능&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티: 보안을 담당하는 하위 프레임워크&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CSRF 공격 방지 ( = 봉인스티커)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CSRF 공격: 사용자의 권한을 가지고 특정 동작을 수행하도록 유도하는 공격&lt;/li&gt;
&lt;li&gt;Spring Security는 모든&lt;span&gt;&amp;nbsp;&lt;/span&gt;POST,&lt;span&gt;&amp;nbsp;&lt;/span&gt;PUT,&lt;span&gt;&amp;nbsp;&lt;/span&gt;DELETE&lt;span&gt;&amp;nbsp;&lt;/span&gt;요청에 대해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;CSRF 토큰&lt;/b&gt;을 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;세션고정 공격 방지 (= 비밀번호 변경 후 새 열쇠 발급)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 인증 정보를 탈취하거나, 변조하는 공격&lt;/li&gt;
&lt;li&gt;공격자가 미리 발급받은 세션 ID(쿠키)를 피해자에게 물고 들어가 로그인 유도&lt;/li&gt;
&lt;li&gt;Spring Security는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;로그인 직후&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;항상 새로운 세션 ID를 발급하도록 기본 설정(SessionAuthenticationStrategy)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;spring-security&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Spring Security&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필터기반&lt;span&gt;&amp;nbsp;&lt;/span&gt;javax.servlet.Filter로 인증&amp;middot;인가 처리&lt;/li&gt;
&lt;li&gt;세션-쿠키 방식&lt;br /&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/bxmcFd/dJMcagFAgny/AAAAAAAAAAAAAAAAAAAAAOWZsCEYW0PfMpj_Jv46x9EaOQGd-a8BOOzL9BH-SxS0/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1780239599&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=mCtEq3WjAvCvgoZZTXTGWBLVnOo%3D&quot; /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SecurityContextPersistenceFilter&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;:&lt;br /&gt;SecurityContextRepository에서 SecurityContext(접근 주체와 인증에 대한 정보를 담고 있는 객체) 를 가져오거나 저장하는 역할&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LogoutFilter&lt;/b&gt;:&lt;br /&gt;설정된 로그아웃 URL로 오는 요청을 확인해 해당 사용자를 로그아웃 처리함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UsernamePasswordAuthenticationFilter&lt;/b&gt;: 인증관리자&lt;br /&gt;폼 기반 로그인 시 사용되는 필터로 아이디 패스워드 데이터를 파싱하여 인증 요청을 위임인증이 성공하면 AuthenticationSuccessHandler, 실패하면 AuthenticationFailureHandler를 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DefaultLoginPageGeneratingFilter&lt;/b&gt;:&lt;br /&gt;사용자가 로그인 페이지를 지정하지 않았을 때 기본으로 설정하는 로그인 페이지 관련 필터&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BasicAuthenticationFilter&lt;/b&gt;:&lt;br /&gt;요청 헤더에 있는 아이디와 패스워드를 파싱해 인증 요청&lt;br /&gt;위임인증이 성공하면 AuthenticationSuccessHandler,&lt;br /&gt;실패하면 AuthenticationFailureHandler를 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RequestCacheAwareFilter&lt;/b&gt;:&lt;br /&gt;로그인 성공 후, 관련 있는 캐시 요청이 있는지 확인하고 캐시 요청 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SecurityContextHolderAwareRequestFilter&lt;/b&gt;:&lt;br /&gt;HttpServletRequest 정보를 감싸고, 필터 체인 상의 다음 필터들에게 부가 정보 제공&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AnonymousAuthenticationFilter&lt;/b&gt;:&lt;br /&gt;필터가 호출되는 시점까지 인증되지 않았다면 익명 사용자 전용 객체인 AnonymousAuthentication을 만들어 SecurityContext에 넣어준다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SessionManagementFilter&lt;/b&gt;:&lt;br /&gt;인증된 사용자와 관련된 세션 작업 진행, 세션 변조 방지 전략 설정, 유효하지 않은 세션에 대한 처리, 세션 생성 전략을 세우는 등의 작업을 처리한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ExceptionTranslationFilter&lt;/b&gt;:&lt;br /&gt;요청을 처리하는 중에 발생할 수 있는 예외를 위임하거나 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FilterSecurityInterceptor&lt;/b&gt;: 접근 결정 관리자&lt;br /&gt;AccessDecisionManager로 권한 부여 처리를 위임함으로써 접근 제어 결정을 쉽게 해준다.이 과정에서 이미 사용자 인증이 되어있으므로 유효한 사용자인지도 알 수 있음. 인가 관련 설정 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;461&quot; data-origin-height=&quot;499&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BEb77/dJMcaak0DXR/eWMrkDikv1QE1DjPYfzhm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BEb77/dJMcaak0DXR/eWMrkDikv1QE1DjPYfzhm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BEb77/dJMcaak0DXR/eWMrkDikv1QE1DjPYfzhm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBEb77%2FdJMcaak0DXR%2FeWMrkDikv1QE1DjPYfzhm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;461&quot; height=&quot;499&quot; data-origin-width=&quot;461&quot; data-origin-height=&quot;499&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SecurityContextHolder(금고 열쇠 통)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SecurityContext를 제공하는&lt;br /&gt;static 메소드(getContext)를 지원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SecurityContext (금고)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;접근 주체와 인증에 대한 정보를 담고 있는 Context&lt;br /&gt;즉, Authentication 을 담고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Authentication (보안카드 등록한 프로필)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Principal과 GrantAuthority를 제공한다.인증이 이루어지면 해당 Athentication이저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Principal (보안카드의 내정보)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유저에 해당하는 정보, 대부분의 경우 Principal로 UserDetails를 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GrantAuthority&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ROLE_ADMIN, ROLE_USER 등 Principal이 가지고 있는 권한을 나타낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스프링 시큐리티 인증 처리 과정&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1273&quot; data-origin-height=&quot;898&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsu0wp/dJMcaak0DXT/VNCw8YmZu4MzmLmMczECH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsu0wp/dJMcaak0DXT/VNCw8YmZu4MzmLmMczECH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsu0wp/dJMcaak0DXT/VNCw8YmZu4MzmLmMczECH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbsu0wp%2FdJMcaak0DXT%2FVNCw8YmZu4MzmLmMczECH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1273&quot; height=&quot;898&quot; data-origin-width=&quot;1273&quot; data-origin-height=&quot;898&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id=&quot;2-jwtjson-web-token의-기본-구조와-장점-알아보기&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. JWT(JSON Web Token)의 기본 구조와 장점 알아보기&lt;/h3&gt;
&lt;h3 id=&quot;기본구조&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;기본구조&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;헤더(Header)&lt;/b&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;내용(Payload)&lt;/b&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;서명(Signature)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;세 부분으로 구성되며, 각 부분은 마침표(.)로 구분&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;264&quot; data-origin-height=&quot;116&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rxWv8/dJMcabRIV58/nG1fDAvdyGKbUOeSlUNmWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rxWv8/dJMcabRIV58/nG1fDAvdyGKbUOeSlUNmWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rxWv8/dJMcabRIV58/nG1fDAvdyGKbUOeSlUNmWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrxWv8%2FdJMcabRIV58%2FnG1fDAvdyGKbUOeSlUNmWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;264&quot; height=&quot;116&quot; data-origin-width=&quot;264&quot; data-origin-height=&quot;116&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;헤더(Header)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토큰타입 , 해싱 알고리즘 정보 담고 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ada&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;{ &amp;ldquo;typ&amp;rdquo;:&amp;rdquo;JWT&amp;rdquo;,  토큰 타입
&amp;ldquo;alg&amp;rdquo; : &amp;ldquo;HS256&amp;rdquo; } 해싱 알고리즘&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;내용(Payload)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Payload는 클레임(claim)의 집합으로 &amp;rarr; 내용을 한덩어리로
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클레임: 키 값의 한쌍으로 이루어져 있음 &amp;rarr; JWT에 데이터 넣을수있음&lt;/li&gt;
&lt;li&gt;등록 클레임 (토큰에 대한 정보 ex. 토큰 발급자, 제목, 대상자, 만료시간, 발급 시간 등등)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;iss,&lt;span&gt;&amp;nbsp;&lt;/span&gt;sub,&lt;span&gt;&amp;nbsp;&lt;/span&gt;iat,&lt;span&gt;&amp;nbsp;&lt;/span&gt;exp&lt;span&gt;&amp;nbsp;&lt;/span&gt;등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;공개 클레임 : URI로 명명함 (공개되어도 상관 없음)&lt;/li&gt;
&lt;li&gt;비공개 클레임&lt;br /&gt;- 애플리케이션 정의 정보&lt;/li&gt;
&lt;li style=&quot;background-color: #fbfcfd;&quot;&gt;{ &quot;iss&quot;: &quot;[ajufresh@gmail.com](mailto:ajufresh@gmail.com)&quot;, // 등록된 클레임 &quot;iat&quot;: 1622370878, // 등록된 클레임 &quot;exp&quot;: 1622372678, // 등록된 클레임 &quot;&lt;a href=&quot;https://shinsunyoung.com/jwt_claims/is_admin&quot;&gt;https://shinsunyoung.com/jwt_claims/is_admin&lt;/a&gt;&quot;: true, // 공개 클레임 &quot;email&quot;: &quot;[ajufresh@gmail.com](mailto:ajufresh@gmail.com)&quot;, // 비공개 클레임 &quot;hello&quot;: &quot;안녕하세요!&quot; // 비공개 클레임 }&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사용자의 인증, 인가 정보가 담김&lt;/li&gt;
&lt;li&gt;토큰과 관련된 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서명(Signature)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토큰 변조 여부를 확인하는 용도&lt;/li&gt;
&lt;li&gt;헤더와 페이로드의 정보를 인코딩한 후, 비밀 키로 서명하여 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;lisp&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;HMACSHA256(
  base64UrlEncode(Header) + &quot;.&quot; + base64UrlEncode(Payload),
  secretKey
)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;장단점&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;장단점&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JWT 이전의 세션 기반 인증&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로그인&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 ID/PW 검증 후, 서버가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;세션 객체&lt;/b&gt;(Map 형태)를 만들고 고유 ID(예:&lt;span&gt;&amp;nbsp;&lt;/span&gt;JSESSIONID)를 발급&lt;/li&gt;
&lt;li&gt;이 ID를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;쿠키&lt;/b&gt;로 돌려줌&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요청 처리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트는 매 요청마다 쿠키의 세션 ID를 전송&lt;/li&gt;
&lt;li&gt;서버는 이 ID로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;DB/인메모리 캐시&lt;/b&gt;에 저장된 세션 정보를 조회 &amp;rarr; 사용자 정보 로딩&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;서버 상태 유지(Stateful)&lt;/b&gt;: 접속자 수만큼 메모리/DB 부하 상승&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장성 문제&lt;/b&gt;: 여러 대 서버를 띄울 때 세션 동기화 혹은 Sticky Session 필요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쿠키 의존&lt;/b&gt;: 브라우저 설정에 따라 거부될 수 있고, 모바일 앱에서는 관리 불편&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 id=&quot;jwt-장점&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;JWT 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;웹 표준 준수&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(RFC 7519)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;URL-safe&lt;/b&gt;: 쿼리 파라미터나 헤더에 안전하게 포함 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Self-contained&lt;/b&gt;: 사용자 정보나 권한 정보를 토큰에 모두 담아 서버 상태 비저장(Stateless)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필요한 모든 정보를 하나의 객체에 담아서 전달하기 때문에 JWT 하나로 인증을 마칠 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장성&lt;/b&gt;: 서버에 세션 저장소 불필요 &amp;rarr; 인프라 비용 절감&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설정 가능한 만료/발급 시간&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;exp,&lt;span&gt;&amp;nbsp;&lt;/span&gt;iat를 통해 만료 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;jwt-단점&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;JWT 단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;토큰 무효화 어려움&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;stateless 방식 &amp;rarr; 탈취된 토큰은 만료 전까지 유효 &amp;rarr; 블랙리스트/키 회전 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;크기 증가&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Header.Payload.Signature&lt;span&gt;&amp;nbsp;&lt;/span&gt;3부분, 각 파트에 들어가는 정보(클레임)가 많아질수록 길이가 커짐&lt;/li&gt;
&lt;li&gt;HTTP 요청마다&lt;span&gt;&amp;nbsp;&lt;/span&gt;Authorization: Bearer {JWT}&lt;span&gt;&amp;nbsp;&lt;/span&gt;헤더에 수백~수천 바이트의 문자열을 전송해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안 이슈&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;XSS&amp;middot;CSRF 공격에 취약할 수 있어 HTTPS, Secure/Cookie 설정 권장
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;XSS(크로스 사이트 스크립팅) : 악성 스크립트 삽입 &amp;rarr; 방문 이용자가 스크립트 실행되도록 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;보완책&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;보완책&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;리프레시 토큰&lt;/b&gt;: 액세스 토큰 만료 시 재발급용으로 사용&lt;br /&gt;- 액세스 토큰이 만료되어 새로운 액세스 토큰을 만들때 사용&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Uo2XC/dJMcagFAgnx/6Mrs6htX6Wvvq33z9V4na0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Uo2XC/dJMcagFAgnx/6Mrs6htX6Wvvq33z9V4na0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Uo2XC/dJMcagFAgnx/6Mrs6htX6Wvvq33z9V4na0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUo2XC%2FdJMcagFAgnx%2F6Mrs6htX6Wvvq33z9V4na0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;794&quot; height=&quot;584&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfYw5H/dJMcabRIV59/aioh1NXffCvIYjWl4HQgl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfYw5H/dJMcabRIV59/aioh1NXffCvIYjWl4HQgl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfYw5H/dJMcabRIV59/aioh1NXffCvIYjWl4HQgl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfYw5H%2FdJMcabRIV59%2Faioh1NXffCvIYjWl4HQgl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;653&quot; height=&quot;444&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;444&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;블랙리스트 or 키 회전(Key Rotation)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;전략&lt;/li&gt;
&lt;li&gt;&lt;b&gt;짧은 액세스 토큰 수명&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;+&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;안전한 저장소&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(예: HttpOnly Cookie)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;jwt-적용-방식-http-request&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;JWT 적용 방식 HTTP request&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Stateless 인증&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션 대신 HTTP 헤더&lt;span&gt;&amp;nbsp;&lt;/span&gt;Authorization: Bearer {token}&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용&lt;/li&gt;
&lt;li&gt;서버가 클레임(Claim) 기반으로 토큰 서명 확인 &amp;rarr; 사용자 정보(Subject, 권한) 복원&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커스텀 필터&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(OncePerRequestFilter&lt;span&gt;&amp;nbsp;&lt;/span&gt;상속)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청마다 JWT 추출 &amp;rarr; 유효성 검사 &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Authentication&lt;span&gt;&amp;nbsp;&lt;/span&gt;생성 &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;SecurityContextHolder저장&lt;/li&gt;
&lt;li&gt;헤더에서 JWT 추출&lt;/li&gt;
&lt;li&gt;서명&amp;middot;만료일 검사&lt;/li&gt;
&lt;li&gt;토큰에서 사용자 정보 파싱 &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;UsernamePasswordAuthenticationToken&lt;span&gt;&amp;nbsp;&lt;/span&gt;생성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;휴대폰 QR 체크인&amp;rdquo;&lt;/b&gt;&lt;br /&gt;QR 체크인 과정 JWT 인증 흐름&lt;br /&gt;1. QR 코드 제시&lt;br /&gt;&amp;ldquo;한 번만 앱을 켜고 QR 보여주기&amp;rdquo;&lt;br /&gt;클라이언트가 Authorization: Bearer {JWT} 헤더에 토큰 담아 요청&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;직원이 스캔하여 확인&lt;br /&gt;&amp;ldquo;QR 스캔 &amp;rarr; 유효 기간&amp;middot;위조 검사&amp;rarr; OK OncePerRequestFilter가 토큰 추출 &amp;rarr; 서명&amp;middot;만료일 검증 &amp;rarr; Authentication 생성&lt;/li&gt;
&lt;li&gt;그 뒤로는 QR만 보여주면 통과&amp;ldquo;매번 신분증 없이 QR로만 입장 OK&amp;rdquo; 매 요청마다 토큰 검증 &amp;rarr; 상태 비저장(Stateless) 인증 유지&lt;/li&gt;
&lt;li&gt;QR 잃어버리거나 훼손 시 입장 불가&lt;br /&gt;&amp;ldquo;유효 기간 지난 QR 혹은 변조된 QR은 차단&amp;rdquo; 만료된 토큰(exp) 또는 서명 불일치 시 인증 실패(403)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;-새롭게-알게-된-점--어려웠던-부분&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  새롭게 알게 된 점 &amp;amp; 어려웠던 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보안과 관련해 처음 배우는 내용이라, 이해하는데 시간이 꽤 걸림.&lt;/li&gt;
&lt;li&gt;배울 범위가 방대해 천천히 개념을 잡아가야 할듯..&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-과제실습-결과&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  과제/실습 결과&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인증, 인가 실습 -&amp;gt; 스프링 시큐리티&lt;/b&gt;&lt;br /&gt;UserDetails 클래스&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Service
// 스프링 시큐리티에서 사용자 정보를 가져오는 인터페이스
public class UserDetailService implements UserDetailsService {

    private final UserRepository userRepository;

    // 사용자 이름(email)을 통해 사용자 정보를 가져오는 메서드
    @Override
    public User loadUserByUsername(String email) {
        return userRepository.findByEmail(email)
                .orElseThrow(() -&amp;gt; new IllegalArgumentException((email)));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
    private final UserDetailService userService;
    // 스프링 시큐리티 비활성화 (모든 기능 사용하지 못하게)
    // 일반적으로 정적 리소스에 인증, 인가 사용
    @Bean
    public WebSecurityCustomizer configure() {
        return (web) -&amp;gt; web.ignoring()
                .requestMatchers(toH2Console())
                .requestMatchers(new AntPathRequestMatcher(&quot;/static/**&quot;));
    }
    // 특정 HTTP 요청에 대한 보안 설정
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(auth -&amp;gt; auth // 인증, 인가 설정
                        .requestMatchers( // 특정 요청과 일치하는 url 액세스
                                new AntPathRequestMatcher(&quot;/login&quot;),
                                new AntPathRequestMatcher(&quot;/signup&quot;),
                                new AntPathRequestMatcher(&quot;/user&quot;)
                        ).permitAll() // 모든 사용자에게 허용(로그인, 회원가입은 인증 필요 없음)
                        .anyRequest().authenticated())
                //anyRequest()는 모든 요청을 의미하며,
                // authenticated()는 인증된 사용자만 접근할 수 있도록 설정
                .formLogin(formLogin -&amp;gt; formLogin // 폼 기반 로그인
                        .loginPage(&quot;/login&quot;) // 로그인 페이지 경로
                        .defaultSuccessUrl(&quot;/articles&quot;) // 로그인 성공 시 이동할 경로
                )
                .logout(logout -&amp;gt; logout // 로그아웃 설정
                        .logoutSuccessUrl(&quot;/login&quot;) // 로그아웃 성공 시 이동할 경로
                        .invalidateHttpSession(true) // 세션 무효화
                )
                .csrf(AbstractHttpConfigurer::disable) // CSRF 비활성화 (실습때문에 비활성화)
                .build();
    }
    // 인증 매니저 설정
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder, UserDetailService userDetailService) throws Exception {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userService); // 사용자 정보 서비스 설정
        authProvider.setPasswordEncoder(bCryptPasswordEncoder);
        return new ProviderManager(authProvider);
    }
    // 패스워드 인코더로 사용할 빈 설정
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Service
public class UserService {
    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    public Long save(AddUserRequest dto) {
        return userRepository.save(User.builder()
                .email(dto.getEmail())
                // 비밀번호 암호화
                .password(bCryptPasswordEncoder.encode(dto.getPassword()))
                .build()).getId();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;# Spring JPA
spring.application.name=springboot-developer
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.defer-datasource-initialization=true

# datasource
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=

# Hibernate DDL
spring.jpa.hibernate.ddl-auto=update

# H2
spring.h2.console.enabled=true&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6EvbU/dJMcabRIV6a/wh5tegEjRMK5Xh0WpJycd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6EvbU/dJMcabRIV6a/wh5tegEjRMK5Xh0WpJycd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6EvbU/dJMcabRIV6a/wh5tegEjRMK5Xh0WpJycd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6EvbU%2FdJMcabRIV6a%2Fwh5tegEjRMK5Xh0WpJycd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1114&quot; height=&quot;176&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JWT 실습&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토큰 생성&lt;/li&gt;
&lt;li&gt;올바른 토큰인지 유효성 검사&lt;/li&gt;
&lt;li&gt;토큰에서 필요한 정보 가져오기&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Service
public class TokenProvider {

    private final JwtProperties jwtProperties;
    // 사용자 정보와 만료 기간을 입력받아 JWT를 생성합니다.
    // 만료 시간을 계산한 뒤, makeToken 메서드를 호출합니다.
    public String generateToken(User user, Duration expiredAt) {
        Date now = new Date();
        return makeToken(new Date(now.getTime() + expiredAt.toMillis()), user);
    }
    // JWT 토큰을 생성하는 메서드
    // JWT의 헤더, 페이로드, 서명을 설정합니다.
    // 설정된 정보를 기반으로 JWT 문자열을 생성
    private String makeToken(Date expiry, User user) {
        Date now = new Date();

        return Jwts.builder()
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 헤더 JWT 타입 설정
                // 내용 iss: ajufresh@gmail.com (properties에서 가져옴)
                .setIssuer(jwtProperties.getIssuer())
                .setIssuedAt(now) // 내용 iat : 발급 시간
                .setExpiration(expiry) // 내용 exp : expiry 멤버 변숫값
                .setSubject(user.getEmail()) // 내용 sub : 사용자 이메일
                .claim(&quot;id&quot;, user.getId()) // 클레임 id : 사용자 id
                //서명: 비밀값과 함께 해시값을 H256 알고리즘으로 암호화
                .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
                .compact();
    }
    // JWT 토큰을 유효성 검증하는 메서드
    public boolean validToken(String token) {
        try {
            Jwts.parser()
                    .setSigningKey(jwtProperties.getSecretKey()) //비밀값으로 복호화
                    // Properties에서 가져온 비밀값으로 복호화
                    .parseClaimsJws(token);

            return true;
        } catch (Exception e) {// 복호화 과정에서 에러가 나면 유효하지않은 토큰
            return false;
        }
    }

    // JWT 토큰에서 사용자 인증 정보를 가져오는 메서드
    // 토큰을 받아 인증정보를 담은 Authentication 객체를 생성
    public Authentication getAuthentication(String token) {
        // 1. 토큰 복호화 및 클레임 추출
        Claims claims = getClaims(token); // JWT 토큰을 복호화하고, 클레임(Claims) 정보를 가져옵니다.
        //클레임은 JWT의 페이로드에 포함된 데이터로, 여기서는 사용자 이메일(sub)과 기타 정보를 포함

        // 2. 권한 설정
        Set&amp;lt;SimpleGrantedAuthority&amp;gt; authorities = Collections.singleton(new SimpleGrantedAuthority(&quot;ROLE_USER&quot;));
        // Set&amp;lt;SimpleGrantedAuthority&amp;gt;를 사용하여 사용자 권한을 설정합니다. (ROLE_USER라는 권한을 부여)

        // 3. Authentication 객체 생성
        return new UsernamePasswordAuthenticationToken(new org.springframework.security.core.userdetails.User(claims.getSubject
                (), &quot;&quot;, authorities), token, authorities);
        //UsernamePasswordAuthenticationToken 객체를 생성하여 반환
        //이 객체는 스프링 시큐리티에서 인증 정보를 담는 데 사용됩니다.
        //첫 번째 인자로 들어가는 User는 스프링 시큐리티에서 제공하는 org.springframework.security.core.userdetails.User 클래스를 사용해야 합니다
    }

    // JWT 토큰에서 사용자 ID를 가져오는 메서드
    public Long getUserId(String token) {
        Claims claims = getClaims(token); // JWT 토큰을 복호화하고, 클레임(Claims) 정보를 가져옵니다.
        // 클레임 정보 반환 -&amp;gt; 클레임에서 id 값 추출
        return claims.get(&quot;id&quot;, Long.class);
    }

    private Claims getClaims(String token) {
        return Jwts.parser() // 클레임 조회
                .setSigningKey(jwtProperties.getSecretKey())
                .parseClaimsJws(token)
                .getBody();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Setter
@Getter
@Component
@ConfigurationProperties(&quot;jwt&quot;)
public class JwtProperties {
    private String issuer;
    private String secretKey;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;-스터디-피드백&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  스터디 피드백&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Q. Refresh Token도 탈취될 경우, Access Token의 보완책으로 나온 의미가 없는데, 이를 어떻게 처리하면 되는가?&lt;br /&gt;A. Refresh Token의 유효기간을 짧게 설정하고, 2중 3중으로 암호화를 하는 방법이 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Q. Access/Refresh Token 전략 설계 -&amp;gt; Access Token만 사용할때?&lt;br /&gt;A. 짧은 유효기간 (15분~1시간)으로 Access Token을 설정하기.&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Q. 토큰 저장 위치는 어디로 할까? (쿠키 vs 로컬스토리지)&lt;br /&gt;A. 쿠키 -&amp;gt; XSS 공격 방지, 자동으로 쿠키가 전송되어 편리&lt;br /&gt;로컬스토리지 -&amp;gt; XSS 취약, 자동 전송되지 않으므로 CSRF 공격에 안전, 토큰을 수동으로 추가&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보안측면에선 쿠키를 사용하는것이 더 안전.&lt;/p&gt;</description>
      <author>myhousemouse</author>
      <guid isPermaLink="true">https://myhousemouse.tistory.com/7</guid>
      <comments>https://myhousemouse.tistory.com/7#entry7comment</comments>
      <pubDate>Sun, 17 May 2026 21:32:17 +0900</pubDate>
    </item>
    <item>
      <title>큐시즘 - SEMOSAN 밋업 프로젝트 중간 회고 | PM</title>
      <link>https://myhousemouse.tistory.com/6</link>
      <description>&lt;h1&gt;세모산 밋업 프로젝트 중간 회고&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 세모산 밋업 프로젝트를 중간까지 해오면서, PM인 내가 어떤 고민을 했고 무엇을 정리해왔는지 조금 편하게 회고처럼 남겨보려고 한다. 완성된 프로젝트를 돌아보는 회고라기보다는, 중간 시점에서 우리가 어디까지 왔는지, 지금 어떤 걸 배우고 있는지 정리하는 기록에 더 가깝다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;먼저 느낀 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반의 세모산은 아이디어 자체는 분명히 매력적이었는데, PM 입장에서는 그 아이디어를 실제 서비스 흐름으로 정리하는 일이 가장 먼저 필요하다고 느꼈다. 누구를 위한 서비스인지, 첫 화면에서 사용자가 뭘 이해해야 하는지, 어떤 기능부터 MVP로 잡아야 하는지가 한 번에 또렷하게 보이지는 않았기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 세모산이 초보자용 추천 서비스인지, 등산 기록 서비스인지, 커뮤니티형 서비스인지가 동시에 보이는 순간들이 있었고, 그래서 처음에는 기능이 많아 보이는데도 핵심 메시지가 조금 퍼져 있는 느낌이 있었다. 그걸 정리하는 게 중간까지 내 역할의 핵심이었다고 생각한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;중간까지 PM으로서 가장 많이 한 일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 중간 시점까지 가장 많이 한 일은 새로운 기능을 마구 추가하는 게 아니라, 세모산의 중심 흐름을 계속 정리하고 문서화하는 일이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 우선 세모산의 1차 핵심 흐름을 아래처럼 다시 잡았다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;온보딩&lt;/li&gt;
&lt;li&gt;코스 탐색 / 등산 시작&lt;/li&gt;
&lt;li&gt;실시간 도우미&lt;/li&gt;
&lt;li&gt;기록 아카이빙&lt;/li&gt;
&lt;li&gt;기록 공유 또는 커뮤니티 연결&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름이 잡히고 나서야 요구사항 정의서, 기능명세서, 요구사항-기능 매핑표, 운영정책, 심사용 테스트 시나리오 같은 문서들이 하나의 방향으로 연결되기 시작했다. 개인적으로는 이게 중간 시점까지 가장 큰 변화였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;작업하면서 남겨둔 시안들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 이미지는 중간 작업 과정에서 정리한 세모산 홍보물 시안이다. 단순 디자인 결과물이라기보다, 세모산이 어떤 메시지로 보이길 원하는지 정리해가는 과정이기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;중간까지 오면서 잘했다고 느낀 점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 서비스 방향을 계속 초보자 친화적으로 붙잡고 있었던 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세모산은 결국 등산을 어렵게 느끼는 사람도 시작할 수 있게 만들어주는 서비스여야 한다고 생각했다. 그래서 문서를 쓸 때도 전문 용어보다 생활 언어, 권한 요청 전에 이유 설명, 로그인 없이도 코스 탐색 가능, 추천 결과에 이유를 붙이는 구조 같은 원칙을 계속 놓치지 않으려고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 나중에 디자인이나 홍보 메시지를 정할 때도 꽤 중요한 기준이 됐다. 서비스가 똑똑해 보이는 것보다, 사용자가 &quot;아, 나도 써볼 수 있겠다&quot;고 느끼는 게 더 중요하다고 봤다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 기능 나열보다 사용자 흐름 중심으로 문서를 정리한 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초반에는 기능 하나하나를 보면 다 중요해 보였는데, 중간쯤 오니까 중요한 건 기능의 개수가 아니라 사용자가 어디서 들어와서 어디까지 가는지를 명확히 보여주는 거라는 걸 더 분명히 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 온보딩, 코스 추천, 기록 저장, 커뮤니티, 심사 대응을 따로따로 보지 않고, 하나의 서비스 경험 안에서 이어지도록 정리하려고 했다. 이 흐름이 잡히면서 팀 안에서 대화할 때도 기준이 조금 더 선명해졌다고 느꼈다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 운영과 심사까지 기획 범위 안으로 끌어온 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획하다 보면 보통 정책 문서나 신고/차단, 테스트 계정, 심사 흐름은 뒤로 밀리기 쉽다. 그런데 세모산은 기록 공유와 커뮤니티 요소가 있는 서비스라서, 이런 부분을 나중으로 미루면 결국 마지막에 크게 흔들릴 수밖에 없다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 개인정보처리방침, 이용약관, 운영정책, 앱 심사용 테스트 시나리오까지 기획 단계 안에서 먼저 다뤄본 건 PM 입장에서 꽤 의미 있었다. 적어도 지금은 출시 직전에 허둥대는 대신, 어떤 게 아직 비어 있는지는 알고 있는 상태가 됐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아쉬웠던 점도 분명히 있었다&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 초반에 MVP 경계가 조금 더 빨리 정리됐으면 좋았겠다는 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세모산 안에는 좋은 아이디어가 정말 많았는데, 그만큼 PM 입장에서는 &quot;지금 꼭 필요한 것&quot;과 &quot;나중에 해도 되는 것&quot;을 빨리 나눠야 했다. 그걸 중간 시점에 와서야 조금 더 단단하게 정리한 게 아쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 더 빠르게 핵심 가치, 후순위 기능, 출시 기준을 나눴다면 문서 작업과 논의가 더 효율적이었을 것 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 세부 기능 상상이 전체 흐름보다 앞선 순간들이 있었던 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온보딩 질문을 어떻게 할지, 커뮤니티 타입을 몇 개로 나눌지, 공동 기록은 어떻게 만들지 같은 세부 기능 고민은 늘 재미있다. 그런데 그런 고민을 하다 보면 전체 서비스 흐름과 연결이 약해질 때가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간 이후에는 문서 구조를 통해 어느 정도 잡아갔지만, 초반부터 같은 기준으로 밀고 갔으면 더 좋았을 것 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 역할 분담과 오너십을 더 빨리 선명하게 만들 필요가 있었던 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회의 기록을 다시 보면 초반에는 PM 내부 역할 분담이나 준비물 오너십이 생각보다 느슨하게 남아 있었다. 누가 어떤 문서를 책임지고, 정책 문서는 누가 쓰고, 심사용 데이터는 누가 챙기는지 조금 더 빨리 선명하게 만들었으면 좋았겠다는 생각이 든다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번 프로젝트를 하면서 배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 세모산 프로젝트를 하면서 다시 느낀 건, 좋은 아이디어와 좋은 기획 문서는 전혀 다른 일이라는 점이었다. 서비스 방향을 말하는 것만으로는 부족하고, 디자이너와 개발자, 운영 관점이 같은 그림을 볼 수 있게 정리해야 비로소 실행이 가능해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 사용자 경험을 설계할 때는 기능을 설명하는 것보다 다음 행동으로 자연스럽게 이어지게 만드는 것이 훨씬 중요하다는 것도 많이 느꼈다. 세모산도 결국 사용자가 앱을 처음 켰을 때 이해할 수 있어야 하고, 첫 산을 찾고, 기록을 남기고, 다시 돌아오게 만들어야 한다. 그 흐름을 만드는 게 PM 역할이라는 걸 중간 시점에서 더 크게 체감하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나는, 출시형 프로젝트에서는 정책과 운영이 절대 부가 업무가 아니라는 점이다. 기록 공개, 신고, 차단, 계정 탈퇴, 심사용 테스트 시나리오는 나중에 붙이는 옵션이 아니라 처음부터 서비스 설계 안에 같이 들어가 있어야 한다는 걸 다시 배웠다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;남은 기간 동안 가장 중요하다고 생각하는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 이후로는 기능을 계속 넓히기보다, 이미 정리한 흐름을 더 단단하게 만드는 게 중요하다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 기준에서 남은 핵심 우선순위는 이렇다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;온보딩 질문과 체력/경험 기반 추천 로직을 더 명확히 정리하기&lt;/li&gt;
&lt;li&gt;커뮤니티 MVP 범위를 후기/질문/인증 중심으로 단단하게 고정하기&lt;/li&gt;
&lt;li&gt;로그인 전/후 접근 범위와 권한 거절 대체 흐름을 더 선명하게 정리하기&lt;/li&gt;
&lt;li&gt;앱 심사 대응 문서와 테스트 데이터 준비 책임을 구체화하기&lt;/li&gt;
&lt;li&gt;문서 간 용어와 흐름을 통일해서 팀 전체가 같은 기준으로 이야기할 수 있게 만들기&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개인적으로 남기고 싶은 한 줄&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간 시점까지의 세모산은 아직 완성이라기보다 정리의 단계에 더 가깝다. 그래도 처음보다 훨씬 더 누구를 위한 서비스인지, 무엇을 먼저 구현해야 하는지, 어떤 기준으로 출시를 준비해야 하는지는 분명해졌다. 남은 기간에는 이 방향성을 흔들지 않고, 진짜 사용 흐름과 출시 준비까지 연결하는 PM 역할에 더 집중하고 싶다.&lt;/p&gt;</description>
      <category>it학회</category>
      <category>kusitms</category>
      <category>대외학회</category>
      <category>밋업프로젝트</category>
      <category>큐시즘</category>
      <category>큐시즘학술</category>
      <author>myhousemouse</author>
      <guid isPermaLink="true">https://myhousemouse.tistory.com/6</guid>
      <comments>https://myhousemouse.tistory.com/6#entry6comment</comments>
      <pubDate>Sun, 10 May 2026 22:38:10 +0900</pubDate>
    </item>
    <item>
      <title>백엔드 스터디 후기</title>
      <link>https://myhousemouse.tistory.com/5</link>
      <description>&lt;h3 id=&quot;-기본-정보&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  기본 정보&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;주차/주제&lt;/b&gt;: 6주차 - 비즈니스 로직 구현과 서비스 레이어&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-학습-내용-요약&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  학습 내용 요약&lt;/h3&gt;
&lt;h3 id=&quot;1-service-레이어의-역할과-책임-조사하기&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. Service 레이어의 역할과 책임 조사하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Layer는&amp;nbsp;관심사의 집합&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;663&quot; data-origin-height=&quot;451&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d8hbSt/dJMcaib7gHw/eJeiBuaaZgJX27wYp02ATK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d8hbSt/dJMcaib7gHw/eJeiBuaaZgJX27wYp02ATK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d8hbSt/dJMcaib7gHw/eJeiBuaaZgJX27wYp02ATK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd8hbSt%2FdJMcaib7gHw%2FeJeiBuaaZgJX27wYp02ATK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;663&quot; height=&quot;451&quot; data-origin-width=&quot;663&quot; data-origin-height=&quot;451&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Presentation Layer (프레젠테이션 계층)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자(UI) 요청 받기: 웹브라우저&amp;middot;앱&amp;middot;윈도우 애플리케이션 등 어디서든 들어오는 요청을 처리&lt;/li&gt;
&lt;li&gt;응답(View) 반환: 처리 결과를 HTML 페이지나 JSON 등으로 포장해 사용자에게 돌려줌&lt;/li&gt;
&lt;li&gt;입력 검증&amp;middot;예외 처리: URL 잘못 호출, 필수 파라미터 누락 같은 &amp;ldquo;화면 관점&amp;rdquo; 오류를 잡아서 사용자에게 친절한 메시지 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Service Layer (서비스 계층)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;순수 비즈니스 로직 구현&lt;/li&gt;
&lt;li&gt;트랜잭션 경계 관리 (@Transactional)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모두 성공시키거나, 모두 취소(rollback)&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;461&quot; data-origin-height=&quot;265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MUbYv/dJMcaib7gHt/t2WYKf6SgxSTwT02kSfHuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MUbYv/dJMcaib7gHt/t2WYKf6SgxSTwT02kSfHuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MUbYv/dJMcaib7gHt/t2WYKf6SgxSTwT02kSfHuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMUbYv%2FdJMcaib7gHt%2Ft2WYKf6SgxSTwT02kSfHuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;461&quot; height=&quot;265&quot; data-origin-width=&quot;461&quot; data-origin-height=&quot;265&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;계층 간 조율(Orchestration)역할 구분예시성격
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;b&gt;비즈니스 로직&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;이메일 중복 체크, 배송 상태에 따른 취소 여부 판단&lt;/td&gt;
&lt;td&gt;도메인 고유의 핵심 규칙&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;b&gt;트랜잭션 관리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;@Transactional&lt;/td&gt;
&lt;td&gt;데이터 일관성 보장 (기술적 처리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;b&gt;DTO 변환&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;PostRequestDto &amp;rarr; Post 엔티티&lt;/td&gt;
&lt;td&gt;계층 간 데이터 이동 구조화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;b&gt;검증(Validation)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;비밀번호 길이 검사&lt;/td&gt;
&lt;td&gt;입력 무결성(일부는 비즈니스 로직)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;b&gt;캐싱&amp;middot;보안&amp;middot;로깅&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;@Cacheable, @PreAuthorize, AOP 로깅&lt;/td&gt;
&lt;td&gt;애플리케이션 전반 공통 기능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;b&gt;외부 연동&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;메일&amp;middot;SMS&amp;middot;결제 API 호출&lt;/td&gt;
&lt;td&gt;외부 시스템 통합&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Repository Layer (저장소 계층)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스 CRUD 전담&lt;hr data-ke-style=&quot;style1&quot; /&gt;Service 계층과 Controller 계층의 책임 분리 모호함.&lt;b&gt;&amp;rArr; 재사용성과 유지보수 때문&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;ex) 새로 Grapqhql를 도입 하고자 할 때 이미 Controller에 모든 기능을 구현하였다면, Grapqhql API를 구현하는&amp;nbsp;Controller에 또 다시 구현해야함.&lt;br /&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cUugvk/dJMcab46bqR/AAAAAAAAAAAAAAAAAAAAAM9aS0Hj4KDB8Olst4g8f-ccyX0CC7PS1qhfMoIhwVwN/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1777561199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=%2BmCebx2%2Fo5TzC4KunI3FgCuBrgY%3D&quot; /&gt;&lt;br /&gt;- GqlController를 새로 만들어 Service에 연결&lt;/li&gt;
&lt;li&gt;왜 이렇게 분리할까?&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;어디에 어떤 코드를 두면 좋을까?&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;에 도움되는 아키텍처 패턴&lt;/p&gt;
&lt;h3 id=&quot;서비스-레이어-유틸화service-layer-utilization&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;서비스 레이어 유틸화(Service Layer Utilization)&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A서비스가 B, C, D&amp;hellip; 여러 도메인을 다 써야 하는 상황
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A서비스 클래스 안에&lt;span&gt;&amp;nbsp;&lt;/span&gt;BService,&lt;span&gt;&amp;nbsp;&lt;/span&gt;BRepository,&lt;span&gt;&amp;nbsp;&lt;/span&gt;CService,&lt;span&gt;&amp;nbsp;&lt;/span&gt;CRepository&amp;hellip; 14개 필드가 주입된 상황&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;필요한 Service Layer의 메서드를 정적(static) 메서드로 &amp;rarr; A서비스에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;UtilityClass&lt;span&gt;&amp;nbsp;&lt;/span&gt;하나만 참조 하면 됨&lt;/li&gt;
&lt;li&gt;만들어야 하는 Mock 객체 줄어듬&lt;/li&gt;
&lt;li&gt;유연하게 대처 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ex) A서비스 = &amp;ldquo;&amp;lsquo;책상&amp;rsquo;, &amp;lsquo;의자&amp;rsquo;, &amp;lsquo;컴퓨터&amp;rsquo;, &amp;lsquo;프린터&amp;rsquo; &amp;hellip; 14가지&amp;rdquo; 다 갖고 일해야 하는 사람&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;A서비스 = &amp;ldquo;사무도우미(UtilityClass) 한 명&amp;rdquo;만 옆에 두고 필요한 일은 그 사람이 다 해줘서&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;A서비스는 &amp;ldquo;프린터 하나만 딱 쓰면&amp;rdquo; 되는 상황&lt;/p&gt;
&lt;h3 id=&quot;cqrs-command-and-query-responsibility-segregation&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;CQRS&lt;/b&gt;&amp;nbsp;(Command and Query Responsibility Segregation)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;명령(Command, 쓰기)&amp;rdquo; 과 &amp;ldquo;조회(Query, 읽기)&amp;rdquo; 를 완전히 분리해서 다루는 아키텍처 패턴&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Command : 객체를 변경하는 쓰기 (읽기는 빠른 조회)&lt;/li&gt;
&lt;li&gt;Query : 데이터 조회 기능만을 가진 함수 (쓰기는 정합성, 복잡한 비즈니스 규칙)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;전통적인 CRUD&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;jboss-cli&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;POST /orders   &amp;rarr; 주문 생성  
GET  /orders   &amp;rarr; 주문 목록 조회  
PUT  /orders/1 &amp;rarr; 주문 수정  &lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CQRS 적용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쓰기(Commands)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST /orders&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;OrderWriteService.createOrder(...)&lt;/li&gt;
&lt;li&gt;내부에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;order_write_table&lt;span&gt;&amp;nbsp;&lt;/span&gt;에 INSERT&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;읽기(Queries)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET /orders&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;OrderReadService.findAllOrders()&lt;/li&gt;
&lt;li&gt;order_read_table&lt;span&gt;&amp;nbsp;&lt;/span&gt;또는 캐시/검색엔진에서 SELECT&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Service 레이어 내부를 쓰기/읽기용 두 클래스로 나눈 예제&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;// Command 서비스 (트랜잭션, 검증)
@Service
public class OrderWriteService {
    @Transactional
    public Long createOrder(CreateOrderCommand cmd) {
        // 검증, 엔티티 생성, 저장
    }
}

// Query 서비스 (빠른 조회)
@Service
public class OrderReadService {
    public List&amp;lt;OrderViewDto&amp;gt; findAllOrders() {
        // 최적화된 뷰 테이블/캐시에서 가져오기
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;명령(Command) DTO 와 조회(Query) DTO 를 완전히 분리한 예제&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;// Command DTO
public record CreatePostCmd(String title, String content) { }

// Query DTO
public record PostOverview(Long id, String title, LocalDateTime createdAt) { }

// 쓰기 컨트롤러
@PostMapping
public long create(@RequestBody CreatePostCmd cmd) {
  return postCommandService.create(cmd);
}

// 조회 컨트롤러
@GetMapping(&quot;/overview&quot;)
public List&amp;lt;PostOverview&amp;gt; overview() {
  return postQueryService.listOverview();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ex)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Command 창구&lt;/b&gt;: &amp;ldquo;주문 받기&amp;rdquo; 전담, 메뉴 옵션 검증&amp;middot;주문표 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Query 창구&lt;/b&gt;: &amp;ldquo;주문 현황 조회&amp;rdquo; 전담, 손님에게 빠르게 현재 대기&amp;middot;조리 상태 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-dto를-활용한-데이터-전달-패턴-예시-정리하기&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. DTO를 활용한 데이터 전달 패턴 예시 정리하기&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DTO / DAO / VO 차이점&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DTO(Data Transfer Object)&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;532&quot; data-origin-height=&quot;163&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Rnxqe/dJMcab46bqQ/2nuHFduw4MkjuUyeIlwt0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Rnxqe/dJMcab46bqQ/2nuHFduw4MkjuUyeIlwt0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Rnxqe/dJMcab46bqQ/2nuHFduw4MkjuUyeIlwt0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRnxqe%2FdJMcab46bqQ%2F2nuHFduw4MkjuUyeIlwt0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;532&quot; height=&quot;163&quot; data-origin-width=&quot;532&quot; data-origin-height=&quot;163&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DAO(Data Access Object)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB에 접근하는 역할&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uw8GN/dJMcag6pOfT/6Gd8J2Pg01geUuEsefrRkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uw8GN/dJMcag6pOfT/6Gd8J2Pg01geUuEsefrRkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uw8GN/dJMcag6pOfT/6Gd8J2Pg01geUuEsefrRkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuw8GN%2FdJMcag6pOfT%2F6Gd8J2Pg01geUuEsefrRkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;376&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;376&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;VO(Value Object)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변경 불가능한 객체&lt;/li&gt;
&lt;li&gt;일반적으로 날짜, 통화 또는 수량을 나타내는 데 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;엔티티와 DTO 차이&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DTO(Data Transfer Object) : 클라이언트와 서버 간 데이터 전송을 전송을 위한 객체&lt;/li&gt;
&lt;li&gt;Entity : 데이터베이스에 저장되는 데이터 객체로, 데이터베이스와 직접적으로 매핑되는 객체&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Request ⇄ Response DTO 분리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Request DTO : 클라이언트가 보내는(요청)&lt;/li&gt;
&lt;li&gt;Response DTO : 서버가 돌려주는(응답)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;// 글 작성 요청 DTO
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class AddArticleRequest {
    private String title;
    private String content;
    public Article toEntity() {
        return Article.builder()
                .title(title)
                .content(content)
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;// 글 작성 응답 DTO
@Getter
public class ArticleResponse {

    private final String title;
    private final String content;

    public ArticleResponse(Article article) {
        this.title = article.getTitle();
        this.content = article.getContent();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Mapper/Assembler 분리&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Entity &amp;harr; DTO 변환 로직을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;한곳&lt;/b&gt;에 모아 재사용&amp;middot;테스트 용이&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Component
public class PostMapper {
  public Post toEntity(AddPostRequest req) {
    return Post.builder()
               .title(req.getTitle())
               .content(req.getContent())
               .build();
  }
  public PostDto toDto(Post post) {
    return new PostDto(post.getId(), post.getTitle(), post.getContent());
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;컬렉션 DTO&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;글 목록 + 전체 개수&amp;middot;페이지 정보 같은 메타데이터를 함께 전달&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Getter
public class ArticleListViewResponse {
    private final Long id;
    private final String title;
    private final String content;

    public ArticleListViewResponse(Article article) {
        this.id = article.getId();
        this.title = article.getTitle();
        this.content = article.getContent();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중첩(Nested) DTO&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;글과 그에 달린 댓글을 한 번에 내려줄 때&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;화면(View) 전용 DTO (ViewModel)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Thymeleaf 같은 템플릿에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;딱 필요한&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;데이터만 전달&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@Controller //컨트롤러 표시 -&amp;gt; 컴포넌트 스캔을 통해 빈으로 등록
public class ExampleController {
    //컨트롤러 클래스 선언
    @GetMapping(&quot;/thymeleaf/example&quot;) // 이 URL로 GET 요청이 오면, thymeleafExample() 호출
    public String thymeleafExample(Model model) { //Model 객체를 통해 데이터를 전달 (뷰로 전달)
        Person examplePerson = new Person(); // 컨트롤러 내부 DTO 역할을 할 Person 객체 생성
        examplePerson.setId(1L);
        examplePerson.setName(&quot;홍길동&quot;);
        examplePerson.setAge(11);
        examplePerson.setHobbies(List.of(&quot;운동&quot;, &quot;독서&quot;));

        model.addAttribute(&quot;person&quot;, examplePerson); // 뷰에 Person 객체 전달
        model.addAttribute(&quot;today&quot;, LocalDate.now()); // today key로 날짜를 뷰에 전달

        return &quot;example&quot;; // example.html로 이동
        // src/main/resources/templates/example.html)을 찾아 렌더링
    }

    @Setter
    @Getter
    class Person { // Person 클래스 정의 == 내부 DTO 역할
        private Long id;
        private String name;
        private int age;
        private List&amp;lt;String&amp;gt; hobbies;
        // 단순 데이터 전달용 -&amp;gt; 엔티티 사용 X
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Projection 기반 DTO (JPA 인터페이스 프로젝션)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DB 조회 시&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;특정 컬럼만&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;바로 뽑아와 SQL/PERFORMANCE 최적화&lt;/p&gt;
&lt;h3 id=&quot;3-간단한-비즈니스-로직-구상해오기-ex-회원가입-게시글-작성&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3. 간단한 비즈니스 로직 구상해오기 (ex: 회원가입, 게시글 작성)&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;도서 대출 기능&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 대출을 원하는 도서명을 입력한다.&lt;/li&gt;
&lt;li&gt;입력된 도서명으로 데이터베이스에서 해당 도서명을 가진 모든 책을 검색한다.&lt;/li&gt;
&lt;li&gt;검색된 책 목록을 사용자에게 보여준다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 책의 ID, 제목, 대출 가능 여부를 포함한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사용자가 대출을 원하는 책의 ID를 선택한다.&lt;/li&gt;
&lt;li&gt;선택된 책의 대출 가능 여부를 확인한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대출 가능 여부가&lt;span&gt;&amp;nbsp;&lt;/span&gt;false인 경우, 대출 불가능하다는 메시지를 출력한다.&lt;/li&gt;
&lt;li&gt;대출 가능 여부가&lt;span&gt;&amp;nbsp;&lt;/span&gt;true인 경우:&lt;br /&gt;a. 해당 책의 대출 상태를&lt;span&gt;&amp;nbsp;&lt;/span&gt;false로 업데이트한다.&lt;br /&gt;b. 데이터베이스에 변경 사항을 저장한다.&lt;br /&gt;c. 대출 성공 메시지를 출력한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;-새롭게-알게-된-점--어려웠던-부분&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  새롭게 알게 된 점 &amp;amp; 어려웠던 부분&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;✔️ DTO의 개념을 다시한번 복습하게 됨&lt;br /&gt;✔️ 데이터 전달 패턴이 다양해서, 이해하는데 시간이 걸렸음&lt;/p&gt;
&lt;h3 id=&quot;-과제실습-결과-있을-시&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  과제/실습 결과 (있을 시)&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;도서 대출 로직&lt;/p&gt;
&lt;pre class=&quot;livescript&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import me.nam.springbootdeveloper.Domain.Book;
import me.nam.springbootdeveloper.Repository.BookRepository;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class BookService {

    private final BookRepository bookRepository;

    public List&amp;lt;BookResponse&amp;gt; searchBooksByTitle(String title) {
        List&amp;lt;Book&amp;gt; books = bookRepository.findAllByTitle(title);
        return books.stream()
                .map(BookResponse::new)
                .toList();
    }

    @Transactional
    public void borrowingBook(Long id) {
        Book book = bookRepository.findById(id)
                .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;해당 ID의 책을 찾을 수 없습니다: &quot; + id));

        if (!book.isAvailable()) {
            throw new IllegalStateException(&quot;해당 책은 대출이 불가능합니다: &quot; + book.getTitle());
        }

        book.setAvailable(false);
        bookRepository.save(book);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;-스터디-피드백&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  스터디 피드백&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;✔️서비스 로직에 예외 처리를 두는 이유&lt;br /&gt;도메인 규칙(예: 중복 회원 검증)을 처리하는 역할&lt;br /&gt;컨트롤러에서 하면 안 되는 이유&lt;br /&gt;계층 간 역할이 모호,코드 재사용성이 떨어지고, 테스트가 어려워짐&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;✔️공통 에러 처리: 예외를 한 곳에서 처리&lt;br /&gt;@ControllerAdvice:모든 컨트롤러에서 발생하는 예외를 처리하는 클래스&lt;br /&gt;@ExceptionHandler: 특정 예외를 처리하는 메서드를 정의&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;✔️스파게티 코드 -&amp;gt; 리팩토링&lt;/p&gt;</description>
      <author>myhousemouse</author>
      <guid isPermaLink="true">https://myhousemouse.tistory.com/5</guid>
      <comments>https://myhousemouse.tistory.com/5#entry5comment</comments>
      <pubDate>Thu, 30 Apr 2026 11:01:35 +0900</pubDate>
    </item>
    <item>
      <title>백엔드 스터디 후기</title>
      <link>https://myhousemouse.tistory.com/4</link>
      <description>&lt;div style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot;&gt;
&lt;h3 id=&quot;-기본-정보&quot; data-ke-size=&quot;size23&quot;&gt;  기본 정보&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;주차/주제&lt;/b&gt;: 5주차 - 데이터베이스 연동 기초&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-학습-내용-요약&quot; data-ke-size=&quot;size23&quot;&gt;  학습 내용 요약&lt;/h3&gt;
&lt;h2 id=&quot;1-jpa와-hibernate의-관계-및-주요-개념&quot; data-ke-size=&quot;size26&quot;&gt;1.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;JPA와 Hibernate의 관계 및 주요 개념&lt;/b&gt;&lt;/h2&gt;
&lt;h3 id=&quot;jpajava-persistence-api&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;JPA&lt;/b&gt;(Java Persistence API)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바 객체와 관계형 데이터베이스를 연결하기 위한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;표준 인터페이스(API)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;주요 기능&lt;/b&gt;&lt;br /&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;엔티티 매핑&lt;/b&gt;: 자바 클래스 &amp;harr; DB 테이블&lt;br /&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;엔티티 관리&lt;/b&gt;: 객체 상태(비영속, 영속, 분리, 삭제) 추적 및 CRUD 자동 처리&lt;br /&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;트랜잭션 관리&lt;/b&gt;: 커밋&amp;middot;롤백 처리&lt;br /&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;쿼리 언어(JPQL)&lt;/b&gt;: 객체 지향적인 방식으로 데이터베이스에 쿼리를 날릴 수 있도록 해주는 JPQL(Java Persistence Query Language)&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OlPcC/dJMcad2JzVk/l1zXWavfD5USZIm3xSnWYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OlPcC/dJMcad2JzVk/l1zXWavfD5USZIm3xSnWYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OlPcC/dJMcad2JzVk/l1zXWavfD5USZIm3xSnWYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOlPcC%2FdJMcad2JzVk%2Fl1zXWavfD5USZIm3xSnWYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;270&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;b&gt;Hibernate&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA를 구현한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;대표적 ORM&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(Object-Relational Mapping)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;프레임워크&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;추가 기능&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;영속성 컨텍스트&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;1차 캐시&lt;/b&gt;: 조회 시 메모리 캐시 우선 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쓰기 지연(Write-Behind)&lt;/b&gt;: 트랜잭션 커밋 전에 SQL을 모아 한 번에 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;변경 감지(Dirty Checking)&lt;/b&gt;: 커밋 시점에 변경된 엔티티를 자동 반영&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지연 로딩(Lazy Loading)&lt;/b&gt;: 실제 사용할 때만 관련 데이터를 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자동 생성 SQL&lt;/b&gt;: 직접 SQL 작성 없이도 CRUD 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;img src=&quot;https://blog.kakaocdn.net/dna/bnbDFG/dJMcad2JzVi/AAAAAAAAAAAAAAAAAAAAABs8F7V-pX-rehYPuAj07M9jlJdDJX5PLL5sfTfhtvPQ/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1777561199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=M9W4gzm%2BonAtUkOCMSY0I3NDyqo%3D&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote style=&quot;background-color: #f8f9fa; color: #212529;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA는 &amp;ldquo;무엇을 할지&amp;rdquo; 규격만 정의하고, Hibernate는 &amp;ldquo;어떻게 할지&amp;rdquo; 실제 동작을 구현한 라이브러리&lt;/p&gt;
&lt;/blockquote&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;507&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ciw77/dJMcadVYLxD/N7sEkYDzAuClcIwRLW3fRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ciw77/dJMcadVYLxD/N7sEkYDzAuClcIwRLW3fRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ciw77/dJMcadVYLxD/N7sEkYDzAuClcIwRLW3fRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCiw77%2FdJMcadVYLxD%2FN7sEkYDzAuClcIwRLW3fRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;507&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;507&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h3 id=&quot;jpa-vs-hibernate-vs-spring-data-jpa&quot; data-ke-size=&quot;size23&quot;&gt;JPA vs Hibernate vs Spring Data JPA&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JPA (Java Persistence API)&lt;/b&gt;&lt;br /&gt;&amp;bull; 자바 객체와 관계형 DB를 연결하는 ORM 표준 인터페이스&lt;br /&gt;&amp;bull; SQL 지식 없이도 엔티티 객체를 통해 DB 조작 가능&lt;br /&gt;&amp;bull; 비즈니스 로직에 집중, DB 종속성 감소&lt;br /&gt;&amp;bull; 복잡&amp;middot;무거운 쿼리는 어려움&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Hibernate&lt;/b&gt;&lt;br /&gt;&amp;bull; JPA 인터페이스 대표 구현체&lt;br /&gt;&amp;bull; JDBC API 위에서 동작하는 자바용 ORM 프레임워크&lt;br /&gt;&amp;bull; 영속성 컨텍스트&lt;br /&gt;&amp;bull; 1차 캐시 (조회 시 캐시 활용)&lt;br /&gt;&amp;bull; 쓰기 지연 (트랜잭션 커밋 전 배치 실행)&lt;br /&gt;&amp;bull; 변경 감지 (커밋 시 자동 반영)&lt;br /&gt;&amp;bull; 지연 로딩 (필요 시 쿼리 실행)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Data JPA&lt;/b&gt;&lt;br /&gt;&amp;bull; 스프링이 JPA 위에 편리 기능을 추가한 모듈&lt;br /&gt;&amp;bull; PagingAndSortingRepository 상속한 JpaRepository 제공&lt;br /&gt;&amp;bull; 인터페이스 선언만으로 기본 CRUD + 페이징&amp;middot;정렬 기능 자동 제공&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #fbfcfd;&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {
} // 기본 CRUD 사용 가능&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;2-entity&amp;middot;repository-관련-어노테이션&quot; data-ke-size=&quot;size26&quot;&gt;2. Entity&amp;middot;Repository 관련 어노테이션&lt;/h2&gt;
어노테이션용도
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;b&gt;@Entity&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;클래스가 DB 테이블과 매핑되는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;JPA 엔티티&lt;/b&gt;임을 선언&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;b&gt;@Id&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;해당 필드를 엔티티의 PK(기본 키)로 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;b&gt;@GeneratedValue&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;PK 값을 자동 생성 전략(IDENTITY,&lt;span&gt;&amp;nbsp;&lt;/span&gt;SEQUENCE&lt;span&gt;&amp;nbsp;&lt;/span&gt;등)으로 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;b&gt;@Table&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;엔티티가 매핑될 테이블 이름&amp;middot;스키마 등 커스터마이징&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;b&gt;@Column&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;컬럼 이름&amp;middot;제약조건&amp;middot;길이 등을 커스터마이징&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;b&gt;@Repository&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;해당 클래스를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;JPA 데이터 액세스 계층&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;빈으로 등록, 예외를DataAccessException으로 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;b&gt;@Transactional&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;메서드 또는 클래스 단위로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;트랜잭션&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;시작&amp;middot;커밋&amp;middot;롤백 자동 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;b&gt;@PersistenceContext&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;EntityManager&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;주입 시 사용 (JPA 표준)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;b&gt;@Autowired&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Spring 컨텍스트에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;EntityManager&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;또는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Repository&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;빈 주입 시 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Entity&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 클래스가 DB 테이블과 매핑되는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;JPA 엔티티&lt;/b&gt;임을 선언&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Id&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 해당 필드를 엔티티의 PK(기본 키)로 지정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@GeneratedValue&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: PK 값을 자동 생성 전략(IDENTITY,&lt;span&gt;&amp;nbsp;&lt;/span&gt;SEQUENCE&lt;span&gt;&amp;nbsp;&lt;/span&gt;등)으로 지정&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #fbfcfd;&quot;&gt;&lt;code&gt;@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Repository :&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;해당 클래스가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;JPA 데이터 액세스 계층&lt;/b&gt;임을 나타내는&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #fbfcfd;&quot;&gt;&lt;code&gt;@Repository // @Component와 동일한 역할
public class MemberRepository {
    // 데이터베이스와의 상호작용 구현
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Transactional :&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메서드 또는 클래스 단위로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;트랜잭션&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;시작&amp;middot;커밋&amp;middot;롤백 자동 관리&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;background-color: #fbfcfd;&quot;&gt;&lt;code&gt;@Transactional
public void saveMember(Member member) {
    memberRepository.save(member);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@Table&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #fbfcfd;&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;members&quot;)// 이 엔티티는 members 테이블에 매핑된다
public class Member {
...
// 컬럼 이름을 username 으로 지정
@Column(name = &quot;username&quot;,
// 아무 속성 없이 쓰면, 필드명이 컬럼명이 된다
@Column                       
private String email;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@PersistenceContext&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;background-color: #fbfcfd;&quot;&gt;&lt;code&gt;@PersistenceContext
private EntityManager em; // JPA 표준 EntityManager&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Autowired&lt;span&gt;&amp;nbsp;&lt;/span&gt;대신&lt;span&gt;&amp;nbsp;&lt;/span&gt;@PersistenceContext를 써야&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;영속성 컨텍스트&lt;/b&gt;가 올바르게 연결&lt;/p&gt;
&lt;h2 id=&quot;h2-jdbc-url-모드별-차이&quot; data-ke-size=&quot;size26&quot;&gt;H2 JDBC URL 모드별 차이&lt;/h2&gt;
모드URL 예시설명
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;b&gt;로컬 파일 모드 (Embedded File)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;jdbc:h2:~/test&lt;/td&gt;
&lt;td&gt;- 홈 디렉터리(~)에&lt;span&gt;&amp;nbsp;&lt;/span&gt;test.mv.db&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일로 영구 저장- 애플리케이션 프로세스가 꺼져도 데이터 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;b&gt;TCP 서버 모드 (Server-TCP)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;jdbc:h2:tcp://localhost/~/test&lt;/td&gt;
&lt;td&gt;- 별도의 H2 서버 프로세스를 띄워 파일 모드 DB를 TCP로 공유- 다중 애플리케이션에서 같은 DB 접속 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;background-color: #f8f9fa;&quot;&gt;
&lt;td&gt;&lt;b&gt;인메모리 모드 (In-Memory)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;jdbc:h2:mem:testdb&lt;/td&gt;
&lt;td&gt;- 메모리 상에만 존재, 애플리케이션이 종료되면 사라짐- 빠른 테스트용, 별도 서버 불필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #12b886;&quot; href=&quot;http://localhost:8080/h2-console&quot;&gt;http://localhost:8080/h2-console&lt;/a&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I15oW/dJMcadVYLxA/Ba1z77xbPkBLSrQPxENO20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I15oW/dJMcadVYLxA/Ba1z77xbPkBLSrQPxENO20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I15oW/dJMcadVYLxA/Ba1z77xbPkBLSrQPxENO20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI15oW%2FdJMcadVYLxA%2FBa1z77xbPkBLSrQPxENO20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;912&quot; height=&quot;648&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;gradle , 의존성 설정&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #fbfcfd;&quot;&gt;&lt;code&gt;# (1) datasource URL만으로 드라이버 자동 감지
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=

# (2) Hibernate Dialect는 JPA 스타터가 자동 선택
spring.application.name=springboot-developer
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.defer-datasource-initialization=true

# Hibernate DDL
spring.jpa.hibernate.ddl-auto=update

# H2
spring.h2.console.enabled=true&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #fbfcfd;&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.h2database:h2' // 인메모리 데이터베이스
    implementation 'org.projectlombok:lombok:1.18.22'  // Add Lombok dependency
    annotationProcessor 'org.projectlombok:lombok:1.18.22'  // Add annotation processor
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL&lt;/p&gt;
&lt;pre class=&quot;sql&quot; style=&quot;background-color: #fbfcfd;&quot;&gt;&lt;code&gt;CREATE TABLE member (
id   BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50)
);
INSERT INTO member(name) VALUES('A'),('B');
SELECT * FROM member;

INSERT INTO article (title, content) VALUES ('제목 1', '내용 1');
INSERT INTO article (title, content) VALUES ('제목 2', '내용 2');
INSERT INTO article (title, content) VALUES ('제목 3', '내용 3');&lt;/code&gt;&lt;/pre&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;442&quot; data-origin-height=&quot;548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p5EhN/dJMcaf7lZhr/T8SbZP2fx1rd0Knk6IcKOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p5EhN/dJMcaf7lZhr/T8SbZP2fx1rd0Knk6IcKOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p5EhN/dJMcaf7lZhr/T8SbZP2fx1rd0Knk6IcKOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp5EhN%2FdJMcaf7lZhr%2FT8SbZP2fx1rd0Knk6IcKOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;442&quot; height=&quot;548&quot; data-origin-width=&quot;442&quot; data-origin-height=&quot;548&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;380&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPwStn/dJMcadVYLxB/gVZe0bbgdTKuFHHasYAiA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPwStn/dJMcadVYLxB/gVZe0bbgdTKuFHHasYAiA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPwStn/dJMcadVYLxB/gVZe0bbgdTKuFHHasYAiA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPwStn%2FdJMcadVYLxB%2FgVZe0bbgdTKuFHHasYAiA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;380&quot; height=&quot;582&quot; data-origin-width=&quot;380&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h3 id=&quot;-새롭게-알게-된-점--어려웠던-부분&quot; data-ke-size=&quot;size23&quot;&gt;  새롭게 알게 된 점 &amp;amp; 어려웠던 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관련 어노테이션을 찾아보면서, 몰랐던 기능과 잘못알고 있던 지식들을 바로 잡을 수 있게 되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-스터디-피드백&quot; data-ke-size=&quot;size23&quot;&gt;  스터디 피드백&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Data JPA를 사용하며 느꼈던 장단점&lt;/b&gt;&lt;br /&gt;장점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Query 어노테이션사용 &amp;rarr; JPQL 간단히 추가 가능&lt;/li&gt;
&lt;li&gt;JPARepository 상속으로 기본 CRUD 모두 제공 가능&lt;/li&gt;
&lt;li&gt;트랜잭션, 예외처리가 자동 &amp;rarr; 깔끔한 코드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복잡한 JOIN쿼리 같은경우 코드가 지저분해 보일 수 있음&lt;/li&gt;
&lt;li&gt;연관관계가 복잡할수록 N+1 문제가 쉽게 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;엔티티 클래스에 비즈니스 로직을 추가하면 어떨까요?&lt;/b&gt;&lt;br /&gt;관련 로직이 엔티티 안에 모여 있어 이해,유지보수가 용이하지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티 내부 로직이 데이터 접근, 의존하게 되면 단위테스트가 어려울 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 단순 검증, 상태 전환 같은 핵심 비즈니스 로직은 엔티티 메서드로 구현해도 되지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡하거나 외부 호출을 포함하는 비즈니스 로직은 서비스 계층에 두어 책임을 명확히 분리해야하지 않을까 싶음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에도 다른 팀원분들과의 토론을 진행했습니다.&lt;/p&gt;
&lt;/div&gt;</description>
      <author>myhousemouse</author>
      <guid isPermaLink="true">https://myhousemouse.tistory.com/4</guid>
      <comments>https://myhousemouse.tistory.com/4#entry4comment</comments>
      <pubDate>Sun, 19 Apr 2026 12:44:02 +0900</pubDate>
    </item>
    <item>
      <title>리트머스 기업 프로젝트 회고 - 대학생이 실제 스타트업이랑 협업해봤습니다(with 큐시즘)</title>
      <link>https://myhousemouse.tistory.com/3</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 스타트업 리트머스(Litmers)라는 IT 컨설팅&amp;middot;개발 스타트업과 기업 연계 프로젝트를 진행하면서 경험한 것들을 정리한 회고글이에요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 처음엔 그냥 포트폴리오 좀 쌓자는 마음으로 시작했는데, 끝나고 나니까 생각보다 훨씬 많은 걸 배웠고, 동시에 생각보다 훨씬 복잡했다는 걸 느꼈어요.. ㅠㅠ&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리트머스는 어떤 회사인가요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리트머스는 고객사(파트너)의 IT 프로젝트를 기획부터 개발까지 통합으로 진행해주는 스타트업이에요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 리트머스사의 Pain Point는 고객사와 소통할 때 채널(카카오톡, 이메일, 유선 등등)이 분산돼 있어서 히스토리 관리가 어렵다는 문제가 있었고, 우리 팀은 이걸 해결하는 &lt;b&gt;리트머스 파트너 플랫폼&lt;/b&gt;을 기획&amp;middot;디자인&amp;middot;개발하는 과제를 받았어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.instagram.com/litmers_kr/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.instagram.com/litmers_kr/&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://litmers.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://litmers.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774950592137&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;리트머스 | 고객의 사업 성공을 함께하는 IT 컨설팅 파트너&quot; data-og-description=&quot;리트머스는 기업 규모와 도메인에 무관하게 결과로 증명하는 IT 컨설팅 파트너입니다. 업계 최고 수준 전문가들이 합리적인 비용으로 신뢰할 수 있는 외주개발, 노코드 에이전시, 팀구독 솔루션&quot; data-og-host=&quot;litmers.com&quot; data-og-source-url=&quot;https://litmers.com/&quot; data-og-url=&quot;https://litmers.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bK21Wl/dJMb8RRR2YL/KvTzGYtOXBD49z5ZgTD6w1/img.jpg?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628&quot;&gt;&lt;a href=&quot;https://litmers.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://litmers.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bK21Wl/dJMb8RRR2YL/KvTzGYtOXBD49z5ZgTD6w1/img.jpg?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;리트머스 | 고객의 사업 성공을 함께하는 IT 컨설팅 파트너&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;리트머스는 기업 규모와 도메인에 무관하게 결과로 증명하는 IT 컨설팅 파트너입니다. 업계 최고 수준 전문가들이 합리적인 비용으로 신뢰할 수 있는 외주개발, 노코드 에이전시, 팀구독 솔루션&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;litmers.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;우리가 만든 것: 리트머스 파트너 플랫폼&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한마디로 설명하면 &lt;b&gt;&quot;고객사와 리트머스 PM이 프로젝트를 함께 관리하는 B2B 협업 툴&quot;&lt;/b&gt; 이에요. 기획 단계에서 확정한 핵심 기능들이에요:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;홈 대시보드&lt;/b&gt; &amp;mdash; 고객사가 로그인하자마자 &quot;지금 내가 뭘 해야 하는지&quot; 바로 파악&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문서함&lt;/b&gt; &amp;mdash; 리트머스가 올린 산출물을 고객사가 열람하고, 특정 줄에 하이라이트 댓글 달기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문의/요청&lt;/b&gt; &amp;mdash; 카카오톡으로 분산되던 요청을 구조화된 양식으로 플랫폼 안으로 흡수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해야할 일 (투두)&lt;/b&gt; &amp;mdash; PM이 고객사에게 할 일을 명확하게 배정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;회의록&lt;/b&gt; &amp;mdash; AI 요약 첨부해서 논의 내용 자동 구조화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;진행현황&lt;/b&gt; &amp;mdash; 기획&amp;rarr;디자인&amp;rarr;개발&amp;rarr;QA 단계를 프로세스 바로 시각화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;QA&lt;/b&gt; &amp;mdash; 고객사가 직접 결과물 검수하는 수정사항 칸반&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기술 스택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분류 기술&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;프론트엔드&lt;/td&gt;
&lt;td&gt;Next.js (TypeScript), shadcn/ui, Tailwind CSS, Zustand, TanStack Query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;백엔드&lt;/td&gt;
&lt;td&gt;Supabase (PostgreSQL + RLS + Storage + Realtime), Prisma ORM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인증&lt;/td&gt;
&lt;td&gt;WorkOS (B2B 이메일 초대 기반)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인프라&lt;/td&gt;
&lt;td&gt;Vercel + GitHub CI/CD&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스프린트별로 내가 한 것들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기획 파트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고객사 대부분이 IT 비전공자임을 감안했고 이를 토대로 사용자리서치를 진행했어요 사용자의 82%가 직관적이고 쉬운용어를 선호함을 알게 되었어요, 전체 화면에서 IT 용어를 전부 일상 언어로 치환하는 작업을 했어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QA &amp;rarr; 검수, TODO &amp;rarr; 해야할 일, 칸반 &amp;rarr; 처리 현황판 같은 식으로요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 &quot;그냥 단어 바꾸는 거잖아?&quot; 싶었는데, 막상 하다 보니까 고객 입장에서 어떤 표현이 더 직관적인지 계속 고민해야 해요!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기능명세서 DB 구축&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 29개 기능 항목을 노션 DB로 정리했어요. 각 항목마다 순번, 기능명, 세부기능, 기능설명, 상세동작, 예외처리, 입출력 로직, 개발 우선순위까지 구체적으로 정의하고 전달하는 업무를 진행했어요. 리트머스에서 사용하는 데이터 구조를 반영하기까지 많은 시간이 소요되어 어려운 경험이었어요 ㅠㅠ&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;선택과제 &amp;mdash; AI 자동화 파이프라인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PM이 반복적으로 하는 작업들(카카오톡 문의 &amp;rarr; 노션 정리, 회의 내용 &amp;rarr; 회의록 작성 등)을 AI가 어떻게 대체할 수 있는지 설계하는 과제였는데, AX설계하는 역량도 키운것 같아용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획 &amp;rarr; 디자인 &amp;rarr; 개발 각 단계마다 AI가 자동으로 다음 파트에 넘길 산출물을 생성하는 구조를 설계했어요. 기획 단계 완료 시 디자이너에게 화면 목록 초안이 자동으로 넘어가고, 디자인 완료 시 개발자에게 API Endpoint 초안이 자동으로 넘어가는 식으로 설계했어용&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;영상 시연 시나리오&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PM 홍길동이라는 가상의 캐릭터를 중심으로 4분 40초짜리 시연 흐름을 설계했어요. &quot;어드민이 아닌 PM 시점에서 어떻게 플랫폼을 쓰는지&quot;를 보여주는 게 핵심이었는데, 씬 구성부터 각 화면에서 강조할 포인트, 더미 데이터 세팅 체크리스트까지 다 작성하여 성공적으로 시연까지 마쳤습니다~&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;느낀점들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기획은 질문이 나오지 않을 만큼 구체적으로 적어야한다!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 &quot;기획이야 뭐 생각한 거 적으면 되지&quot; 했는데 실제로 해보니까 전혀 달랐어요. 기능 하나를 정의할 때 고객사 입장, PM 입장, Admin 입장을 전부 고려해야 하고, 예외 케이스를 빠짐없이 잡아야 하고, 개발자가 읽고 바로 이해할 수 있게 써야 해요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 &quot;진행현황에서 단계는 반드시 순서대로 진행되지 않는다&quot;는 설계 주의사항 같은 걸 실제 비즈니스 맥락을 이해해야 나올 수 있는 거거든요. 이런 부분에서 현업 PM의 인터뷰를 추가적으로 요청하고 대표님과의 미팅을 주기적으로 진행하여 맥락을 더 깊이 이해하려고 노력했어요&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 스타트업 미팅은 학교 팀플과 다르다!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표님이 미팅에서 &quot;지금 기획이 확정적이지 않아서 방향성을 잡기가 어렵다&quot;고 하셨을 때 좀 당황했어요. 학교 팀플에서는 요구사항이 명확하게 주어지는데, 실제 스타트업에서는 방향 자체가 계속 바뀌거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거기다 민감한 내부 DB에는 접근 불가, AI 워크플로우는 리트머스가 자체 고도화 중이라 보안 문제로 볼 수 없는 것들이 있어서, 이런 제약 안에서 우리가 할 수 있는 걸 찾는 게 생각보다 어려웠어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문서화는 모든 협업의 기초!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 기획서, 기능명세서, 영상 시연 시나리오, 선택과제 문서 등 엄청난 양의 문서를 작성했는데, 팀원끼리 커뮤니케이션할 때 문서가 큰 역할을 한 경우가 많았어요 &quot;이 기능이 어떻게 동작한다고 했지?&quot;를 문서 없이 기억에만 의존했으면 중요한 결정을 빠트렸을것 같아요 모든 기록을 노션으로 정리하였고 구두로 진행한 회의도 바로바로 기록하여 아카이빙 했습니다~&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현업에서 사용되는 AI는?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미팅에서 대표님이 직접 보여주신 AI 워크플로우가 신선한 충격이었어요. MCP로 디자인 스펙을 자동으로 읽고, 바이브 칸반으로 개발 태스크를 AI 에이전트한테 할당해서 자동으로 PR까지 올라오는 구조를 보여주셨거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI를 적극적으로 활용하는 회사에서 실제 워크플로우에 적용된 사례를 보며 JD에서 AI 역량을 강조하는 이유를 알게 되었어요.. !! 이번 경험을 계기로 AI 자동화에 대한 큰 관심이 생겼고, 지금도 매일 새로운 기술이 등장하는 만큼 이를 적극적으로 직접 활용해보는 것이 중요하다는 생각이 들었어요. 다양한 영역에서 폭넓게 활용하려고 해요!&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아쉬웠던 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 실제 고객사 피드백을 못 받은 것&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플랫폼을 만들었는데 실제 고객사가 써보고 &quot;이거 불편해요&quot;라는 피드백을 받았다면 훨씬 더 좋은 서비스가 됐을 것 같아요. 리서치, 큐시즘 내부 UT는 했지만 실제 사용자 테스트까지는 못 갔어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 기획-개발 간 싱크 맞추기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획에서 생각한 것과 개발에서 실제로 구현 가능한 것 사이에 갭이 있을 때 조율이 쉽지 않았어요. 기술적 제약에 대한 이해가 기획자한테도 필요하다는 걸 느꼈어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 변경 사항에 유연하게 대응하는 것&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스타트업 특성상 방향이 자주 바뀌는데, 그때마다 문서를 업데이트하고 팀원들이 싱크를 맞추는 게 꽤 힘들었어요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음에 이런 프로젝트를 하게 된다면&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;초반에 기업 쪽 제약 사항을 더 빠르게 파악할 것&lt;/b&gt; - 실제 워크 플로우를 볼 수 있다면, 초반에 보면서 맥락을 이해하는게 중요할것 같아요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;짧은 주기로 자주 공유하기&lt;/b&gt; - 기간이 5주밖에 되지않아서, 피드백을 받을 수 있는 시간이 생각보다 짧았어요 많은걸 보여주기보다, 짧게라도 보고하며 진행하는게 더 효율적이었을것 같아요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기술 스택을 기획자도 어느 정도 이해할 것&lt;/b&gt; - 리트머스 사에서 요구하는 개발 스택이 생소했어요.. 이를 어느정도 알고 있었다면, 구체적으로 설명하기 수월했을것 같아요&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트는 솔직히 처음에 생각했던 것보다 훨씬 어려웠어요. 학교 팀플처럼 요구사항이 딱 주어지고 그걸 만들면 끝나는 게 아니라, 요구사항 자체를 같이 만들어가야 했거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 그래서 더 재밌었고, 더 많이 배웠어요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서비스가 어떻게 기획되고, PM이 어떻게 일하고, AI가 실제 업무 현장에서 어떻게 쓰이는지를 몸으로 느낀 경험이었어요. 이론으로는 절대 배우지 못하는 실제 경험으로 쌓은 소중한 경험이었어요!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2268&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IwPKD/dJMcafTCjTx/xhQYsCbyQTHH3PYzvV03ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IwPKD/dJMcafTCjTx/xhQYsCbyQTHH3PYzvV03ek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IwPKD/dJMcafTCjTx/xhQYsCbyQTHH3PYzvV03ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIwPKD%2FdJMcafTCjTx%2FxhQYsCbyQTHH3PYzvV03ek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;186&quot; height=&quot;331&quot; data-origin-width=&quot;2268&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>it학회</category>
      <category>kusitms</category>
      <category>litmers</category>
      <category>기업프로젝트</category>
      <category>대외학회</category>
      <category>리트머스</category>
      <category>스타트업</category>
      <category>큐시즘</category>
      <category>큐시즘기획</category>
      <category>큐시즘학술</category>
      <author>myhousemouse</author>
      <guid isPermaLink="true">https://myhousemouse.tistory.com/3</guid>
      <comments>https://myhousemouse.tistory.com/3#entry3comment</comments>
      <pubDate>Tue, 31 Mar 2026 19:00:33 +0900</pubDate>
    </item>
    <item>
      <title>백엔드 스터디 후기</title>
      <link>https://myhousemouse.tistory.com/2</link>
      <description>&lt;h3 id=&quot;-기본-정보&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  기본 정보&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;주차/주제&lt;/b&gt;: 3주차 - REST API 설계와 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-학습-내용-요약&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  학습 내용 요약&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;API, REST API, RESTful API의 개념 정리하기&lt;/b&gt;&lt;b&gt;API는 응용 프로그램(애플리케이션)에서 운영 체제나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스를 뜻합니다.&lt;/b&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;기본 개념&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;애플리케이션&lt;/b&gt;이 다른&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;애플리케이션&lt;/b&gt;에 요청을 보내고, 응답을 받을 수 있도록&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;정해진 규칙&lt;/b&gt;(메서드, 데이터 형식 등)을 제공합니다.&lt;/li&gt;
&lt;li&gt;예를 들어,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;카카오톡&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;앱이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;구글 지도&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;API를 사용해서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;지도&lt;/b&gt;를 표시하는 것처럼, 다양한 프로그램들이 서로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;서로의 기능을 호출&lt;/b&gt;하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;결과를 받&lt;/b&gt;습니다.✅ REST API(Representational State Transfer)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;정의&lt;/b&gt;:웹상에서 사용되는 여러 리소스를&amp;nbsp;&lt;b&gt;HTTP URI로 표현&lt;/b&gt;하고, 해당 리소스에 대한 행위를&amp;nbsp;&lt;b&gt;HTTP Method로 정의&lt;/b&gt;하는&amp;nbsp;&lt;b&gt;방식&lt;/b&gt;을 말합니다.&lt;/li&gt;
&lt;li id=&quot;-기본-개념-1&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;기본 개념&lt;/b&gt;:&lt;/li&gt;
&lt;li&gt;&lt;b&gt;REST API&lt;/b&gt;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;웹에서 데이터를 주고받기 위해 사용하는 API&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/mft7l/dJMcabwLvWC/AAAAAAAAAAAAAAAAAAAAAENI5HwIFpCVRrJoSPm-Z_kYYyThIez6KWV0honxUAg-/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1774969199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=GkCZm2zx5hEX2%2BoOc6689RViAH0%3D&quot; /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTP&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;프로토콜을 사용하여 데이터를 주고받는 방식을 정의합니다.&lt;/li&gt;
&lt;li&gt;REST API는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;URL을 통해 리소스를 지정&lt;/b&gt;하고, HTTP 메서드(GET, POST, PUT, DELETE 등)를 통해 리소스에 대한 동작을 수행합니다.&lt;/li&gt;
&lt;li&gt;리소스는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터&lt;/b&gt;나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;객체&lt;/b&gt;일 수 있고, 이를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;HTTP 메서드&lt;/b&gt;로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;조회, 생성, 수정, 삭제&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li id=&quot;구성요소&quot;&gt;구성요소&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;자원(Resource) : HTTP URI&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자원에 대한 행위(Verb) : HTTP Method&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자원에 대한 행위의 내용 (Representations) : HTTP Message Pay Load, 데이터 형식&lt;/b&gt;&lt;b&gt;RESTful API&lt;/b&gt;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;REST 아키텍처 스타일을 잘 따르는 API&lt;/b&gt;&lt;b&gt;API를 RESTful 하게 만들어서&amp;nbsp;API의 목적이 무엇인지 명확하게 하기 위해&amp;nbsp;RESTful 함을 지향&amp;nbsp;합니다.&lt;/b&gt;&lt;/li&gt;
&lt;li id=&quot;-기본-개념-2&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;기본 개념&lt;/b&gt;:&lt;/li&gt;
&lt;li&gt;즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;REST API를 설계할 때&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;좋은 방식&lt;/b&gt;으로 설계된 API를 의미&lt;/li&gt;
&lt;li id=&quot;-restful-api-정의&quot;&gt;✅ RESTful API&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;정의&lt;/b&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;RESTful API&lt;/b&gt;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;REST&lt;/b&gt;의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;원칙과 규칙을 충실히 따르는 API&lt;/b&gt;입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/(슬래시)는 계층 관계를 나타낼때 사용합니다.&lt;/li&gt;
&lt;li&gt;URI 마지막 문자에 /(슬래시)를 사용하지 않습니다.&lt;/li&gt;
&lt;li&gt;URI에 _(underscore)는 사용하지 않도록 합니다.&amp;nbsp;또한 영어 대문자보다는&amp;nbsp;소문자를 씁니다.&lt;/li&gt;
&lt;li&gt;URI에&amp;nbsp;명사를 사용합니다.&lt;/li&gt;
&lt;li&gt;URI에 파일의 확장자(예를들어 .json , .JPGE)를 포함 시키지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;각 리소스는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;URL로 고유하게 식별&lt;/b&gt;되며, HTTP 메서드(GET, POST, PUT, DELETE 등)를 통해 리소스를 조작합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태를 유지하지 않으며&lt;/b&gt;, 각 요청은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;독립적&lt;/b&gt;으로 처리됩니다. 즉, 서버는 각 요청에 대해 필요한 정보만을 처리하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이전 요청을 기억하지 않습니다&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주로 파일 제어, 창 제어, 화상 처리, 문자 제어 등을 위한 인터페이스를 제공합니다&lt;/b&gt;&lt;/li&gt;
&lt;li id=&quot;--apiapplication-programming-interface-정의&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;API(Application&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Programming&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Interface) 정의&lt;/b&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1096&quot; data-origin-height=&quot;518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lyXm3/dJMcagSoTW6/OTKiwfZZg9lJ89K3lCK13K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lyXm3/dJMcagSoTW6/OTKiwfZZg9lJ89K3lCK13K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lyXm3/dJMcagSoTW6/OTKiwfZZg9lJ89K3lCK13K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlyXm3%2FdJMcagSoTW6%2FOTKiwfZZg9lJ89K3lCK13K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1096&quot; height=&quot;518&quot; data-origin-width=&quot;1096&quot; data-origin-height=&quot;518&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id=&quot;-restful-api-특징&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RESTful API 특징&lt;/b&gt;:&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;자원(Resource)&lt;/b&gt;: 모든 데이터는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;자원&lt;/b&gt;(리소스)으로 취급됩니다.&lt;br /&gt;- 예: 사용자(users), 게시글(posts), 제품(products)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTP 메서드&lt;/b&gt;: 각 자원에 대해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;GET, POST, PUT, DELETE&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메서드를 사용해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;읽기, 생성, 수정, 삭제&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;작업을 수행합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v07GC/dJMcaiimsGO/qD6YaPWgwMyK9i312oAEhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v07GC/dJMcaiimsGO/qD6YaPWgwMyK9i312oAEhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v07GC/dJMcaiimsGO/qD6YaPWgwMyK9i312oAEhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv07GC%2FdJMcaiimsGO%2FqD6YaPWgwMyK9i312oAEhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;480&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;무상태성(Stateless)&lt;/b&gt;: 각 요청은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;독립적&lt;/b&gt;이며, 서버는 이전 요청을 기억하지 않습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 로그인하지 않은 상태에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;GET /users&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;요청을 보내면, 서버는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;사용자 인증 정보를 기억하지 않음&lt;/b&gt;. 클라이언트는 매번&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;인증 정보&lt;/b&gt;를 요청에 담아 보내야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;유연한 응답 형식&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;JSON&lt;/b&gt;이나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;XML&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;형식으로 데이터를 응답받을 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Uniform Interface (인터페이스 일관성)&lt;/b&gt;: RESTful API는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;일관된 인터페이스&lt;/b&gt;를 유지하며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;URL&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;구조나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;메서드&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용을 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;명확한 규칙따름.&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리소스 이름은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;복수형&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Layered System (계층화)&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;HTTP 메서드(GET, POST, PUT, DELETE)의 용도와 차이점 정리하기&lt;/b&gt;&lt;b&gt;HTTPRequestMethod&lt;/b&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;용도&lt;/b&gt;:&lt;b&gt;POST&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메서드는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;새로운 리소스(데이터)를 서버에 생성&lt;/b&gt;할 때 사용됩니다. 즉, 서버에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;새로운 데이터를 추가&lt;/b&gt;하거나,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;새로운 객체를 생성&lt;/b&gt;할 때 쓰는 메서드입니다.✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;주요 특징&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;리소스 생성&lt;/b&gt;: 클라이언트가 서버에 요청을 보내고, 서버는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;새로운 리소스&lt;/b&gt;를 생성하여 저장합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유일한 ID&lt;/b&gt;: 서버에서 새로운 리소스를 생성할 때, 해당 리소스에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;고유 ID&lt;/b&gt;를 부여하는 경우가 많습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;본문(body)&lt;/b&gt;: POST 요청은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터를 요청 본문(body)&lt;/b&gt;에 담아 보냅니다. 이 데이터는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;주로 JSON 형식&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;멱등성 없음&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;같은 요청을 여러 번 보내면 매번 다른 결과&lt;/b&gt;를 초래할 수 있습니다. (즉, 중복 생성이 될 수 있음)&lt;/li&gt;
&lt;li id=&quot;-장점&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;장점&lt;/b&gt;:&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버에 데이터를 추가&lt;/b&gt;하는 데 적합합니다.&lt;/li&gt;
&lt;li&gt;데이터를 요청 본문에 담아 보낼 수 있어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;복잡한 데이터도 함께 전송 가능&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;요청이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;서버 상태를 변경&lt;/b&gt;하기 때문에,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;응답으로 생성된 리소스를 반환&lt;/b&gt;하는 것이 일반적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li id=&quot;●-post--생성-서버에-자원을-등록&quot;&gt;●&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;POST :&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;생성,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;서버에 자원을 등록&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;485&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J2a2T/dJMcabwLvWD/da2OmArraVZdhlPoN1GsMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J2a2T/dJMcabwLvWD/da2OmArraVZdhlPoN1GsMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J2a2T/dJMcabwLvWD/da2OmArraVZdhlPoN1GsMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ2a2T%2FdJMcabwLvWD%2Fda2OmArraVZdhlPoN1GsMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;485&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;485&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;새 유저 추가&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;/users에 POST 요청을 보내면 새로운 유저가 생성됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@PostMapping
    public String createUser(@RequestBody User user) {
        service.createUser(user);
        return &quot;사용자가 저장되었습니다.&quot;;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;POST /users
body : {date : &quot;example&quot;}
Content-Type : &quot;application/json&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;●-get-읽기-서버-자원을-얻음&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;●&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;GET:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;읽기,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;서버 자원을 얻음&lt;/b&gt;&lt;/h3&gt;
&lt;h3 id=&quot;-용도-1&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;용도&lt;/b&gt;:&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GET&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메서드는 주로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터를 조회하는 데 사용&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 서버에서 데이터를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;읽기만&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;수정하거나 삭제하지는 않습니다&lt;/b&gt;.&lt;/p&gt;
&lt;h3 id=&quot;-주요-특징-1&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;주요 특징&lt;/b&gt;:&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 조회&lt;/b&gt;: 클라이언트가 서버에 요청을 보내고, 서버는 데이터를 응답으로 보내줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;멱등성(Idempotency)&lt;/b&gt;: 같은 요청을 여러 번 보내도 결과가 변하지 않아요. 예를 들어,&lt;span&gt;&amp;nbsp;&lt;/span&gt;/coffees를 여러 번 요청해도, 항상 같은 커피 목록이 응답으로 돌아옵니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;URL 경로&lt;/b&gt;: 조회할 데이터를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;경로로 구분&lt;/b&gt;해 보냅니다. 예를 들어:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/coffees&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr; 모든 커피 목록 조회&lt;/li&gt;
&lt;li&gt;/coffees/{id}&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr; 특정 ID의 커피 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-장점-1&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;장점&lt;/b&gt;:&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;단순하고 직관적&lt;/b&gt;: 데이터를 요청하는 방식이 매우 직관적이라서 이해하기 쉽습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐싱 가능&lt;/b&gt;: GET 요청은 서버나 브라우저에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;캐싱&lt;/b&gt;될 수 있어, 동일한 요청을 더 빠르게 처리할 수 있습니다.&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;682&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DIlVr/dJMcaiimsGN/AbKPypDrtcaq3Ysh4kDvcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DIlVr/dJMcaiimsGN/AbKPypDrtcaq3Ysh4kDvcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DIlVr/dJMcaiimsGN/AbKPypDrtcaq3Ysh4kDvcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDIlVr%2FdJMcaiimsGN%2FAbKPypDrtcaq3Ysh4kDvcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;658&quot; height=&quot;682&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;682&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;1112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5DOTN/dJMcabwLvWF/4mR56jQwk1aiH3iYHqYZu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5DOTN/dJMcabwLvWF/4mR56jQwk1aiH3iYHqYZu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5DOTN/dJMcabwLvWF/4mR56jQwk1aiH3iYHqYZu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5DOTN%2FdJMcabwLvWF%2F4mR56jQwk1aiH3iYHqYZu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;664&quot; height=&quot;1112&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;1112&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모든 유저 조회&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;/users/에 GET 요청을 보내면 유저 목록을 받을 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특정 유저 조회&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;/users/{id}에 GET 요청을 보내면, 그 특정 유저 정보를 받을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@GetMapping(&quot;/&quot;)
    public List&amp;lt;User&amp;gt; getAllUsers() {
        return service.getAllUsers();  // 모든 사용자 목록 반환
    }
@GetMapping(&quot;/{id}&quot;)
    public User getUser(@PathVariable Long id) {
        return service.getUserById(id);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;●-put-업데이트-서버-자원을-새로운-자원으로-치환&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;*&lt;b&gt;*●&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;PUT:&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;업데이트,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;서버 자원을 새로운 자원으로 치환**&lt;/h3&gt;
&lt;h3 id=&quot;-용도-2&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;용도&lt;/b&gt;:&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PUT&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메서드는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;기존 리소스를 수정&lt;/b&gt;하거나,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;없는 리소스는 새로 생성&lt;/b&gt;할 때 사용됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 서버에 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;기존 데이터를 업데이트&lt;/b&gt;하거나,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;존재하지 않으면 새 데이터를 추가&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;-주요-특징-2&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;주요 특징&lt;/b&gt;:&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기존 리소스 수정&lt;/b&gt;: 클라이언트가 요청한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;URI&lt;/b&gt;에 해당하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;기존 데이터를 수정&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리소스 생성 가능&lt;/b&gt;: 만약 해당 리소스가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;존재하지 않으면&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;새로운 리소스를 생성할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;멱등성(Idempotency)&lt;/b&gt;: 같은 PUT 요청을 여러 번 보내도 결과가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;변하지 않음&lt;/b&gt;. 즉, 한 번 수정된 데이터는 같은 PUT 요청을 여러 번 보내도 결과가 동일합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;본문(body)&lt;/b&gt;: PUT 요청은 데이터를 요청 본문(body)에 담아 보내며, 주로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;JSON 형식&lt;/b&gt;으로 데이터를 보냅니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-장점-2&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;장점&lt;/b&gt;:&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;리소스 수정&lt;/b&gt;에 적합합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;불필요한 중복 요청을 피할 수&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;있어, 같은 요청을 여러 번 보내도 데이터는 동일하게 유지됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 업데이트와 생성&lt;/b&gt;을 하나의 메서드로 처리할 수 있어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;간편&lt;/b&gt;합니다.&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;534&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0tqjD/dJMcagSoTW7/xHDBb9aahTo259Da4USj6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0tqjD/dJMcagSoTW7/xHDBb9aahTo259Da4USj6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0tqjD/dJMcagSoTW7/xHDBb9aahTo259Da4USj6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0tqjD%2FdJMcagSoTW7%2FxHDBb9aahTo259Da4USj6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;686&quot; height=&quot;534&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;534&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;650&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/07nT7/dJMcabwLvWG/LkhkVRCHmYhcbCVwCYlVf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/07nT7/dJMcabwLvWG/LkhkVRCHmYhcbCVwCYlVf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/07nT7/dJMcabwLvWG/LkhkVRCHmYhcbCVwCYlVf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F07nT7%2FdJMcabwLvWG%2FLkhkVRCHmYhcbCVwCYlVf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;638&quot; height=&quot;650&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;650&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유저 정보 수정&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;/users/{id}에 PUT 요청을 보내면 해당 유저 정보를 수정합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;새로운 유저 생성&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;/users/{id}에 PUT 요청을 보내면, 해당 ID 유저가 없으면 새로 생성합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@PutMapping(&quot;/{id}&quot;)
    public String updateUser(@PathVariable Long id, @RequestBody User user) {
        user.setId(id);
        service.updateUser(user);
        return &quot;사용자 전체 수정 완료!&quot;;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;groovy&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;PUT /user/1
body : {date : &quot;update example&quot;}
Content-Type : &quot;application/json&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;●--patch-업데이트-서버-자원의-일부만-수정&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;●&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;PATCH:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;업데이트,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;서버 자원의 일부만 수정&lt;/b&gt;&lt;/h3&gt;
&lt;h3 id=&quot;-용도-3&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;용도&lt;/b&gt;:&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PATCH&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메서드는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;리소스의 일부만 수정&lt;/b&gt;할 때 사용됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 기존 리소스를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;완전히 교체&lt;/b&gt;하지 않고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;일부 속성만 업데이트&lt;/b&gt;할 때 사용해요.&lt;/p&gt;
&lt;h3 id=&quot;-주요-특징-3&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;주요 특징&lt;/b&gt;:&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;부분 수정&lt;/b&gt;: 리소스의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;전체가 아닌 일부만 수정&lt;/b&gt;합니다. 예를 들어, 커피의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이름만 바꾸고&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;가격은 그대로 두는 식입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;멱등성 없음&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;PATCH 요청은 멱등성&lt;/b&gt;을 보장하지 않아요. 즉, 같은 PATCH 요청을 여러 번 보내면 결과가 달라질 수 있습니다. (수정된 값이 여러 번 적용될 수 있음)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;본문(body)&lt;/b&gt;: PATCH 요청은 수정할&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터만 본문에 담아 보냅니다&lt;/b&gt;. 주로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;JSON 형식&lt;/b&gt;으로 보내며, 수정할 부분만 포함시킵니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적용 범위&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;PUT&lt;/b&gt;은 전체 리소스를 대체하지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;PATCH&lt;/b&gt;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;부분적인 수정만&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;진행해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-장점-3&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;장점&lt;/b&gt;:&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;부분 수정&lt;/b&gt;을 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;불필요한 데이터 변경을 피할 수&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;있습니다.&lt;/li&gt;
&lt;li&gt;데이터의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;일부만 변경&lt;/b&gt;하고 나머지는 그대로 두고 싶을 때 적합합니다.&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7ZV6Z/dJMcabwLvWH/pWgRSqkyDMyhVBpF6RcEh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7ZV6Z/dJMcabwLvWH/pWgRSqkyDMyhVBpF6RcEh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7ZV6Z/dJMcabwLvWH/pWgRSqkyDMyhVBpF6RcEh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7ZV6Z%2FdJMcabwLvWH%2FpWgRSqkyDMyhVBpF6RcEh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;664&quot; height=&quot;472&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자의 정보 일부 수정&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;/users/{id}에 PATCH 요청을 보내면 특정 정보만 업데이트됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dart&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@PatchMapping(&quot;/{id}&quot;)
    public String partialUpdateUser(@PathVariable Long id, @RequestBody Map&amp;lt;String, Object&amp;gt; updates) {
        service.partialUpdateUser(id, updates);
        return &quot;사용자 일부 수정 완료!&quot;;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PUT과 PATCH의 차이점과 멱등성&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;PUT의 멱등성&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PUT&lt;/b&gt;은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;리소스 전체를 대체&lt;/b&gt;하는 방식&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;첫 번째 요청 이후에 같은 데이터를 다시 보내면 결과가 변하지 않음&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;PATCH의 멱등성&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PATCH&lt;/b&gt;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;부분 수정&lt;/b&gt;을 하는 메서드&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;PATCH&lt;span&gt;&amp;nbsp;&lt;/span&gt;요청이 들어올 때마다&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;부분적인 값이 계속 수정&lt;/b&gt;될 수 있음&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PUT&lt;/b&gt;은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;리소스 전체를 대체&lt;/b&gt;하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;결과가 항상 같게 유지되므로 멱등성&lt;/b&gt;이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;보장됨&lt;/b&gt;.&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PATCH&lt;/b&gt;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;부분만 수정&lt;/b&gt;하기 때문에, 같은 요청을 여러 번 보낼 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;결과가 달라질 수 있어 멱등성&lt;/b&gt;이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;보장되지 않음&lt;/b&gt;.&lt;/p&gt;
&lt;h3 id=&quot;●-delete--삭제-서버-자원을-삭제&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;●&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;DELETE:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;삭제,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;서버 자원을 삭제&lt;/b&gt;&lt;/h3&gt;
&lt;h3 id=&quot;-용도-4&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;용도&lt;/b&gt;:&lt;/h3&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DELETE&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메서드는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;리소스를 삭제&lt;/b&gt;할 때 사용됩니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 서버에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;특정 데이터를 삭제&lt;/b&gt;하는 요청을 처리합니다.&lt;/p&gt;
&lt;h3 id=&quot;-주요-특징-4&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;주요 특징&lt;/b&gt;:&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;리소스 삭제&lt;/b&gt;: 클라이언트가 서버에 요청을 보내면, 서버는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;특정 리소스&lt;/b&gt;를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;삭제&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;멱등성(Idempotency)&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;DELETE&lt;/b&gt;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;멱등성&lt;/b&gt;을 보장합니다. 즉, 같은 DELETE 요청을 여러 번 보내도,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;결과가 같고&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;삭제된 리소스는 다시 복구되지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;URI 경로&lt;/b&gt;: DELETE 요청은 보통&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;삭제할 리소스의 URI&lt;/b&gt;를 지정해 보내며, 리소스를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;찾아서 삭제&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;응답 상태 코드&lt;/b&gt;: 삭제가 성공하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;204 No Content&lt;span&gt;&amp;nbsp;&lt;/span&gt;응답을 보낼 수 있고, 리소스가 없으면&lt;span&gt;&amp;nbsp;&lt;/span&gt;404 Not Found를 보낼 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-장점-4&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;✅&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;장점&lt;/b&gt;:&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;리소스 삭제&lt;/b&gt;를 간단하고 명확하게 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;멱등성&lt;/b&gt;이 보장되므로 여러 번 삭제 요청을 보내도 결과가 동일합니다.&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;934&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3QSr7/dJMcagSoTW9/RhNddl1JsNh1dUKRdmsP8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3QSr7/dJMcagSoTW9/RhNddl1JsNh1dUKRdmsP8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3QSr7/dJMcagSoTW9/RhNddl1JsNh1dUKRdmsP8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3QSr7%2FdJMcagSoTW9%2FRhNddl1JsNh1dUKRdmsP8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;860&quot; height=&quot;934&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;934&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자 삭제&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;/users/{id}에 DELETE 요청을 보내면, 해당 사용자를 삭제합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;@DeleteMapping(&quot;/{id}&quot;)
    public String deleteUser(@PathVariable Long id) {
        service.deleteUser(id);
        return &quot;사용자 삭제 완료!&quot;;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;●&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;OPTIONS:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;통신옵션 조회 (리소스가 지원하고 있는 메소드의 취득)&lt;br /&gt;●&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;HEAD:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;응답헤더 조회(메타 데이터의 취득)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyBXdt/dJMcaiimsGP/vCtjTCIHe25L1h3uxN6Du0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyBXdt/dJMcaiimsGP/vCtjTCIHe25L1h3uxN6Du0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyBXdt/dJMcaiimsGP/vCtjTCIHe25L1h3uxN6Du0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyBXdt%2FdJMcaiimsGP%2FvCtjTCIHe25L1h3uxN6Du0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;502&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;간단한 API 기능 2개 구상해오기 (ex: 사용자 조회, 게시글 작성 등)&lt;/b&gt;&lt;br /&gt;&lt;b&gt;1. 도서 추가 (POST /books)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;POST /books
{
    &quot;title&quot;: &quot;자바의 정석&quot;,
    &quot;author&quot;: &quot;남궁성&quot;,
    &quot;price&quot;: 25000,
    &quot;category&quot;: &quot;Programming&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 도서 목록 조회 (GET /books)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;GET /books?category=Programming&amp;amp;price_max=30000
[
    {
        &quot;id&quot;: 1,
        &quot;title&quot;: &quot;자바의 정석&quot;,
        &quot;author&quot;: &quot;남궁성&quot;,
        &quot;price&quot;: 25000,
        &quot;category&quot;: &quot;Programming&quot;
    },
    {
        &quot;id&quot;: 2,
        &quot;title&quot;: &quot;파이썬 완벽 가이드&quot;,
        &quot;author&quot;: &quot;홍길동&quot;,
        &quot;price&quot;: 20000,
        &quot;category&quot;: &quot;Programming&quot;
    }
]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;-새롭게-알게-된-점--어려웠던-부분&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  새롭게 알게 된 점 &amp;amp; 어려웠던 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새롭게 알게된 점: API 개념을 다시 정확하게 알게 되었고, HTTP 메서드와 API 기능에 대해 더 깊게 탐구하고 싶어졌습니다.&lt;/li&gt;
&lt;li&gt;어려웠던 부분: HTTP 메서드와 API 기능을 더 깊게 이해하는 부분이 어려웠습니다.&lt;/li&gt;
&lt;li&gt;해결 방법: HTTP 메서드와 API 기능에 대해 더 많은 예제와 실습을 통해 이해를 돕고, 구체적인 동작 방식을 연구해볼 계획입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-과제실습-결과&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  과제/실습 결과&lt;/h3&gt;
&lt;pre class=&quot;markdown&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;- Spring Boot
- Gradle
- Jsp templeate
- H2 데이터베이스
- JDK 17

###  ️설정&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;    plugins {
        id 'java'
        id 'org.springframework.boot' version '3.4.4'
        id 'io.spring.dependency-management' version '1.1.7'
    }
    
    group = 'com.example'
    version = '0.0.1-SNAPSHOT'
    
    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(17)
        }
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        implementation 'org.springframework.boot:spring-boot-starter-web'
        runtimeOnly 'com.h2database:h2'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    }
    
    test {
        useJUnitPlatform()
    }&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;# H2 메모리 데이터베이스 설정
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# H2 콘솔 사용 설정
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# 초기 SQL 실행
spring.sql.init.mode=always&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;schema 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;CREATE TABLE users (
                       id BIGINT AUTO_INCREMENT PRIMARY KEY,
                       name VARCHAR(100),
                       email VARCHAR(100)
);

CREATE TABLE posts (
                       id BIGINT AUTO_INCREMENT PRIMARY KEY,
                       title VARCHAR(200),
                       content TEXT,
                       user_id BIGINT,
                       FOREIGN KEY (user_id) REFERENCES users(id)
);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;사용자조회-기능&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;사용자조회 기능&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byR8oI/dJMcabwLvWE/3g6H0cQQutqSw2srNF0VN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byR8oI/dJMcabwLvWE/3g6H0cQQutqSw2srNF0VN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byR8oI/dJMcabwLvWE/3g6H0cQQutqSw2srNF0VN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyR8oI%2FdJMcabwLvWE%2F3g6H0cQQutqSw2srNF0VN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;404&quot; height=&quot;288&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id=&quot;1-model&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. Model&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;package com.example.userapi.model;
public class User {
    private Long id;
    private String name;
    private String email;

    public User() {} // Spring이 객체를 만들 때 기본 생성자를 사용

    public User(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    // Getter / Setter
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;id&lt;/li&gt;
&lt;li&gt;name&lt;/li&gt;
&lt;li&gt;email&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-controller&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. Controller&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;package com.example.userapi.controller;

import com.example.userapi.model.User;
import com.example.userapi.service.UserService;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping(&quot;/users&quot;)
public class UserController {

    private final UserService service;

    public UserController(UserService service) {
        this.service = service;
    }

    @GetMapping(&quot;/&quot;)
    public List&amp;lt;User&amp;gt; getAllUsers() {
        return service.getAllUsers();  // 모든 사용자 목록 반환
    }

    @GetMapping(&quot;/{id}&quot;)
    public User getUser(@PathVariable Long id) {
        return service.getUserById(id);
    }

    @PostMapping
    public String createUser(@RequestBody User user) {
        service.createUser(user);
        return &quot;사용자가 저장되었습니다.&quot;;
    }

    @PutMapping(&quot;/{id}&quot;)
    public String updateUser(@PathVariable Long id, @RequestBody User user) {
        user.setId(id);
        service.updateUser(user);
        return &quot;사용자 전체 수정 완료!&quot;;
    }

    @PatchMapping(&quot;/{id}&quot;)
    public String partialUpdateUser(@PathVariable Long id, @RequestBody Map&amp;lt;String, Object&amp;gt; updates) {
        service.partialUpdateUser(id, updates);
        return &quot;사용자 일부 수정 완료!&quot;;
    }

    @DeleteMapping(&quot;/{id}&quot;)
    public String deleteUser(@PathVariable Long id) {
        service.deleteUser(id);
        return &quot;사용자 삭제 완료!&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-service&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3. Service&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;package com.example.userapi.service;
import com.example.userapi.model.User;
import com.example.userapi.repository.UserRepository;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.Optional;

@Service
public class UserService {

    private final UserRepository repo;

    public UserService(UserRepository repo) {
        this.repo = repo;
    }

    public void createUser(User user) {
        repo.save(user);
    }

    public List&amp;lt;User&amp;gt; getAllUsers() {
        return repo.findAll();  // 모든 사용자 목록 반환
    }

    public User getUserById(Long id) {
        return repo.findById(id)
                .orElseThrow(() -&amp;gt; new RuntimeException(&quot;사용자를 찾을 수 없습니다.&quot;));
    }

    public void updateUser(User user) {
        repo.update(user);
    }

    public void partialUpdateUser(Long id, Map&amp;lt;String, Object&amp;gt; updates) {
        User user = getUserById(id);
        if (updates.containsKey(&quot;name&quot;)) {
            user.setName((String) updates.get(&quot;name&quot;));
        }
        if (updates.containsKey(&quot;email&quot;)) {
            user.setEmail((String) updates.get(&quot;email&quot;));
        }
        repo.update(user);
    }

    public void deleteUser(Long id) {
        repo.deleteById(id);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4-repository&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4. Repository&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;package com.example.userapi.repository;
import com.example.userapi.model.User;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public class UserRepository {

    private final JdbcTemplate jdbc;

    public UserRepository(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    public void save(User user) {
        jdbc.update(&quot;INSERT INTO users (name, email) VALUES (?, ?)&quot;,
                user.getName(), user.getEmail());
    }

    public List&amp;lt;User&amp;gt; findAll() {
        return jdbc.query(&quot;SELECT * FROM users&quot;, userRowMapper());
    }

    public Optional&amp;lt;User&amp;gt; findById(Long id) {
        // jdbc.query()를 통해 결과를 리스트로 받고, 그 리스트에서 첫 번째 값을 Optional로 반환
        List&amp;lt;User&amp;gt; result = jdbc.query(&quot;SELECT * FROM users WHERE id = ?&quot;,
                new Object[]{id},
                userRowMapper());
        return result.stream().findAny();  // 결과가 있으면 첫 번째 항목을 Optional로 반환
    }

    public void update(User user) {
        jdbc.update(&quot;UPDATE users SET name = ?, email = ? WHERE id = ?&quot;,
                user.getName(), user.getEmail(), user.getId());
    }

    public void deleteById(Long id) {
        jdbc.update(&quot;DELETE FROM users WHERE id = ?&quot;, id);
    }

    private RowMapper&amp;lt;User&amp;gt; userRowMapper() {
        return (rs, rowNum) -&amp;gt; new User(
                rs.getLong(&quot;id&quot;),
                rs.getString(&quot;name&quot;),
                rs.getString(&quot;email&quot;)
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;게시글-게시-기능&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;게시글 게시 기능&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kQqid/dJMcagSoTW8/GvqLZZZBSEEb6ePyOp3qEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kQqid/dJMcagSoTW8/GvqLZZZBSEEb6ePyOp3qEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kQqid/dJMcagSoTW8/GvqLZZZBSEEb6ePyOp3qEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkQqid%2FdJMcagSoTW8%2FGvqLZZZBSEEb6ePyOp3qEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;854&quot; height=&quot;344&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id=&quot;1-model-1&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. Model&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;package com.example.userapi.model;

public class Post {
    private Long id;
    private String title;
    private String content;
    private Long userId;  // 작성자 ID

    public Post() {}

    public Post(Long id, String title, String content, Long userId) {
        this.id = id;
        this.title = title;
        this.content = content;
        this.userId = userId;
    }

    // Getter/Setter
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }

    public String getContent() { return content; }
    public void setContent(String content) { this.content = content; }

    public Long getUserId() { return userId; }
    public void setUserId(Long userId) { this.userId = userId; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;id&lt;/li&gt;
&lt;li&gt;title&lt;/li&gt;
&lt;li&gt;content&lt;/li&gt;
&lt;li&gt;userId&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-controller-1&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. Controller&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;package com.example.userapi.controller;
import com.example.userapi.model.Post;
import com.example.userapi.service.PostService;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(&quot;/posts&quot;)
public class PostController {

    private final PostService service;

    public PostController(PostService service) {
        this.service = service;
    }

    @PostMapping
    public String createPost(@RequestBody Post post) {
        service.createPost(post);
        return &quot;게시글이 저장되었습니다.&quot;;
    }

    @GetMapping(&quot;/{id}&quot;)
    public Post getPost(@PathVariable Long id) {
        return service.getPost(id);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-service-1&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3. Service&lt;/h3&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;package com.example.userapi.service;
import com.example.userapi.model.Post;
import com.example.userapi.repository.PostRepository;
import com.example.userapi.repository.UserRepository;
import org.springframework.stereotype.Service;

@Service
public class PostService {

    private final PostRepository postRepo;
    private final UserRepository userRepo;

    public PostService(PostRepository postRepo, UserRepository userRepo) {
        this.postRepo = postRepo;
        this.userRepo = userRepo;
    }

    public void createPost(Post post) {
        // 작성자 존재 여부 확인 (옵션)
        userRepo.findById(post.getUserId())
                .orElseThrow(() -&amp;gt; new RuntimeException(&quot;작성자(User)를 찾을 수 없습니다.&quot;));
        postRepo.save(post);
    }

    public Post getPost(Long id) {
        return postRepo.findById(id)
                .orElseThrow(() -&amp;gt; new RuntimeException(&quot;게시글을 찾을 수 없습니다.&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4-repository-1&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4. Repository&lt;/h3&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;package com.example.userapi.repository;

import com.example.userapi.model.Post;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public class PostRepository {

    private final JdbcTemplate jdbc;

    public PostRepository(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    public void save(Post post) {
        jdbc.update(&quot;INSERT INTO posts (title, content, user_id) VALUES (?, ?, ?)&quot;,
                post.getTitle(), post.getContent(), post.getUserId());
    }

    public Optional&amp;lt;Post&amp;gt; findById(Long id) {
        return jdbc.query(&quot;SELECT * FROM posts WHERE id = ?&quot;,
                new Object[]{id},
                rs -&amp;gt; {
                    if (rs.next()) {
                        return Optional.of(new Post(
                                rs.getLong(&quot;id&quot;),
                                rs.getString(&quot;title&quot;),
                                rs.getString(&quot;content&quot;),
                                rs.getLong(&quot;user_id&quot;)
                        ));
                    } else {
                        return Optional.empty();
                    }
                });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;사용자-조회-게시글-플로우차트&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;lt;사용자 조회, 게시글 플로우차트&amp;gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;1240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NxrsI/dJMcagSoTXa/YMzLa8oXSsADPlJsV4QUDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NxrsI/dJMcagSoTXa/YMzLa8oXSsADPlJsV4QUDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NxrsI/dJMcagSoTXa/YMzLa8oXSsADPlJsV4QUDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNxrsI%2FdJMcagSoTXa%2FYMzLa8oXSsADPlJsV4QUDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;786&quot; height=&quot;1240&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;1240&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 id=&quot;-스터디-피드백&quot; style=&quot;background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;  스터디 피드백&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;받은 피드백:&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #fbfdfc; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;REST API 설계에서 가장 중요한 원칙은 무엇인가요?&lt;br /&gt;일관성이 가장 중요하며, URL, 메서드, 응답 형식 등을 일관되게 설계하면 개발자들이 API를 쉽게 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;HTTP 상태 코드의 의미와 중요성&lt;br /&gt;상태 코드는 요청 결과를 나타내며, 404 Not Found, 500 Internal Server Error 등의 코드가 오류를 파악하고 디버깅에 중요합니다. 각 코드의 숫자는 상태를 쉽게 파악할 수 있기 때문입니다.&lt;/li&gt;
&lt;li&gt;API 성능 최적화 방법&lt;br /&gt;캐싱과 상태 코드 출력을 통해 응답 속도를 높이고, 쿼리 최적화를 통해 성능을 개선할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <author>myhousemouse</author>
      <guid isPermaLink="true">https://myhousemouse.tistory.com/2</guid>
      <comments>https://myhousemouse.tistory.com/2#entry2comment</comments>
      <pubDate>Sun, 22 Mar 2026 16:56:41 +0900</pubDate>
    </item>
    <item>
      <title>백엔드 스터디 후기</title>
      <link>https://myhousemouse.tistory.com/1</link>
      <description>&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;주차/주제&lt;/b&gt;: 2주차 - 프로젝트 구조와 아키텍처 이해&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div style=&quot;color: #212529;&quot;&gt;
&lt;div&gt;
&lt;h3 id=&quot;-학습-내용-요약&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;학습 내용 요약&lt;/h3&gt;
&lt;h2 id=&quot;mvc-구조&quot; data-ke-size=&quot;size26&quot;&gt;MVC 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MVC&lt;/b&gt;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;애플리케이션을 세 가지 주요 구성 요소로 분리한 디자인 패턴&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 독립적으로 동작&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI와 관련된 로직 분리를 목표&lt;/p&gt;
&lt;h3 id=&quot;model-모델&quot; data-ke-size=&quot;size23&quot;&gt;Model (모델)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터와 관련된 로직을 담당&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;DB와 연결되어 데이터를 저장하거나 가져오고, 가공하는 등의 역할&lt;/li&gt;
&lt;li&gt;스프링부트에선 보통&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Entity, DTO, Repository, Service&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이런 것들이 모델 영역&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;package hello.hello_spring.domain;

public class Member {
    private Long id;  // 회원 ID
    private String name;  // 회원 이름

    // id getter()
    public Long getId() {
        return id;
    }

    // id setter ()
    public void setId(Long id) {
        this.id = id; 
    }

    // name getter
    public String getName() {
        return name;
    }

    // name setter
    public void setName(String name) {
        this.name = name;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Getter 메서드 : 객체의 속성(필드)의 값을 반환 (외부에서 읽을 수 있게)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주로&lt;span&gt;&amp;nbsp;&lt;/span&gt;get시작&lt;/li&gt;
&lt;li&gt;필드 이름 그대로 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Setter 메서드 : 객체의 속성(필드)의 값을 설정 (외부에서 수정할 수 있게)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주로&lt;span&gt;&amp;nbsp;&lt;/span&gt;set시작&lt;/li&gt;
&lt;li&gt;필드 이름 그대로 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;view-뷰&quot; data-ke-size=&quot;size23&quot;&gt;View (뷰)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자에게 보여지는 화면 (UI 해당)&lt;/li&gt;
&lt;li&gt;HTML, Thymeleaf, JSON 등 사용자에게 결과를 보여주는 부분&lt;/li&gt;
&lt;li&gt;스프링부트에선&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Thymeleaf, Mustache&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 템플릿 엔진이나,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;REST API&lt;/b&gt;라면 JSON 응답이 이에 해당
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Thymeleaf - 복잡한 화면&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Mustache - 가볍게 사용&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dust&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;Hello View&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;h1 th:text=&quot;${data}&quot;&amp;gt;Default Text&amp;lt;/h1&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;controller-컨트롤러&quot; data-ke-size=&quot;size23&quot;&gt;Controller (컨트롤러)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;클라이언트의 요청을 받아 적절한 Model 준비하고 View에 전달하는 역할&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;HTTP 요청을 받고, 필요한 비즈니스 로직(서비스)을 호출하고, 그 결과를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;View나 JSON으로 반환&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;@Controller
public class HelloController {
    @GetMapping(&quot;hello&quot;) // http://localhost:8080/hello GET 요청을 보낼 때 이 메서드가 실행됨
    public String hello(Model model) { //뷰 이름을 뷰 리졸버(ViewResolver)로 뷰 파일을 찾아서 렌더링
        model.addAttribute(&quot;data&quot;, &quot;hello!!&quot;); //뷰(View) 가 데이터를 필요로 할 때 이 데이터를 전달
        return &quot;hello&quot;;
    } //뷰 이름 반환, resources/templates/hello.html 을 렌더링함 (기본 템플릿이 Thymeleaf)
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 id=&quot;예시-도서관에서-책을-대출하고-반납하는-과정&quot; data-ke-size=&quot;size23&quot;&gt;예시 (도서관에서 책을 대출하고 반납하는 과정)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Model (모델)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Book 클래스
&lt;pre class=&quot;arduino&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;public class Book {
private String title;
private String author;
private boolean isAvailable;  // 대출 가능 여부

public Book(String title, String author) {
    this.title = title;
    this.author = author;
    this.isAvailable = true;  // 기본적으로 책은 대출 가능
}

// Getter and Setter
public String getTitle() {
    return title;
}

public boolean isAvailable() {
    return isAvailable;
}

public void setAvailable(boolean available) {
    isAvailable = available;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Library 클래스&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cs&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;public class Library {
    private List&amp;lt;Book&amp;gt; books;

    public Library() {
        books = new ArrayList&amp;lt;&amp;gt;();
        // 도서 목록 추가
        books.add(new Book(&quot;Java Programming&quot;, &quot;John Doe&quot;));
        books.add(new Book(&quot;Spring Framework&quot;, &quot;Jane Doe&quot;));
    }

    public List&amp;lt;Book&amp;gt; getBooks() {
        return books;
    }

    public boolean borrowBook(String title) {
        for (Book book : books) {
            if (book.getTitle().equals(title) &amp;amp;&amp;amp; book.isAvailable()) {
                book.setAvailable(false);  // 책 대출 처리
                return true;
            }
        }
        return false;  // 책이 없거나 대출 불가능
    }

    public boolean returnBook(String title) {
        for (Book book : books) {
            if (book.getTitle().equals(title) &amp;amp;&amp;amp; !book.isAvailable()) {
                book.setAvailable(true);  // 책 반납 처리
                return true;
            }
        }
        return false;  // 책이 없거나 반납할 수 없는 경우
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. View(뷰)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;book-list.html : 사용자에게 책 목록을 보여주는 페이지&lt;/p&gt;
&lt;pre class=&quot;dust&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;도서관&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;도서 목록&amp;lt;/h1&amp;gt;
    &amp;lt;ul&amp;gt;
        &amp;lt;li th:each=&quot;book : ${books}&quot;&amp;gt;
            &amp;lt;span th:text=&quot;${book.title}&quot;&amp;gt;&amp;lt;/span&amp;gt; 
            &amp;lt;span th:text=&quot;${book.author}&quot;&amp;gt;&amp;lt;/span&amp;gt; 
            &amp;lt;span th:text=&quot;${book.isAvailable ? '대출 가능' : '대출 중'}&quot;&amp;gt;&amp;lt;/span&amp;gt;
        &amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;borrow-book.html&lt;/b&gt;: 사용자가 책을 대출할 수 있는 양식을 제공하는 페이지&lt;/p&gt;
&lt;pre class=&quot;xml&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;책 대출&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;책을 대출합니다&amp;lt;/h1&amp;gt;
    &amp;lt;form action=&quot;/borrow-book&quot; method=&quot;post&quot;&amp;gt;
        &amp;lt;label for=&quot;title&quot;&amp;gt;책 제목:&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;text&quot; id=&quot;title&quot; name=&quot;title&quot; /&amp;gt;
        &amp;lt;button type=&quot;submit&quot;&amp;gt;대출하기&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Controller (컨트롤러)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;@Controller
public class LibraryController {

    private Library library = new Library();

    @GetMapping(&quot;/books&quot;)
    public String showBooks(Model model) {
        // 모델에 도서 목록 추가
        model.addAttribute(&quot;books&quot;, library.getBooks());
        return &quot;book-list&quot;;  // book-list.html로 전달
    }

    @GetMapping(&quot;/borrow-book&quot;)
    public String borrowBookForm() {
        return &quot;borrow-book&quot;;  // borrow-book.html로 대출 폼 전달
    }

    @PostMapping(&quot;/borrow-book&quot;)
    public String borrowBook(@RequestParam(&quot;title&quot;) String title, Model model) {
        boolean success = library.borrowBook(title);
        if (success) {
            model.addAttribute(&quot;message&quot;, &quot;책이 대출되었습니다.&quot;);
        } else {
            model.addAttribute(&quot;message&quot;, &quot;책을 대출할 수 없습니다.&quot;);
        }
        return &quot;redirect:/books&quot;;  // 대출 후 책 목록으로 리다이렉트
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비즈니스 로직 처리와 데이터 접근을 명확하게 분리&lt;/li&gt;
&lt;li&gt;서비스 계층(Service Layer) 중심 비즈니스 로직을 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터베이스와의 상호작용&lt;/b&gt;을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Repository&lt;/b&gt;에서 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;csrcontroller-service-repository-계층-조사하기&quot; data-ke-size=&quot;size26&quot;&gt;CSR(Controller-Service-Repository) 계층 조사하기&lt;/h2&gt;
&lt;h3 id=&quot;controller-계층-web-layer&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Controller 계층 (Web Layer)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;HTTP 요청&lt;/b&gt;을 처리&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자로부터&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;입력 데이터를 받기&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(HTTP 요청 파라미터, 폼 데이터 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Service 계층&lt;/b&gt;을 호출하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;비즈니스 로직 처리&lt;/b&gt;를 전달&lt;/li&gt;
&lt;li&gt;그 후,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;응답을 사용자에게 전달&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(뷰 반환 또는 REST API 응답)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;service-계층-business-logic-layer&quot; data-ke-size=&quot;size23&quot;&gt;Service 계층 (Business Logic Layer)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Controller&lt;/b&gt;에서 받은 요청을 바탕으로 실제&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;비즈니스 로직&lt;/b&gt;을 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스나 외부 API와의 상호작용을 할 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Repository 계층&lt;/b&gt;을 호출&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;비즈니스 로직 처리&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Controller&lt;/b&gt;에서 요청받은 데이터를 처리합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Repository 계층 호출&lt;/b&gt;: 데이터를 조회하거나 수정할 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Repository&lt;/b&gt;를 호출하여 데이터를 처리합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 처리&lt;/b&gt;: 서비스 계층에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;트랜잭션 관리&lt;/b&gt;를 담당하는 경우가 많습니다. (@Transactional&lt;span&gt;&amp;nbsp;&lt;/span&gt;어노테이션 사용)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;package hello.hello_spring.service;

import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    public Long join(Member member) {
        // 같은 이름이 있는 중복 회원 X
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -&amp;gt; {
                    throw new IllegalStateException(&quot;이미 존재하는 회원&quot;);
                });
    }

    public List&amp;lt;Member&amp;gt; findMembers() {
        return memberRepository.findAll();
    }

    public Optional&amp;lt;Member&amp;gt; findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;repository-계층-data-access-layer&quot; data-ke-size=&quot;size23&quot;&gt;Repository 계층 (Data Access Layer)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Repository&lt;/b&gt;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터베이스와의 상호작용&lt;/b&gt;을 담당하는 계층&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Data JPA&lt;/b&gt;와 같은 ORM(Object-Relational Mapping) 프레임워크를 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터베이스 쿼리&lt;/b&gt;를 수행&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터베이스 접근&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Entity&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;객체를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터베이스&lt;/b&gt;에 저장하거나 조회하는 역할&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쿼리 메서드 제공&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;JpaRepository를 상속하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;기본적인 CRUD 작업&lt;/b&gt;을 자동으로 제공하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;맞춤형 쿼리 메서드&lt;/b&gt;를 정의&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;
import java.util.Optional;
import java.util.List;

public interface MemberRepository {
   Member save(Member member); //저장소에 저장
   Optional&amp;lt;Member&amp;gt; findById(Long id); //조회
   Optional&amp;lt;Member&amp;gt; findByName(String name);
   List&amp;lt;Member&amp;gt; findAll();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원 정보 저장&lt;/li&gt;
&lt;li&gt;회원정보 id 찾기&lt;/li&gt;
&lt;li&gt;회원정보 이름 찾기&lt;/li&gt;
&lt;li&gt;전체 회원 찾기&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import java.util.*;

// MemberRepository를 구현한 MemoryMemberRepository
public class MemoryMemberRepository implements MemberRepository {

    // 메모리 저장소 (Long: id, Member: Member 객체)
    private static Map&amp;lt;Long, Member&amp;gt; store = new HashMap&amp;lt;&amp;gt;();

    // 회원 id에 대한 자동 증가 값
    private static long sequence = 0L;

    // 회원을 저장하는 메서드
    @Override
    public Member save(Member member) {
        member.setId(++sequence);  // 자동으로 증가하는 id 설정
        store.put(member.getId(), member);  // store에 회원 저장
        return member;  // 저장된 회원 객체 반환
    }

    // id로 회원을 찾는 메서드
    @Override
    public Optional&amp;lt;Member&amp;gt; findById(Long id) {
        return Optional.ofNullable(store.get(id));  // id로 찾은 값을 Optional로 감싸서 반환
    }

    // 모든 회원을 반환하는 메서드
    @Override
    public List&amp;lt;Member&amp;gt; findAll() {
        return new ArrayList&amp;lt;&amp;gt;(store.values());  // store의 값을 리스트로 반환
    }

    // 이름으로 회원을 찾는 메서드
    @Override
    public Optional&amp;lt;Member&amp;gt; findByName(String name) {
        return store.values().stream()  // 저장된 모든 회원들에 대해
                .filter(member -&amp;gt; member.getName().equals(name))  // 이름이 일치하는 회원을 필터링
                .findAny();  // 일치하는 첫 번째 회원 반환 (없으면 Optional.empty() 반환)
    }

    public void clearStore() {
        store.clear();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;dtodata-transfer-object의-필요성과-예시-정리하기&quot; data-ke-size=&quot;size26&quot;&gt;DTO(Data Transfer Object)의 필요성과 예시 정리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;객체간 네트워크를 통한 데이터 전송&lt;/b&gt;을 목적&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 로직 포함 X&lt;/p&gt;
태그는 웹 브라우저에서 서버로 데이터를 전송할때 사용
&lt;p data-ke-size=&quot;size16&quot;&gt;ex. 게시판에 글 쓰고 전송 버튼 &amp;rarr; 글이 서버로 전송&lt;/p&gt;
태그에 컨트롤러가 객체에 담아 받음 &amp;rarr; 이 객체가 DTO &amp;rarr; DB에 저장
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; DTO를 엔티티로 변환&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 전송 최적화&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;필요한 데이터만&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;선택적으로 전송 (&lt;b&gt;네트워크&lt;/b&gt;나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;서버 리소스&lt;/b&gt;를 절약)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;계층 간 데이터 격리&lt;/b&gt;: 데이터베이스 모델(엔티티) 클라이언트 모델(뷰) 분리 ,&lt;b&gt;계층 간의 의존성&lt;/b&gt;을 줄이고, 변경 사항이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터베이스 모델&lt;/b&gt;에만 영향을 미치게 할 수 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안과 캡슐화&lt;/b&gt;:&lt;b&gt;민감한 데이터&lt;/b&gt;를 제외한 정보만을 전송&lt;/li&gt;
&lt;li&gt;&lt;b&gt;API 응답 구조 정의&lt;/b&gt;:&lt;b&gt;API 설계&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;시, 데이터 교환 형식을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;명확히 정의함. API 응답 구조&lt;/b&gt;를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;표준화&lt;/b&gt;하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;일관성&lt;/b&gt;을 유지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 향상&lt;/b&gt;: 데이터만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;선택적으로&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;전달함으로써&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;네트워크 비용&lt;/b&gt;과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;메모리 사용&lt;/b&gt;을 절약(메모리와 CPU 리소스, 데이터 처리 시간를 절약)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;public class MemberDTO {
    private Long id;  // 회원 ID
    private String name;  // 회원 이름

    // 기본 생성자
    public MemberDTO() {}

    // 생성자 (필드를 통해 초기화)
    public MemberDTO(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    // Getter &amp;amp; Setter
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return &quot;MemberDTO{id=&quot; + id + &quot;, name='&quot; + name + &quot;'}&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;model과-dto의-차이점&quot; data-ke-size=&quot;size23&quot;&gt;Model과 DTO의 차이점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Model &amp;rarr; 비즈니스 객체로서 사용될 가능성 있음&lt;/li&gt;
&lt;li&gt;DTO는 데이터만 담고, 어떤 비즈니스 로직을 처리하는 데 사용되지 않아야함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;의존성-주입di의-개념과-spring에서의-구현-방법-학습하기&quot; data-ke-size=&quot;size26&quot;&gt;의존성 주입(DI)의 개념과 Spring에서의 구현 방법 학습하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DI : Dependency Injection, 외부에서 만들어진 객체를 필요한 곳으로 가져오는 기법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 의존성 주입을 컨테이너(IOC Container)를 사용하여 자동으로 관리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IOC((Inversion of Control) 자동으로 관리&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체의 생성&lt;/li&gt;
&lt;li&gt;생명주기 관리&lt;/li&gt;
&lt;li&gt;의존성 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;di-필요성&quot; data-ke-size=&quot;size23&quot;&gt;DI 필요성&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;결합도 낮추기&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;객체가 다른 객체에 의존하는 방식을&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;유연하게 바꿀 수 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드의 유연성 향상&lt;/b&gt;:객체를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;유연하게 확장&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유지보수 용이성&lt;/b&gt;: 객체의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;변경이나 확장&lt;/b&gt;을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;자주 수정할 필요 없이&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;DI 컨테이너만 수정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 용이성&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;테스트용 객체(목 Mock)를 주입&lt;/b&gt;할 수 있어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;단위 테스트가능&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;di-종류&quot; data-ke-size=&quot;size23&quot;&gt;DI 종류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;생성자 주입 (Constructor Injection)&lt;/b&gt;: 의존성 객체를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;생성자를 통해 주입&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;public class UserService {

    private final UserRepository userRepository;

    // 생성자에서 의존성 주입
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void performService() {
        // userRepository를 사용한 비즈니스 로직
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;private final MemberRepository memberRepository = new
 MemoryMemberRepository();
 
 /* 의존성 주입 예시 - 회원가입 서비스 적용 */

private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세터 주입 (Setter Injection)&lt;/b&gt;: 의존성 객체를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;세터 메서드를 통해 주입&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;public class UserService {

    private UserRepository userRepository;

    // 세터 메서드를 통해 의존성 주입
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void performService() {
        // userRepository를 사용한 비즈니스 로직
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필드 주입 (Field Injection)&lt;/b&gt;: 의존성 객체를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;필드에 직접 주입,&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Autowired 어노테이션으로 의존성 주입을 할수있음&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;public class UserService {

    @Autowired
    private UserRepository userRepository;

    public void performService() {
        // userRepository를 사용한 비즈니스 로직
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;/* 회원 컨트롤러에 의존관계 추가 */ 

@Controller
 public class MemberController {
     private final MemberService memberService;
     @Autowired
     public MemberController(MemberService memberService) {
         this.memberService = memberService;
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;groovy&quot; style=&quot;background-color: #fbfcfd; color: #24292e; text-align: left;&quot;&gt;&lt;code&gt;package hello.hello_spring.service;

import org.springframework.stereotype.Service;

@Service
public class MemberService {
    // 서비스 로직 구현
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;-새롭게-알게-된-점--어려웠던-부분&quot; data-ke-size=&quot;size23&quot;&gt;  새롭게 알게 된 점 &amp;amp; 어려웠던 부분&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새롭게 알게된 점:&lt;br /&gt;MVC 구조와 CSR(Controller-Service-Repository) 계층을 통해 각 계층의 역할을 명확히 이해할 수 있게됨.&lt;br /&gt;특히, 서비스에서 예외 처리를 하는 것이 적절하다는 점과 아키텍처의 역할과 책임을 명확히 구분하는 것의 중요성을 알게됨.&lt;br /&gt;또한, 의존성 주입(DI)에 대해 다양한 주입 방법(생성자 주입, 세터 주입, 필드 주입)을 배우고, 각각의 장단점과 언제 적합한지에 대해 실습을 진행함.&lt;/li&gt;
&lt;li&gt;어려웠던 부분:&lt;br /&gt;의존성 주입(DI)에 대한 개념을 처음 접할 때 어려움을 느낌.&lt;br /&gt;DI가 다양한 형태로 사용되고, 상황에 따라 적합한 주입 방법을 선택해야 하는 점이 복잡하게 느껴짐.&lt;br /&gt;또한, 서비스와 컨트롤러 간의 책임 구분이 모호할 때가 많아 어떤 위치에서 예외 처리를 해야 할지 고민&lt;/li&gt;
&lt;li&gt;해결 방법:&lt;br /&gt;강의와 서적을 참고하면서 실습을 진행함.&lt;br /&gt;생성자 주입, 세터 주입, 필드 주입을 직접 사용해보며 각 방식의 특성을 파악함.&lt;br /&gt;예외 처리에 대해선 서비스 계층에서 처리하는 것이 적합함을 토론을 통해 의논함.&lt;br /&gt;이러한 실습과 정보를 참고하여 점차 개념이 확립되어감.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <author>myhousemouse</author>
      <guid isPermaLink="true">https://myhousemouse.tistory.com/1</guid>
      <comments>https://myhousemouse.tistory.com/1#entry1comment</comments>
      <pubDate>Sun, 8 Mar 2026 14:16:19 +0900</pubDate>
    </item>
  </channel>
</rss>