<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Archive_99</title>
    <link>https://arch2ve-99.tistory.com/</link>
    <description>천리길도 한걸음부터</description>
    <language>ko</language>
    <pubDate>Wed, 10 Jun 2026 18:25:27 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Archive_99</managingEditor>
    <image>
      <title>Archive_99</title>
      <url>https://tistory1.daumcdn.net/tistory/8579267/attach/6b8b57e1a1ea45309f50d32b481f5b60</url>
      <link>https://arch2ve-99.tistory.com</link>
    </image>
    <item>
      <title>[Spring Boot / 백엔드] AWS로 프로젝트 배포하기 - VPC, EC2, GitHub Actions</title>
      <link>https://arch2ve-99.tistory.com/17</link>
      <description>&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;백엔드 개발을 공부하다 보면 기능을 구현하는 것만큼이나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;만든 서비스를 실제로 어디에서 &lt;/b&gt;실행할지도 중요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot 프로젝트를 실행하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;localhost:8080에서 잘 동작하니까&lt;br /&gt;&amp;ldquo;이제 완성된 거 아닌가?&amp;rdquo;라고 생각할 수도 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 곰곰이 생각해보면&lt;br /&gt;내 컴퓨터에서만 실행되는 프로젝트는 아직&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;혼자만 볼 수 있는 프로그램&lt;/b&gt;&lt;/span&gt;에 가깝습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다른 사람이 접속할 수 있게 하려면&lt;br /&gt;&lt;span&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;서버가 필요하고&lt;/b&gt;&lt;/span&gt;,&lt;br /&gt;그 서버가 인터넷과 어떻게 연결되는지도 알아야 하고,&lt;br /&gt;코드를 수정할 때마다 매번 직접 올리는 대신&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;자동으로 배포되는 흐름&lt;/b&gt;&lt;/span&gt;도 필요하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이번 글에서는&amp;nbsp;&lt;br /&gt;&lt;span&gt;&lt;b&gt;클라우드 컴퓨팅이 무엇인지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;,&lt;br /&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;AWS에서 VPC와 EC2는 어떤 역할을 하는지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;,&lt;br /&gt;그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;GitHub Actions를 이용해 어떻게 CI/CD 파이프라인을 구성할 수 있는지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;를 정리해보려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서 다룰 내용은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라우드 컴퓨팅이란 무엇인가&lt;/li&gt;
&lt;li&gt;왜 AWS 같은 클라우드 서비스를 사용할까&lt;/li&gt;
&lt;li&gt;리전과 가용 영역은 무엇인가&lt;/li&gt;
&lt;li&gt;EC2와 VPC는 어떤 역할을 하는가&lt;/li&gt;
&lt;li&gt;Public 서브넷과 Private 서브넷은 어떻게 다른가&lt;/li&gt;
&lt;li&gt;Private 서브넷은 왜 필요한가&lt;/li&gt;
&lt;li&gt;CI/CD 파이프라인이란 무엇인가&lt;/li&gt;
&lt;li&gt;GitHub Actions와 Secrets는 어떻게 사용하는가&lt;/li&gt;
&lt;li&gt;Spring Boot 프로젝트를 AWS에 어떻게 배포할 수 있는가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;왜 배포를 배워야 할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지금까지는 스프링, JPA, 시큐리티, JWT, OAuth 같은 기술들을 이용해서&lt;br /&gt;기능이 동작하는 백엔드 애플리케이션을 만드는 데 집중했다면,&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;배포는 그 결과물을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;실제 서비스처럼 외부에 공개하는 단계&lt;/b&gt;&lt;/span&gt;라고 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 배포는 단순히 프로그램을 켜 두는 작업이 아니라&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;li&gt;코드를 올리고&lt;/li&gt;
&lt;li&gt;다시 수정했을 때 자동으로 반영되게 만드는&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전체 흐름을 포함합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 배포를 공부하다 보니&lt;br /&gt;백엔드 개발은 단순히 API만 만드는 일이 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;그 API가 실제로 돌아갈 환경까지 이해하는 일&lt;/b&gt;&lt;/span&gt;이라는 점이 더 크게 느껴졌습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;클라우드 컴퓨팅이란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클라우드 컴퓨팅은 쉽게 말하면&lt;br /&gt;&lt;span&gt;&lt;b&gt;다른 회사가 제공하는 서버, 스토리지, 네트워크 같은 컴퓨팅 자원을 빌려서 사용하는 방식&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예전에는 회사가 직접 서버를 사고, 직접 네트워크를 구성하고, 직접 운영하는 경우가 많았습니다.&lt;br /&gt;이런 방식을 온프레미스라고 부릅니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 이 방식은 초기 비용이 크고,&lt;br /&gt;프로젝트가 잘 안 되더라도 이미 들어간 서버 비용을 회수하기 어렵다는 문제가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반면 클라우드 컴퓨팅은 필요한 만큼만 자원을 빌려서 쓰고,&lt;br /&gt;규모가 커지면 늘리고 줄어들면 다시 줄일 수 있기 때문에 훨씬 유연합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 클라우드 컴퓨팅은 단순히 &amp;ldquo;남의 서버를 쓰는 것&amp;rdquo;이 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;필요한 인프라를 필요할 때 빠르게 확보할 수 있게 해주는 방식&lt;/b&gt;&lt;/span&gt;이라고 이해하면 편했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;왜 AWS 같은 클라우드 서비스를 사용할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클라우드 서비스를 사용하는 가장 큰 이유는 결국&lt;br /&gt;&lt;span&gt;&lt;b&gt;비용과 운영 부담을 줄일 수 있기 때문&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 프로젝트를 시작할 때마다 직접 서버를 준비해야 한다면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반면 AWS 같은 클라우드 서비스는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;li&gt;배포와 운영을 훨씬 유연하게 할 수 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 개인 프로젝트나 사이드 프로젝트 입장에서는&lt;br /&gt;처음부터 큰 서버를 운영하기보다&lt;br /&gt;&lt;span&gt;&lt;b&gt;작게 시작해서 필요할 때 늘릴 수 있다는 점&lt;/b&gt;&lt;/span&gt;이 꽤 큰 장점처럼 느껴졌습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;리전과 가용 영역은 무엇일까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;AWS를 처음 보면 리전(Region), 가용 영역(Availability Zone)이라는 말이 자주 나옵니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 저도 그냥 서버 위치 정도로만 생각했는데,&lt;br /&gt;정리해보니 생각보다 중요한 개념이었습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;리전(Region)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;리전은 AWS가 전 세계 여러 지역에 만들어둔&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;큰 단위의 물리적 인프라 위치&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 서울 리전, 도쿄 리전, 버지니아 리전처럼&lt;br /&gt;지리적으로 떨어진 여러 위치가 존재합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 리전을 나누는 이유는 단순합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 리전은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;AWS 자원이 모여 있는 큰 지역 단위&lt;/b&gt;&lt;/span&gt;&lt;span&gt;라고 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;가용 영역(Availability Zone)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가용 영역은 하나의 리전 안에 존재하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;서로 독립된 물리적 데이터 센터 단위&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서울 리전 안에도 여러 가용 영역이 있고,&lt;br /&gt;서로 물리적으로 분리되어 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하는 이유는 하나의 데이터 센터에 문제가 생겨도&lt;br /&gt;같은 리전 안의 다른 가용 영역을 사용할 수 있게 하기 위해서입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;라고 이해하면 훨씬 정리가 잘 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;EC2란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;AWS에서 가장 대표적으로 보는 서비스 중 하나가 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;EC2&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;EC2는 쉽게 말하면 &lt;span&gt;&lt;b&gt;AWS에서 빌려 쓰는 가상 서버&lt;/b&gt;&lt;/span&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우리가 Spring Boot 프로젝트를 배포할 때는&lt;br /&gt;결국 어딘가에서 자바 애플리케이션을 실행해야 하는데,&lt;br /&gt;그 실행 환경이 되는 컴퓨터 역할을 EC2가 해줍니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, EC2는 실제 물리 서버 그 자체라기보다&lt;br /&gt;AWS 위에서 실행되는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;가상 인스턴스&lt;/b&gt;&lt;/span&gt;라고 보는 것이 더 자연스럽습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;EC2를 만들 때는 보통 다음과 같은 것들을 정하게 됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 OS를 사용할지(AMI)&lt;/li&gt;
&lt;li&gt;어떤 성능의 인스턴스를 사용할지&lt;/li&gt;
&lt;li&gt;어느 VPC와 서브넷에 둘지&lt;/li&gt;
&lt;li&gt;어떤 보안 규칙을 적용할지&lt;/li&gt;
&lt;li&gt;얼마나 큰 스토리지를 붙일지&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, EC2를 만든다는 것은 단순히 서버 하나를 클릭해서 생성하는 것이 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;실행 환경, 네트워크, 보안까지 함께 구성하는 일&lt;/b&gt;&lt;/span&gt;이라고 느꼈습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;EC2 인스턴스 타입은 왜 중요할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;EC2는 종류가 다양합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어떤 인스턴스는 범용적으로 쓰기 좋고,&lt;br /&gt;어떤 인스턴스는 메모리나 GPU에 특화되어 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또 같은 계열 안에서도&lt;span&gt;&amp;nbsp;&lt;/span&gt;nano&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;micro&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;small&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;large&lt;span&gt;처럼&lt;br /&gt;크기가 다르게 나뉘어 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 의미는 결국 프로젝트 규모에 맞게 서버 자원을 조절할 수 있다는 것입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 처음에는 작은 인스턴스로 시작하다가&lt;br /&gt;트래픽이 많아지면 더 큰 인스턴스로 바꾸거나,&lt;br /&gt;필요하다면 여러 대로 늘리는 방식도 가능합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, EC2의 장점은 단순히 서버를 만들 수 있다는 것보다&lt;br /&gt;&lt;span&gt;&lt;b&gt;상황에 맞게 유연하게 조절할 수 있다는 점&lt;/b&gt;&lt;/span&gt;에 있다고 느꼈습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;VPC란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;EC2가 &amp;ldquo;서버&amp;rdquo;라면,&lt;br /&gt;VPC는 그 서버들이 들어갈&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;가상의 네트워크 공간&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;VPC는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;Virtual Private Cloud&lt;/b&gt;&lt;/span&gt;의 약자로,&lt;br /&gt;AWS 안에서 내가 사용하는 리소스들을 배치할 수 있는&lt;br /&gt;나만의 가상 네트워크라고 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쉽게 말하면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EC2는 그 안에 놓이는 컴퓨터&lt;/li&gt;
&lt;li&gt;VPC는 그 컴퓨터들이 연결되는 네트워크 공간&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 서버만 만드는 것으로 끝나는 것이 아니라&lt;br /&gt;그 서버가 어느 네트워크 안에 있을지까지 정해야&lt;br /&gt;비로소 외부 요청을 받고, 다른 리소스와 통신할 수 있게 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;서브넷은 왜 나눌까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;VPC 안에는 여러 개의 서브넷을 만들 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서브넷은 VPC를 더 작은 네트워크 단위로 나눈 것이라고 보면 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 나누는 이유는 모든 서버를 똑같이 외부에 노출시키지 않고,&lt;br /&gt;역할에 따라 위치를 구분하기 위해서입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 요청을 직접 받아야 하는 서버&lt;/li&gt;
&lt;li&gt;내부에서만 동작해야 하는 DB 서버&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;를 같은 위치에 두는 것은 위험할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 네트워크를 더 잘게 나눠 각 리소스의 역할에 따라 접근 범위를 다르게 설정하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 서브넷은 단순한 분할이 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;보안과 구조를 더 명확하게 만들기 위한 네트워크 분리 방식&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Public 서브넷과 Private 서브넷은 어떻게 다를까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서브넷은 크게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;Public 서브넷&lt;/b&gt;&lt;/span&gt;&lt;span&gt;과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;Private 서브넷&lt;/b&gt;&lt;/span&gt;&lt;span&gt;으로 나눌 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;Public 서브넷&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Public 서브넷은 인터넷 게이트웨이와 라우팅 테이블이 연결되어 있어서&lt;br /&gt;외부 인터넷과 직접 통신할 수 있는 서브넷입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 외부 사용자의 요청을 받아야 하는 EC2라면&lt;br /&gt;보통 Public 서브넷에 두게 됩니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;Private 서브넷&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Private 서브넷은 외부 인터넷과 직접 연결되지 않는 서브넷입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 외부 사용자가 바로 접근하면 안 되는 리소스,&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 DB 서버 같은 것들을 Private 서브넷에 두는 경우가 많습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;정리하면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Public 서브넷은 외부와 연결되는 공간&lt;/li&gt;
&lt;li&gt;Private 서브넷은 내부에서만 사용하는 공간&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이라고 볼 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Private 서브넷은 왜 필요할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 저도 &amp;ldquo;인터넷이 안 되면 불편한 거 아닌가?&amp;rdquo;라는 생각이 들었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 오히려 Private 서브넷의 핵심은&lt;br /&gt;&lt;span&gt;&lt;b&gt;외부에서 직접 접근할 수 없게 만드는 것 자체가 장점&lt;/b&gt;&lt;/span&gt;이라는 점이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Private 서브넷의 장점은 크게 두 가지로 볼 수 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;1. 보안성이 높다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;외부에서 직접 접근할 수 없기 때문에&lt;br /&gt;불필요한 공격 표면을 줄일 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 모든 서버를 다 인터넷에 노출시키는 것보다&lt;br /&gt;정말 외부와 연결돼야 하는 서버만 Public에 두고,&lt;br /&gt;나머지는 Private에 두는 것이 훨씬 안전합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;2. 내부 자원을 더 효율적으로 분리할 수 있다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DB처럼 외부 사용자가 직접 접근할 필요가 없는 리소스는&lt;br /&gt;Public에 둘 이유가 없습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;오히려 애플리케이션 서버만 Public에 두고&lt;br /&gt;DB는 Private에 두는 구조가 더 일반적입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, Private 서브넷은 &amp;ldquo;인터넷이 안 되는 불편한 공간&amp;rdquo;이 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;인터넷에 노출시키지 않기 위해 일부러 분리한 공간&lt;/b&gt;&lt;/span&gt;이라고 이해하는 것이 더 맞았습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;그럼 Private 서브넷의 서버는 어떻게 접속할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 부분이 처음에는 가장 헷갈렸습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;외부에서 직접 접근할 수 없다면&lt;br /&gt;Private 서브넷 안의 서버는 어떻게 관리할 수 있을까요?&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대표적으로 많이 사용하는 방식이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;Bastion Host&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Bastion Host는 Public 서브넷에 있는 서버를 거쳐서&lt;br /&gt;Private 서브넷 내부 서버에 접속하는 방식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;먼저 Public 서브넷의 EC2에 접속하고&lt;/li&gt;
&lt;li&gt;그 서버를 통해 Private 서브넷 내부로 들어가는 구조&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면 Private 서브넷 내부의 서버는 여전히 외부에 직접 노출되지 않으면서도&lt;br /&gt;필요할 때 관리자는 접근할 수 있게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, Bastion Host는 &lt;span&gt;&lt;b&gt;보안을 유지하면서도 내부 서버를 관리하기 위한 중간 접점&lt;/b&gt;&lt;/span&gt;이라고 볼 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;보안 그룹은 어떤 역할을 할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;AWS에서 네트워크를 구성할 때 빠질 수 없는 것이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;보안 그룹(Security Group)&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보안 그룹은 쉽게 말해 EC2에 적용하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;가상 방화벽&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 어떤 포트를 열어둘지, 어떤 요청은 허용하고 어떤 요청은 막을지를 정하는 규칙입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 Spring Boot 프로젝트를 직접 실행한다면 보통 이런 인바운드 규칙을 생각할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;SSH(22)&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 서버에 터미널로 접속하기 위해&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;MySQL(3306)&lt;/b&gt;&lt;/span&gt;: 외부에서 DB에 접속해야 하는 경우&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;TCP 8080&lt;/b&gt;&lt;/span&gt;: Spring Boot 애플리케이션 요청을 받기 위해&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 보안 그룹은 단순한 옵션이 아니라 &lt;span&gt;&lt;b&gt;이 서버가 어떤 요청을 받을 수 있을지 결정하는 핵심 설정&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;CI/CD 파이프라인이란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;배포를 수동으로 하다 보면&lt;br /&gt;코드를 수정할 때마다 직접 서버에 들어가서 파일을 복사하고,&lt;br /&gt;기존 프로세스를 끄고, 다시 실행하는 일을 반복하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 할 수 있어도 프로젝트가 커질수록 이런 방식은 꽤 번거롭고 실수하기도 쉽습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 등장하는 것이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;CI/CD 파이프라인&lt;/b&gt;&lt;/span&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;CI(Continuous Integration)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CI는 개발자가 작업한 코드를 자주 통합하고,&lt;br /&gt;그 코드가 정상적으로 빌드되는지, 테스트는 통과하는지 확인하는 과정입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 PR을 만들거나 main 브랜치에 머지할 때&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자동으로 빌드가 실행되는 것이 CI 흐름에 가깝습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;CD(Continuous Delivery / Deployment)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CD는 빌드된 결과물을 실제 실행 환경에 배포하는 과정입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 빌드까지 끝난 애플리케이션을 실제 서버에 전달하고 실행되게 만드는 단계입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;정리하면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CI는 코드가 잘 합쳐지고 잘 빌드되는지 확인하는 과정&lt;/li&gt;
&lt;li&gt;CD는 그 결과물을 실제 서버에 배포하는 과정&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이라고 볼 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;GitHub Actions는 무엇일까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;GitHub Actions는 GitHub에서 제공하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;CI/CD 자동화 도구&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 저도 그냥 &amp;ldquo;GitHub에 있는 기능&amp;rdquo; 정도로만 생각했는데, 정리해보니 꽤 구조적으로 동작합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;주요 개념은 다음처럼 볼 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Event&lt;/b&gt;&lt;/span&gt;: push, pull request 같은 깃허브에서 발생한 이벤트&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Workflow&lt;/b&gt;&lt;/span&gt;: 이벤트가 발생했을 때 실행할 전체 작업 흐름&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Job&lt;/b&gt;&lt;/span&gt;: 워크플로우 안에서 수행되는 작업 단위&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Step&lt;/b&gt;&lt;/span&gt;: 각 Job 안에서 실행되는 세부 명령&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, GitHub Actions는&lt;br /&gt;&amp;ldquo;특정 이벤트가 발생했을 때, 정해둔 순서대로 자동 작업을 실행해주는 도구&amp;rdquo;라고 이해하면 편했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;GitHub Actions Secrets는 왜 필요할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;배포 자동화를 하다 보면&lt;br /&gt;SSH 키, 서버 주소, 환경 변수 같은 민감한 정보를 워크플로우에서 사용해야 할 때가 많습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 이런 값을 워크플로우 파일에 그대로 적어두면&lt;br /&gt;저장소에 노출될 위험이 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 사용하는 것이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;GitHub Actions Secrets&lt;/b&gt;&lt;/span&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Secrets는 깃허브 저장소 안에서&lt;br /&gt;민감한 값을 안전하게 저장해두고,&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;워크플로우 실행 시점에만 꺼내 쓸 수 있게 해주는 기능입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 이런 값들을 Secrets로 넣을 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EC2_HOST&lt;/li&gt;
&lt;li&gt;EC2_USERNAME&lt;/li&gt;
&lt;li&gt;EC2_SSH_KEY&lt;/li&gt;
&lt;li&gt;ENV&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, Secrets는&lt;br /&gt;&lt;span&gt;&lt;b&gt;배포 자동화 과정에서 민감한 정보를 코드에 직접 노출하지 않기 위한 필수 장치&lt;/b&gt;&lt;/span&gt;라고 할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;배포 흐름은 어떻게 구성할 수 있을까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전체 배포 과정은 크게 이렇게 정리할 수 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;1. VPC 생성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 네트워크 공간을 만들고,&lt;br /&gt;퍼블릭/프라이빗 서브넷, 라우팅 테이블, 인터넷 게이트웨이 등을 구성합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;2. 보안 그룹 생성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SSH, 8080 포트 같은 필요한 인바운드 규칙을 설정합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;3. EC2 생성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;VPC와 Public 서브넷을 선택하고, 키 페어와 보안 그룹을 연결해서 EC2를 생성합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;4. EC2 초기 설정&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SSH로 접속한 뒤 Java, MySQL 등을 설치하고,&lt;br /&gt;필요하다면 스왑 메모리도 설정합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;5. GitHub Secrets 설정&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서버 접속 정보와 환경 변수를 GitHub Secrets에 등록합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;6. GitHub Actions 워크플로우 작성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;main 브랜치에 push되면 자동으로 빌드하고,&lt;br /&gt;빌드 결과물을 EC2에 전송한 뒤 서버에서 실행되도록 구성합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 배포는 단순히 jar 파일만 복사하는 작업이 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;네트워크, 보안, 서버 환경, 자동화까지 이어지는 전체 흐름&lt;/b&gt;&lt;/span&gt;이라고 볼 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;EC2에서는 무엇을 설치해야 할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot 프로젝트를 실행하려면&lt;br /&gt;당연히 자바 런타임이 필요합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 Java 21을 사용한 프로젝트라면&lt;br /&gt;EC2에도 같은 버전의 JDK를 설치해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 MySQL을 EC2 내부에서 함께 운영한다면&lt;br /&gt;MySQL도 설치해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또 프리티어처럼 메모리가 작은 인스턴스를 쓴다면&lt;br /&gt;스왑 메모리를 설정해서 메모리 부족 상황을 조금 보완할 수도 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, EC2를 만든다고 끝나는 것이 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;내 프로젝트가 실제로 실행될 수 있는 환경으로 세팅하는 단계&lt;/b&gt;&lt;/span&gt;가 추가로 필요합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;GitHub Actions 워크플로우는 어떤 식으로 작성할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;워크북에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;.github/workflows&lt;span&gt;&amp;nbsp;&lt;/span&gt;아래에 배포 워크플로우를 두고,&lt;br /&gt;main&lt;span&gt;&amp;nbsp;&lt;/span&gt;브랜치에 push가 발생했을 때 자동으로 빌드와 배포가 이뤄지도록 구성했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예시 흐름은 대략 이런 식입니다.&lt;/p&gt;
&lt;pre class=&quot;http&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;name: CI/CD Pipeline (.jar)

on:
  push:
    branches: [ &quot;main&quot; ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: main 체크아웃
        uses: actions/checkout@v4

      - name: 자바 설정
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '21'

      - name: gradlew 실행 권한 부여
        run: chmod +x gradlew

      - name: gradle 세팅
        uses: gradle/actions/setup-gradle@v4

      - name: gradle 빌드
        run: ./gradlew build -x test

      - name: 빌드된 아티팩트 업로드
        uses: actions/upload-artifact@v4
        with:
          name: BackEnd
          path: build/libs/*.jar

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: 아티팩트 다운로드
        uses: actions/download-artifact@v4
        with:
          name: BackEnd
          path: build/libs/&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 흐름을 보면 먼저 build Job에서 프로젝트를 빌드하고,&lt;br /&gt;그 결과물을 아티팩트로 저장한 뒤,&lt;br /&gt;deploy Job에서 다시 다운로드해서 서버에 전송하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 워크플로우도 그냥 한 번에 다 처리하는 것이 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;빌드 단계와 배포 단계를 분리해서 더 명확하게 구성할 수 있다&lt;/b&gt;&lt;/span&gt;는 점이 인상적이었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;배포 단계에서는 실제로 무엇을 할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;deploy 단계에서는 보통 다음 작업이 필요합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SSH 키 파일 생성&lt;/li&gt;
&lt;li&gt;jar 파일 찾기&lt;/li&gt;
&lt;li&gt;scp&lt;span&gt;로 EC2에 jar 업로드&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;기존 자바 프로세스 종료&lt;/li&gt;
&lt;li&gt;새 jar 실행&lt;/li&gt;
&lt;li&gt;민감한 파일 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 워크북에서는 이런 흐름으로 구성했습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;- name: EC2 배포
  env:
    EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }}
    EC2_USERNAME: ${{ secrets.EC2_USERNAME }}
    EC2_HOST: ${{ secrets.EC2_HOST }}
    ENV: ${{ secrets.ENV }}
  run: |
    echo &quot;$EC2_SSH_KEY&quot; &amp;gt; private_key.pem
    chmod 600 private_key.pem
    jar_file=$(find build/libs -name '*.jar' ! -name '*plain.jar' | head -n 1)
    scp -i private_key.pem -o StrictHostKeyChecking=no &quot;$jar_file&quot; $EC2_USERNAME@$EC2_HOST:/home/$EC2_USERNAME/BackEnd.jar
    echo &quot;$ENV&quot; | ssh -i private_key.pem -o StrictHostKeyChecking=no $EC2_USERNAME@$EC2_HOST &quot;
      cat &amp;gt; .env
      pgrep java | xargs -r kill -15
      sleep 10
      nohup java -jar /home/$EC2_USERNAME/BackEnd.jar &amp;gt; app.log 2&amp;gt;&amp;amp;1 &amp;amp;
    &quot;
    rm -f private_key.pem&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 흐름을 보면서 느낀 점은&lt;br /&gt;배포 자동화란 결국 &amp;ldquo;사람이 하던 작업을 순서대로 스크립트로 옮기는 것&amp;rdquo;에 가깝다는 점이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 수동으로 서버에 접속해서 하던 일을&lt;br /&gt;GitHub Actions가 대신 수행하도록 만든 것이 CI/CD 파이프라인이라고 볼 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;.env 파일은 왜 사용할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;배포할 때 환경 변수 처리는 꽤 중요합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 DB 비밀번호, JWT 시크릿, OAuth 키 같은 값들은&lt;br /&gt;소스 코드에 직접 넣어두면 위험할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 .env&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일을 이용해 민감한 값을 서버에 전달하고,&lt;br /&gt;Spring Boot가 이를 읽어오도록 구성했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면&lt;span&gt;&amp;nbsp;&lt;/span&gt;application.yml에 아래처럼 import 설정을 둘 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;spring:
  config:
    import: optional:file:.env[.properties]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 해두면&lt;span&gt;&amp;nbsp;&lt;/span&gt;.env&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일에 들어 있는 값을&lt;br /&gt;애플리케이션 실행 시 불러올 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 환경 변수 분리는 단순히 깔끔함의 문제가 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;민감한 정보를 코드와 분리해서 더 안전하게 관리하기 위한 방식&lt;/b&gt;&lt;/span&gt;이라고 느꼈습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;배포를 해보면서 느낀 점&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 내용을 정리하면서 느낀 것은&lt;br /&gt;배포는 단순히 &amp;ldquo;AWS에 올렸다&amp;rdquo;로 끝나는 개념이 아니라는 점이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 안에는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;li&gt;환경 변수는 어떻게 숨길지&lt;/li&gt;
&lt;li&gt;배포를 어떻게 자동화할지&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;같은 선택들이 전부 들어 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 VPC, EC2, Public/Private 서브넷 개념을 같이 보면서&lt;br /&gt;서버를 만든다는 것이 단순히 컴퓨터 한 대를 띄우는 것이 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;그 서버가 어떤 네트워크 구조 안에서 어떤 역할을 할지 정하는 일&lt;/b&gt;&lt;/span&gt;이라는 점이 더 크게 와닿았습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또 GitHub Actions를 이용해 CI/CD를 구성해보면서는&lt;br /&gt;코드를 올렸을 때 자동으로 빌드되고 배포되는 흐름이&lt;br /&gt;생각보다 강력한 개발 생산성 도구라는 점도 느낄 수 있었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 저도 배포를&lt;br /&gt;&amp;ldquo;프로젝트를 AWS에 올리는 것&amp;rdquo; 정도로만 생각했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 정리해보니 배포는&lt;br /&gt;단순히 jar 파일을 서버에 복사하는 작업이 아니라&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;li&gt;보안을 설정하고&lt;/li&gt;
&lt;li&gt;자동화된 배포 파이프라인까지 연결하는&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;꽤 넓은 영역의 작업이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 이번 내용을 공부하면서&lt;br /&gt;VPC와 EC2가 각각 어떤 역할을 하는지,&lt;br /&gt;왜 Public 서브넷과 Private 서브넷을 나누는지,&lt;br /&gt;GitHub Actions와 Secrets를 왜 함께 써야 하는지가 조금 더 분명해졌습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞으로 배포를 할 때는 단순히 &amp;ldquo;실행만 되면 된다&amp;rdquo;가 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;이 서버는 어디에 놓여야 할까&lt;/b&gt;&lt;/span&gt;&lt;span&gt;,&lt;br /&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;어떤 요청만 허용해야 할까&lt;/b&gt;&lt;/span&gt;&lt;span&gt;,&lt;br /&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;환경 변수는 어떻게 안전하게 관리할까&lt;/b&gt;&lt;/span&gt;&lt;span&gt;,&lt;br /&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;배포는 어떻게 반복 가능하게 자동화할까&lt;/b&gt;&lt;/span&gt;&lt;span&gt;를 함께 고민해야겠다고 느꼈습니다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>백엔드</category>
      <category>AWS</category>
      <category>springboot</category>
      <category>배포</category>
      <author>Archive_99</author>
      <guid isPermaLink="true">https://arch2ve-99.tistory.com/17</guid>
      <comments>https://arch2ve-99.tistory.com/17#entry17comment</comments>
      <pubDate>Sun, 31 May 2026 01:20:52 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot / 백엔드] JWT와 OAuth 이해하기 - 토큰 기반 인증, 카카오 로그인</title>
      <link>https://arch2ve-99.tistory.com/16</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 개발을 공부하다 보면 로그인 기능을 구현하는 것에서 끝나지 않고,&lt;br /&gt;&lt;b&gt;로그인 상태를 어떻게 유지할지&lt;/b&gt;,&lt;br /&gt;&lt;b&gt;인증된 사용자를 어떻게 식별할지&lt;/b&gt;,&lt;br /&gt;&lt;b&gt;소셜 로그인은 어떤 흐름으로 동작하는지&lt;/b&gt;까지 자연스럽게 궁금해지게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;br /&gt;로그인 이후의 인증 상태를 서버가 기억하는 방식도 있고,&lt;br /&gt;토큰을 이용해서 stateless하게 처리하는 방식도 있다는 점이 꽤 중요하게 느껴졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 실제 서비스에서는&lt;br /&gt;모바일 앱, 프론트엔드와 백엔드 분리 구조, REST API 기반 환경이 많다 보니&lt;br /&gt;세션 기반 로그인보다 &lt;b&gt;JWT 같은 토큰 기반 인증&lt;/b&gt;을 더 자주 보게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 글에서는 노션 내용을 바탕으로&lt;br /&gt;&lt;b&gt;세션 기반 인증과 토큰 기반 인증의 차이&lt;/b&gt;, &lt;b&gt;JWT를 스프링 시큐리티에서 어떻게 적용하는지&lt;/b&gt;,&lt;br /&gt;그리고 &lt;b&gt;OAuth를 이용한 카카오 로그인은 어떤 흐름으로 동작하는지&lt;/b&gt;를&lt;br /&gt;조금 더 &lt;b&gt;구체적인 코드와 함께&lt;/b&gt; 정리해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;세션 기반 인증과 토큰 기반 인증의 차이&lt;/li&gt;
&lt;li&gt;JWT란 무엇인가&lt;/li&gt;
&lt;li&gt;JwtUtil은 어떤 역할을 하는가&lt;/li&gt;
&lt;li&gt;JwtAuthFilter는 어떻게 인증을 처리하는가&lt;/li&gt;
&lt;li&gt;SecurityConfig에 JWT 필터를 어떻게 등록하는가&lt;/li&gt;
&lt;li&gt;@AuthenticationPrincipal로 현재 로그인 사용자를 어떻게 꺼내는가&lt;/li&gt;
&lt;li&gt;OAuth와 카카오 로그인은 어떤 흐름으로 동작하는가&lt;/li&gt;
&lt;li&gt;CustomOAuthService, OAuthSuccessHandler는 왜 필요한가&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;b&gt;세션 기반 인증과 토큰 기반 인증은 뭐가 다를까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 방식을 이해할 때 가장 먼저 정리해야 하는 것이&lt;br /&gt;&lt;b&gt;서버가 사용자의 로그인 상태를 어디에 저장하느냐&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;589&quot; data-origin-height=&quot;495&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzDcMM/dJMcacQKj9C/Y0CwRb9EuPCL6ssnbhG6xK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzDcMM/dJMcacQKj9C/Y0CwRb9EuPCL6ssnbhG6xK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzDcMM/dJMcacQKj9C/Y0CwRb9EuPCL6ssnbhG6xK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzDcMM%2FdJMcacQKj9C%2FY0CwRb9EuPCL6ssnbhG6xK%2Fimg.jpg&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;589&quot; height=&quot;495&quot; data-origin-width=&quot;589&quot; data-origin-height=&quot;495&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;세션 기반 인증&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 기반 인증은 사용자가 로그인하면&lt;br /&gt;&lt;b&gt;서버가 세션을 만들고, 그 세션에 사용자 정보를 저장하는 방식&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 보통 쿠키에 JSESSIONID를 저장하고, 이후 요청마다 해당 쿠키를 함께 보냅니다.&lt;br /&gt;서버는 이 세션 ID를 보고 &amp;ldquo;이 요청은 로그인한 사용자 요청이구나&amp;rdquo;라고 판단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 핵심은 &lt;b&gt;서버가 로그인 상태를 기억하고 있는 방식&lt;/b&gt;이라는 점입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;토큰 기반 인증&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 토큰 기반 인증은 로그인 시 서버가 토큰을 발급하고, 클라이언트가 이 토큰을 저장해두었다가&lt;br /&gt;이후 요청마다 Authorization 헤더에 담아 보내는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 많이 사용하는 방식이 JWT입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 토큰 기반 인증의 핵심은 &lt;b&gt;서버가 사용자 상태를 따로 저장하지 않고, 토큰 자체를 검증해서 인증하는 방식&lt;/b&gt;이라는 점입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 최근에는 JWT를 많이 사용할까?&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;반면 JWT는 &lt;b&gt;stateless&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 REST API 기반 서비스에서는 브라우저가 아닌 앱이나 외부 클라이언트도 함께 쓰는 경우가 많기 때문에&lt;br /&gt;세션보다 JWT 방식이 더 잘 어울리는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번에는 토큰 기반 인증 중에서도 가장 자주 보게 되는 &lt;b&gt;JWT 인증 방식&lt;/b&gt;을 중심으로 정리해보려고 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;JWT란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 &lt;b&gt;Json Web Token&lt;/b&gt;의 약자입니다.&lt;/p&gt;
&lt;p 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;540&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cokakR/dJMcaf0ZFQZ/q1Rj3JKYcK06K1kcZcKUhk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cokakR/dJMcaf0ZFQZ/q1Rj3JKYcK06K1kcZcKUhk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cokakR/dJMcaf0ZFQZ/q1Rj3JKYcK06K1kcZcKUhk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcokakR%2FdJMcaf0ZFQZ%2Fq1Rj3JKYcK06K1kcZcKUhk%2Fimg.jpg&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;540&quot; height=&quot;486&quot; data-origin-width=&quot;540&quot; data-origin-height=&quot;486&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 보통 다음 3부분으로 이루어집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Header&lt;/li&gt;
&lt;li&gt;Payload&lt;/li&gt;
&lt;li&gt;Signature&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 세 부분이 . 으로 이어진 문자열 형태가 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Header&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰 타입과 서명 알고리즘 정보가 들어갑니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Payload&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 식별자, 권한, 만료 시간 등이 들어갑니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Signature&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버의 시크릿 키로 서명한 값입니다. 이 값을 통해 토큰 위조 여부를 검증합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 JWT는 암호화된 값이 아니라 &lt;b&gt;인코딩된 값&lt;/b&gt;이라는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Payload는 누구나 디코딩해서 볼 수 있기 때문에 비밀번호 같은 민감한 정보는 넣지 않는 것이 원칙입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;전체 흐름 한눈에 보기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름을 다시보면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;389&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXb3sE/dJMcadhQZSw/kpqw63y5iayW6uo1w7YGdk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXb3sE/dJMcadhQZSw/kpqw63y5iayW6uo1w7YGdk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXb3sE/dJMcadhQZSw/kpqw63y5iayW6uo1w7YGdk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXb3sE%2FdJMcadhQZSw%2Fkpqw63y5iayW6uo1w7YGdk%2Fimg.jpg&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;389&quot; height=&quot;486&quot; data-origin-width=&quot;389&quot; data-origin-height=&quot;486&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;JWT 관련 코드는 어떻게 나눌까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 관련 코드를 보통 이런 식으로 나누는 편입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;513&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dS77Mi/dJMcagr5Kzo/KyWisbhbpXY5rRHjKBu0qk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dS77Mi/dJMcagr5Kzo/KyWisbhbpXY5rRHjKBu0qk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dS77Mi/dJMcagr5Kzo/KyWisbhbpXY5rRHjKBu0qk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdS77Mi%2FdJMcagr5Kzo%2FKyWisbhbpXY5rRHjKBu0qk%2Fimg.jpg&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;433&quot; height=&quot;513&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;513&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;
&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;JwtUtil: 토큰 생성, 파싱, 검증 담당&lt;/li&gt;
&lt;li&gt;JwtAuthFilter: 요청에서 JWT를 꺼내 인증 처리 담당&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;JWT를 다루는 공통 로직은 util&lt;/b&gt;, &lt;b&gt;요청 흐름에 끼어드는 인증 처리는 filter&lt;/b&gt; 로 분리한다고 이해하면 편합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;JwtUtil은 어떤 역할을 할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT를 사용할 때 가장 먼저 필요한 것은 &lt;b&gt;토큰을 생성하고, 읽고, 검증하는 클래스&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노션의 흐름대로 보면 JwtUtil은 이런 역할을 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Access Token 생성&lt;/li&gt;
&lt;li&gt;토큰에서 이메일 꺼내기&lt;/li&gt;
&lt;li&gt;토큰 유효성 검증&lt;/li&gt;
&lt;li&gt;시크릿 키로 서명 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 코드입니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
public class JwtUtil {

    private final SecretKey secretKey;
    private final Duration accessExpiration;

    public JwtUtil(
            @Value(&quot;${jwt.token.secretKey}&quot;) String secret,
            @Value(&quot;${jwt.token.expiration.access}&quot;) Long accessExpiration
    ) {
        this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
        this.accessExpiration = Duration.ofMillis(accessExpiration);
    }

    public String createAccessToken(AuthMember member) {
        return createToken(member, accessExpiration);
    }

    public String getEmail(String token) {
        try {
            return getClaims(token).getPayload().getSubject();
        } catch (JwtException e) {
            return null;
        }
    }

    public boolean isValid(String token) {
        try {
            getClaims(token);
            return true;
        } catch (JwtException e) {
            return false;
        }
    }

    private String createToken(AuthMember member, Duration expiration) {
        Instant now = Instant.now();

        String authorities = member.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(&quot;,&quot;));

        return Jwts.builder()
                .subject(member.getUsername())
                .claim(&quot;role&quot;, authorities)
                .claim(&quot;email&quot;, member.getUsername())
                .issuedAt(Date.from(now))
                .expiration(Date.from(now.plus(expiration)))
                .signWith(secretKey)
                .compact();
    }

    private Jws&amp;lt;Claims&amp;gt; getClaims(String token) throws JwtException {
        return Jwts.parser()
                .verifyWith(secretKey)
                .clockSkewSeconds(60)
                .build()
                .parseSignedClaims(token);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;이 코드는 왜 이렇게 작성할까?&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 생성자에서 secretKey를 만든다&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yml에 설정한 문자열 시크릿 키를 실제 JWT 서명에 사용할 SecretKey 객체로 바꾸는 부분입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. createAccessToken()은 실제 발급 메서드다&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public String createAccessToken(AuthMember member) {
    return createToken(member, accessExpiration);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 구현은 createToken() 내부에서 처리하지만, 외부에는 &amp;ldquo;이건 Access Token 생성용 메서드다&amp;rdquo;라고 의도를 분명하게 보여줄 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. subject와 claim에 사용자 정보를 담는다&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.subject(member.getUsername())
.claim(&quot;role&quot;, authorities)
.claim(&quot;email&quot;, member.getUsername())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 subject는 JWT의 대표 식별값입니다.&lt;br /&gt;보통 이메일이나 사용자 ID를 넣는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 role, email 같은 정보는 claim으로 추가 저장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 토큰 하나만으로도 &amp;ldquo;누구인지&amp;rdquo;, &amp;ldquo;어떤 권한이 있는지&amp;rdquo;를 표현할 수 있게 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. expiration으로 만료 시간을 둔다&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.expiration(Date.from(now.plus(expiration)))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 탈취되면 위험하기 때문에 만료 시간이 꼭 필요합니다.&lt;br /&gt;이 코드에서는 현재 시간 기준으로 accessExpiration만큼 뒤를 만료 시각으로 지정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이 토큰은 영원히 쓰이는 것이 아니라 &lt;b&gt;정해진 시간까지만 유효&lt;/b&gt;하게 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. signWith(secretKey)로 위조를 막는다&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;.signWith(secretKey)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 JWT에서 가장 중요합니다.&lt;br /&gt;단순히 JSON 문자열을 토큰처럼 꾸며도, 서명이 올바르지 않으면 서버는 신뢰하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, JWT를 믿는 이유는 Payload 때문이 아니라 &lt;b&gt;서명이 우리 서버의 시크릿 키로 생성되었는지 검증할 수 있기 때문&lt;/b&gt;입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6. getClaims()가 실제 검증을 수행한다&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;return Jwts.parser()
        .verifyWith(secretKey)
        .clockSkewSeconds(60)
        .build()
        .parseSignedClaims(token);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 토큰 문자열을 파싱하고,&lt;br /&gt;서명과 만료 시간 등을 검증하는 핵심 로직입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;토큰이 만료됐거나&lt;/li&gt;
&lt;li&gt;시그니처가 틀렸거나&lt;/li&gt;
&lt;li&gt;형식이 잘못됐거나&lt;/li&gt;
&lt;li&gt;조작된 토큰일 가능성&lt;/li&gt;
&lt;/ul&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;b&gt;JwtAuthFilter는 어떤 역할을 할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JwtUtil이 토큰 자체를 다루는 유틸이라면, JwtAuthFilter는 &lt;b&gt;실제 요청이 들어왔을 때 인증을 처리하는 필터&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 사용자가 API 요청을 보냈을 때&lt;br /&gt;헤더에 JWT가 있는지 확인하고, 유효하다면 인증 객체를 만들어 SecurityContextHolder에 넣는 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 코드입니다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final CustomUserDetailsService customUserDetailsService;

    @Override
    protected void doFilterInternal(
            @NonNull HttpServletRequest request,
            @NonNull HttpServletResponse response,
            @NonNull FilterChain filterChain
    ) throws ServletException, IOException {

        try {
            String token = request.getHeader(&quot;Authorization&quot;);

            if (token == null || !token.startsWith(&quot;Bearer &quot;)) {
                filterChain.doFilter(request, response);
                return;
            }

            token = token.replace(&quot;Bearer &quot;, &quot;&quot;);

            if (jwtUtil.isValid(token)) {
                String email = jwtUtil.getEmail(token);

                UserDetails user = customUserDetailsService.loadUserByUsername(email);

                Authentication auth = new UsernamePasswordAuthenticationToken(
                        user,
                        null,
                        user.getAuthorities()
                );

                SecurityContextHolder.getContext().setAuthentication(auth);
            }

            filterChain.doFilter(request, response);

        } catch (Exception e) {
            ObjectMapper mapper = new ObjectMapper();
            BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED;

            response.setContentType(&quot;application/json;charset=UTF-8&quot;);
            response.setStatus(code.getStatus().value());

            ApiResponse&amp;lt;Void&amp;gt; errorResponse = ApiResponse.onFailure(code, null);
            mapper.writeValue(response.getOutputStream(), errorResponse);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;이 필터는 요청을 어떻게 처리할까?&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. Authorization 헤더를 읽는다&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;String token = request.getHeader(&quot;Authorization&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 JWT는 아래처럼 전송합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;Authorization: Bearer {JWT_TOKEN}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 먼저 헤더에서 Authorization 값을 꺼냅니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. Bearer 형식이 아니면 그냥 통과시킨다&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;if (token == null || !token.startsWith(&quot;Bearer &quot;)) {
    filterChain.doFilter(request, response);
    return;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 중요한 이유는 모든 요청이 JWT를 필요로 하는 것은 아니기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 로그인 API나 회원가입 API는 토큰 없이 접근할 수 있어야 합니다.&lt;br /&gt;그래서 헤더가 없으면 바로 막는 것이 아니라, 그냥 다음 필터나 컨트롤러로 넘깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, JWT가 없는 요청을 무조건 거부하는 것이 아니라&lt;br /&gt;&lt;b&gt;보안 설정상 인증이 필요한 API에서만 나중에 걸러지게 만드는 구조&lt;/b&gt;입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. Bearer 접두어를 제거한다&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;token = token.replace(&quot;Bearer &quot;, &quot;&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 JWT 문자열만 남기기 위해 Bearer 부분을 제거합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 토큰이 유효하면 이메일을 꺼낸다&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;if (jwtUtil.isValid(token)) {
    String email = jwtUtil.getEmail(token);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;i&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sValid()에서 서명, 만료 시간 등을 검증하고,&lt;br /&gt;유효하다면 getEmail()로 사용자 식별값을 꺼냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;토큰이 신뢰 가능한지 먼저 확인하고, 그다음 사용자 정보를 해석하는 흐름&lt;/b&gt;입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. DB에서 사용자 정보를 다시 조회한다&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;UserDetails user = customUserDetailsService.loadUserByUsername(email);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &amp;ldquo;토큰 안에 정보가 있는데 왜 DB를 다시 조회하지?&amp;rdquo;라는 궁금증이 생길 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 현재 시점의 사용자 권한이나 상태를 DB 기준으로 다시 확인하기 위해서입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자가 탈퇴했거나 권한이 바뀌었을 수도 있으니, 토큰 정보만 100% 믿기보다 DB에서 다시 확인하는 방식이 더 안전합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6. 인증 객체를 만들어 SecurityContext에 저장한다&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Authentication auth = new UsernamePasswordAuthenticationToken(
        user,
        null,
        user.getAuthorities()
);

SecurityContextHolder.getContext().setAuthentication(auth);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 시큐리티는 SecurityContextHolder 안에 인증 객체가 들어 있으면&lt;br /&gt;&amp;ldquo;이 요청은 인증된 사용자 요청이구나&amp;rdquo;라고 판단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 필터에서 JWT를 검증한 뒤 그 결과를 스프링 시큐리티가 이해할 수 있는 Authentication 객체로 바꿔 넣는 것입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;예외 처리는 왜 필요할까?&lt;/b&gt;&lt;/h2&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;JWT 파싱 실패&lt;/li&gt;
&lt;li&gt;JWT 만료&lt;/li&gt;
&lt;li&gt;JWT Signature 검증 실패&lt;/li&gt;
&lt;li&gt;잘못된 형식의 JWT&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 catch 문에서 일관된 JSON 에러 응답을 내려주는 것이 중요합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;response.setContentType(&quot;application/json;charset=UTF-8&quot;);
response.setStatus(code.getStatus().value());

ApiResponse&amp;lt;Void&amp;gt; errorResponse = ApiResponse.onFailure(code, null);
mapper.writeValue(response.getOutputStream(), errorResponse);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 토큰 관련 오류가 났을 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML 로그인 페이지가 아니라 우리가 원하는 API 응답 형식으로 401 Unauthorized를 내려줄 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;SecurityConfig에는 JWT 필터를 어떻게 등록할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 필터를 만들었다면&lt;br /&gt;이제 실제 스프링 시큐리티 필터 체인에 등록해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 식입니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtUtil jwtUtil;
    private final CustomUserDetailsService customUserDetailsService;

    @Bean
    public JwtAuthFilter jwtAuthFilter() {
        return new JwtAuthFilter(jwtUtil, customUserDetailsService);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -&amp;gt; csrf.disable())
                .authorizeHttpRequests(auth -&amp;gt; auth
                        .requestMatchers(&quot;/auth/**&quot;, &quot;/swagger-ui/**&quot;, &quot;/v3/api-docs/**&quot;).permitAll()
                        .anyRequest().authenticated()
                )
                .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;여기서 중요한 부분은 뭘까?&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. JwtAuthFilter를 Bean으로 등록한다&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Bean
public JwtAuthFilter jwtAuthFilter() {
    return new JwtAuthFilter(jwtUtil, customUserDetailsService);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터도 결국 의존성이 필요합니다.&lt;br /&gt;JwtUtil, CustomUserDetailsService를 주입해서 스프링이 관리하는 Bean 형태로 등록하는 것입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. addFilterBefore()로 필터 체인에 끼워 넣는다&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 만든 JWT 필터를 기존 스프링 시큐리티 필터보다 앞에 두어서,&lt;br /&gt;컨트롤러로 가기 전에 먼저 JWT 인증을 시도하게 만드는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;JWT 인증은 컨트롤러에서 처리하는 것이 아니라 필터 체인에서 처리한다&lt;/b&gt;는 점이 중요합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;로그인 API는 어떻게 JWT를 발급할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 기반 인증에서는 로그인 성공 후 서버가 JWT를 발급해서 응답으로 내려줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 식으로 구성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/auth&quot;)
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JwtUtil jwtUtil;
    private final CustomUserDetailsService customUserDetailsService;

    @PostMapping(&quot;/login&quot;)
    public ApiResponse&amp;lt;LoginResponse&amp;gt; login(@RequestBody LoginRequest request) {
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        request.getEmail(),
                        request.getPassword()
                )
        );

        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        AuthMember authMember = (AuthMember) userDetails;

        String accessToken = jwtUtil.createAccessToken(authMember);

        return ApiResponse.onSuccess(
                MemberSuccessCode.OK,
                new LoginResponse(accessToken)
        );
    }
}&lt;/code&gt;&lt;/pre&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;AuthenticationManager가 인증을 수행한다&lt;/li&gt;
&lt;li&gt;인증 성공 시 UserDetails를 얻는다&lt;/li&gt;
&lt;li&gt;그 사용자 정보를 바탕으로 JWT를 발급한다&lt;/li&gt;
&lt;li&gt;토큰을 응답으로 내려준다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 세션 로그인에서는 세션이 생겼다면&lt;br /&gt;JWT 로그인에서는 &lt;b&gt;토큰을 발급하는 것이 로그인 성공의 핵심 결과&lt;/b&gt;가 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마이페이지 API는 어떻게 개선할 수 있을까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 마이페이지 조회 API에서&lt;br /&gt;사용자 ID를 직접 Request Body나 Path Variable로 받는 방식을 생각할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 JWT 기반 인증에서는 굳이 클라이언트가 사용자 ID를 따로 보내지 않아도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 이미 JWT를 통해 인증이 끝났고, 현재 로그인한 사용자 정보가 SecurityContextHolder 안에 들어 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 컨트롤러에서는 @AuthenticationPrincipal을 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/members&quot;)
public class MemberController {

    private final MemberService memberService;

    @GetMapping(&quot;/mypage&quot;)
    public ApiResponse&amp;lt;MemberResponse&amp;gt; getMyPage(
            @AuthenticationPrincipal AuthMember authMember
    ) {
        return ApiResponse.onSuccess(
                MemberSuccessCode.OK,
                memberService.getMyPage(authMember)
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스는 이런 식이 될 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class MemberService {

    public MemberResponse getMyPage(AuthMember authMember) {
        Member member = authMember.getMember();

        return new MemberResponse(
                member.getId(),
                member.getEmail(),
                member.getName()
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 @AuthenticationPrincipal이 편할까?&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@AuthenticationPrincipal AuthMember authMember&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 한 줄로 스프링이 SecurityContextHolder 안에 들어 있는 인증 객체의 principal을 꺼내서 넣어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 컨트롤러가 직접 아래 코드를 작성하지 않아도 됩니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
AuthMember authMember = (AuthMember) authentication.getPrincipal();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 마이페이지 API는&lt;br /&gt;&amp;ldquo;누구의 마이페이지인지&amp;rdquo;를 클라이언트에게 다시 묻지 않고,&lt;br /&gt;&lt;b&gt;현재 로그인한 사용자 기준으로 자연스럽게 처리&lt;/b&gt;할 수 있게 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;OAuth는 무엇일까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT를 이해했다면 그다음으로 자연스럽게 보게 되는 것이 OAuth입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth는&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 우리가 서비스에서&lt;br /&gt;&amp;ldquo;카카오로 로그인&amp;rdquo;, &amp;ldquo;구글로 로그인&amp;rdquo; 같은 버튼을 눌렀을 때&lt;br /&gt;우리 서버가 카카오 비밀번호를 직접 받는 것이 아니라, 카카오가 인증을 대신 수행하고 결과만 넘겨주는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p 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;559&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wpCHv/dJMcac4hov4/jz4siaxXdk6L4uubKWeOzK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wpCHv/dJMcac4hov4/jz4siaxXdk6L4uubKWeOzK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wpCHv/dJMcac4hov4/jz4siaxXdk6L4uubKWeOzK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwpCHv%2FdJMcac4hov4%2Fjz4siaxXdk6L4uubKWeOzK%2Fimg.jpg&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;559&quot; height=&quot;452&quot; data-origin-width=&quot;559&quot; data-origin-height=&quot;452&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;카카오 로그인 설정은 어떻게 시작할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오는 스프링이 기본 provider로 제공하는 대상이 아니기 때문에&lt;br /&gt;application.yml에 관련 설정을 직접 적어줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 식입니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  security:
    oauth2:
      client:
        registration:
          kakao:
            client-id: ${KAKAO_REST_API_KEY}
            client-secret: ${KAKAO_REST_API_SECRET}
            authorization-grant-type: authorization_code
            redirect-uri: &quot;http://localhost:8080/oauth/callback/kakao&quot;
            scope:
              - profile_nickname
              - account_email
        provider:
          kakao:
            authorization-uri: &quot;https://kauth.kakao.com/oauth/authorize&quot;
            token-uri: &quot;https://kauth.kakao.com/oauth/token&quot;
            user-info-uri: &quot;https://kapi.kakao.com/v2/user/me&quot;
            user-name-attribute: id&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정을 보면&lt;br /&gt;카카오 로그인에 필요한 인증 주소, 토큰 발급 주소, 사용자 정보 조회 주소를 스프링에게 알려주는 역할을 한다고 이해할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;CustomOAuthService는 왜 필요할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 인증이 끝나면 스프링은 OAuth 제공자에게서 사용자 정보를 가져올 수 있습니다.&lt;br /&gt;그런데 그 정보는 아직 &lt;b&gt;우리 서비스의 회원 객체가 아닙니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 제공자에서 받아온 정보를 우리 서비스의 회원 시스템과 연결하는 과정이 필요합니다.&lt;br /&gt;그 역할을 하는 것이 CustomOAuthService입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 코드입니다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class CustomOAuthService extends DefaultOAuth2UserService {

    private final MemberRepository memberRepository;

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

        SocialType providerId;
        String socialUid;

        Map&amp;lt;String, Object&amp;gt; attributes = oAuthMember.getAttribute(&quot;kakao_account&quot;);
        Map&amp;lt;String, Object&amp;gt; profile = (Map&amp;lt;String, Object&amp;gt;) attributes.get(&quot;profile&quot;);

        try {
            providerId = SocialType.valueOf(
                    userRequest.getClientRegistration().getRegistrationId().toUpperCase()
            );
            socialUid = String.valueOf((Long) oAuthMember.getAttribute(&quot;id&quot;));
        } catch (IllegalArgumentException e) {
            throw new MemberException(MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER);
        }

        OAuthDTO dto;
        switch (providerId) {
            case KAKAO -&amp;gt; {
                String email = attributes.get(&quot;email&quot;).toString();
                String name = profile.get(&quot;nickname&quot;).toString();
                dto = new KakaoDTO(socialUid, email, name);
            }
            default -&amp;gt; throw new MemberException(MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER);
        }

        Member member = memberRepository.findBySocialTypeAndSocialUid(providerId, socialUid)
                .orElseGet(() -&amp;gt; {
                    Member newMember = MemberConverter.toMember(dto);
                    memberRepository.save(newMember);
                    return newMember;
                });

        return new OAuthMember(member, oAuthMember.getAttributes());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;이 코드는 어떤 흐름으로 동작할까?&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. super.loadUser()로 제공자 정보 조회&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;OAuth2User oAuthMember = super.loadUser(userRequest);&lt;/code&gt;&lt;/pre&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;p data-ke-size=&quot;size16&quot;&gt;즉, 개발자가 직접 RestTemplate으로 호출하지 않아도 기본 흐름은 스프링이 대신 처리해줍니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 카카오 응답에서 필요한 값 추출&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Map&amp;lt;String, Object&amp;gt; attributes = oAuthMember.getAttribute(&quot;kakao_account&quot;);
Map&amp;lt;String, Object&amp;gt; profile = (Map&amp;lt;String, Object&amp;gt;) attributes.get(&quot;profile&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오는 응답 구조가 조금 중첩되어 있기 때문에&lt;br /&gt;이메일, 닉네임 같은 값을 꺼내기 위해 이런 식으로 파싱합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, OAuth 제공자마다 응답 구조가 다를 수 있어서 이 과정을 DTO로 한 번 정리해주는 것이 중요합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 기존 회원이면 조회, 없으면 저장&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Member member = memberRepository.findBySocialTypeAndSocialUid(providerId, socialUid)
        .orElseGet(() -&amp;gt; {
            Member newMember = MemberConverter.toMember(dto);
            memberRepository.save(newMember);
            return newMember;
        });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소셜 로그인이라고 해서 매번 새 회원을 만드는 것이 아니라, 같은 소셜 UID를 가진 사용자가 이미 있으면 기존 회원을 가져옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, OAuth 로그인도 결국 &lt;b&gt;우리 서비스 회원 시스템 안으로 연결하는 작업&lt;/b&gt;이 필요합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;OAuthSuccessHandler는 왜 필요할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 인증이 성공했다고 해서 그 자체로 우리 서비스 인증이 완전히 끝난 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 서비스는 이후 API 요청을 JWT 기준으로 처리하고 싶을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 소셜 로그인 성공 후에는 &lt;b&gt;우리 서버 전용 JWT를 다시 발급하는 과정&lt;/b&gt;이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 역할을 하는 것이 OAuthSuccessHandler입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class OAuthSuccessHandler implements AuthenticationSuccessHandler {

    private final JwtUtil jwtUtil;

    @Override
    public void onAuthenticationSuccess(
            HttpServletRequest request,
            HttpServletResponse response,
            Authentication authentication
    ) throws IOException, ServletException {

        ObjectMapper objectMapper = new ObjectMapper();
        BaseSuccessCode code = MemberSuccessCode.OK;

        response.setContentType(&quot;application/json;charset=UTF-8&quot;);
        response.setStatus(code.getStatus().value());

        OAuthMember member = (OAuthMember) SecurityContextHolder
                .getContext()
                .getAuthentication()
                .getPrincipal();

        String accessToken = jwtUtil.createAccessToken(new AuthMember(member.getMember()));

        ApiResponse&amp;lt;MemberResDTO.Login&amp;gt; responseBody = ApiResponse.onSuccess(
                code,
                MemberConverter.toLogin(accessToken)
        );

        objectMapper.writeValue(response.getOutputStream(), responseBody);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;이 핸들러는 무엇을 하는 걸까?&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. OAuth 인증 객체를 가져온다&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;OAuthMember member = (OAuthMember) SecurityContextHolder
        .getContext()
        .getAuthentication()
        .getPrincipal();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오 로그인 성공 후&lt;br /&gt;현재 인증 객체 안에는 OAuthMember가 들어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 우리 서비스용 JWT를 발급한다&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;String accessToken = jwtUtil.createAccessToken(new AuthMember(member.getMember()));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 OAuth 사용자 정보를 우리 서비스에서 사용하는 AuthMember 형식으로 바꿔서 JWT를 발급합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 소셜 로그인은 &amp;ldquo;카카오가 인증해주는 단계&amp;rdquo;이고,&lt;br /&gt;그 이후 실제 우리 서비스 API 호출은 &lt;b&gt;우리 서버가 발급한 JWT로 처리하는 구조&lt;/b&gt;가 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. JSON 응답으로 토큰을 내려준다&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;ApiResponse&amp;lt;MemberResDTO.Login&amp;gt; responseBody = ApiResponse.onSuccess(
        code,
        MemberConverter.toLogin(accessToken)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 프론트엔드는&lt;br /&gt;카카오 로그인 성공 후 받은 JWT를 저장해두고, 이후 요청마다 Authorization: Bearer {token} 형식으로 보내면 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결국 OAuth와 JWT는 어떻게 연결될까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 JWT와 OAuth가 별개처럼 느껴질 수 있습니다.&lt;br /&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;카카오가 사용자 인증을 처리한다&lt;/li&gt;
&lt;li&gt;우리 서버가 사용자 정보를 받아 회원 조회/저장을 한다&lt;/li&gt;
&lt;li&gt;우리 서버가 자체 JWT를 발급한다&lt;/li&gt;
&lt;li&gt;이후 API 요청은 JWT로 인증한다&lt;/li&gt;
&lt;/ol&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;OAuth는 &lt;b&gt;외부 제공자를 통한 로그인 방식&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;JWT는 &lt;b&gt;우리 서비스 내부에서 인증 상태를 유지하는 방식&lt;/b&gt;&lt;/li&gt;
&lt;/ul&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;b&gt;정리해보면&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 내용을 코드와 함께 정리하면서 느낀 것은&lt;br /&gt;JWT와 OAuth는 단순히 개념만 아는 것으로는 잘 이해되지 않는다는 점이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;JwtUtil에서 토큰을 생성하고 검증하고&lt;/li&gt;
&lt;li&gt;JwtAuthFilter에서 요청 헤더를 확인하고 인증 객체를 만들고&lt;/li&gt;
&lt;li&gt;SecurityConfig에서 필터 체인에 등록하고&lt;/li&gt;
&lt;li&gt;@AuthenticationPrincipal로 현재 로그인 사용자를 꺼내고&lt;/li&gt;
&lt;li&gt;CustomOAuthService에서 소셜 사용자 정보를 회원 시스템에 연결하고&lt;/li&gt;
&lt;li&gt;OAuthSuccessHandler에서 우리 서비스용 JWT를 발급하는&lt;/li&gt;
&lt;/ul&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;p data-ke-size=&quot;size16&quot;&gt;즉, 인증은 단순히 로그인 성공 여부만 확인하는 기능이 아니라&lt;br /&gt;&lt;b&gt;요청이 들어오는 순간부터 컨트롤러에 도달하기 전까지 어떤 방식으로 사용자를 식별할지 설계하는 일&lt;/b&gt;에 더 가깝다고 느꼈습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 저도 세션 로그인, JWT, OAuth가 전부 따로 배우는 주제처럼 느껴졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이번에 코드까지 같이 정리해보니&lt;br /&gt;세션 기반 인증과 토큰 기반 인증의 차이,&lt;br /&gt;JWT 필터가 인증 객체를 만드는 방식,&lt;br /&gt;OAuth 로그인 이후 우리 서버 JWT를 다시 발급하는 이유가 조금 더 자연스럽게 연결됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 이번 내용을 공부하면서 느낀 것은&lt;br /&gt;JWT는 단순한 문자열이 아니라 &lt;b&gt;서명으로 신뢰를 검증하는 인증 수단&lt;/b&gt;이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth는 단순한 소셜 로그인 버튼이 아니라&lt;br /&gt;&lt;b&gt;외부 인증 결과를 우리 서비스 인증 체계로 연결하는 흐름&lt;/b&gt;이라는 점이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 인증 기능을 구현할 때는 단순히 로그인 성공 여부만 보는 데서 끝나지 않고,&lt;br /&gt;&lt;b&gt;이 토큰은 어디서 생성되고 어디서 검증되는지&lt;/b&gt;,&lt;br /&gt;&lt;b&gt;현재 인증 객체는 어떻게 만들어지는지&lt;/b&gt;,&lt;br /&gt;&lt;b&gt;소셜 로그인 이후 우리 서비스 인증은 어떻게 이어지는지&lt;/b&gt;까지 함께 생각하면서 코드를 봐야겠다고 느꼈습니다.&lt;/p&gt;</description>
      <category>백엔드</category>
      <author>Archive_99</author>
      <guid isPermaLink="true">https://arch2ve-99.tistory.com/16</guid>
      <comments>https://arch2ve-99.tistory.com/16#entry16comment</comments>
      <pubDate>Mon, 25 May 2026 15:15:37 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot / 백엔드] Spring Security 이해하기 - 구조, 인증/인가, 폼 로그인</title>
      <link>https://arch2ve-99.tistory.com/15</link>
      <description>&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;백엔드 개발을 공부하다 보면 기능 구현만큼이나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;사용자를 어떻게 보호할지&lt;/b&gt;&lt;/span&gt;를 고민하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 저도 로그인 기능을 단순히&lt;br /&gt;&amp;ldquo;이메일과 비밀번호를 확인해서 맞으면 통과시키는 것&amp;rdquo; 정도로만 생각했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 실제로 서비스를 만든다고 생각해보면 단순히 로그인만 되는 것으로 끝나지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;누가 누구인지 확인하는 과정은 어떻게 처리할지&lt;/b&gt;&lt;span&gt;,&lt;br /&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;로그인한 사용자가 어떤 API까지 접근할 수 있는지 어떻게 구분할지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;,&lt;br /&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;인증되지 않은 요청이나 권한이 없는 요청은 어떤 방식으로 막을지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;까지 함께 설계해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 자주 등장하는 것이 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;Spring Security&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이번 글에서는 워크북 내용을 바탕으로&lt;br /&gt;&lt;span&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;Spring Security가 무엇인지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;,&lt;br /&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;인증(Authentication)과 인가(Authorization)는 어떻게 다른지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;,&lt;br /&gt;그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;스프링 시큐리티가 어떤 구조로 동작하고, 폼 로그인을 어떻게 구현하는지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;를 정리해보려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서 다룰 내용은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Security란 무엇인가&lt;/li&gt;
&lt;li&gt;인증과 인가는 어떻게 다른가&lt;/li&gt;
&lt;li&gt;Spring Security는 어떤 구조로 동작하는가&lt;/li&gt;
&lt;li&gt;AuthenticationManager, UserDetailsService, SecurityContext는 어떤 역할을 하는가&lt;/li&gt;
&lt;li&gt;Filter Chain은 왜 중요한가&lt;/li&gt;
&lt;li&gt;폼 로그인을 어떻게 구현하는가&lt;/li&gt;
&lt;li&gt;인증/인가 실패 응답은 어떻게 통일할 수 있을까&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Spring Security란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;자바 기반 웹 애플리케이션에서 인증과 인가를 처리하고, 다양한 보안 기능을 제공하는 프레임워크&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;조금 더 쉽게 말하면,&lt;br /&gt;개발자가 인증과 권한 처리를 처음부터 전부 직접 구현하지 않도록&lt;br /&gt;&lt;span&gt;&lt;b&gt;보안 관련 기능을 체계적으로 제공해주는 도구&lt;/b&gt;&lt;/span&gt;라고 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;백엔드 서비스를 만들다 보면 이런 요구사항이 자연스럽게 생깁니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원가입과 로그인을 구현해야 한다&lt;/li&gt;
&lt;li&gt;로그인하지 않은 사용자는 특정 API에 접근하면 안 된다&lt;/li&gt;
&lt;li&gt;로그인했더라도 권한이 없는 사용자는 특정 기능을 사용할 수 없어야 한다&lt;/li&gt;
&lt;li&gt;비밀번호는 안전하게 암호화해서 저장해야 한다&lt;/li&gt;
&lt;li&gt;인증 실패나 권한 부족 상황에서 적절한 응답을 내려줘야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security는 이런 문제들을 비교적 일관된 방식으로 처리할 수 있게 도와줍니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, Spring Security는 단순히 로그인 화면을 띄워주는 라이브러리가 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;애플리케이션의 인증, 인가, 보안 흐름 전체를 관리하는 프레임워크&lt;/b&gt;&lt;/span&gt;라고 이해하는 것이 더 자연스럽습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;인증(Authentication)과 인가(Authorization)는 뭐가 다를까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security를 공부할 때 가장 먼저 정리해야 하는 개념이 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;인증&lt;/b&gt;&lt;/span&gt;과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;인가&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;둘은 비슷하게 들리지만 의미가 다릅니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;1. 인증(Authentication)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인증은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;이 사용자가 누구인지 확인하는 과정&lt;/b&gt;&lt;/span&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자가 이메일과 비밀번호를 입력했을 때&lt;br /&gt;서버가 그 정보가 맞는지 확인하고 &amp;ldquo;이 사용자는 **이다&amp;rdquo;라고 식별하는 과정이 인증입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 로그인은 대표적인 인증 과정이라고 볼 수 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;2. 인가(Authorization)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인가는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;인증된 사용자가 특정 리소스에 접근할 권한이 있는지 확인하는 과정&lt;/b&gt;&lt;/span&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 로그인은 했지만 관리자만 접근할 수 있는 API에 일반 사용자가 요청을 보내면&lt;br /&gt;이때는 &amp;ldquo;누구인지는 확인됐지만, 이 기능을 사용할 권한은 없다&amp;rdquo;고 판단하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;인증은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;누구인지 확인하는 것&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;인가는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;무엇을 할 수 있는지 확인하는 것&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이라고 정리할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 이 둘이 비슷하게 느껴졌는데, 정리해보니 로그인 자체는 인증이고&lt;br /&gt;로그인 이후 특정 API 접근 가능 여부를 판단하는 것은 인가라는 점이 더 분명해졌습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Spring Security는 어떤 문제를 해결할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보안 이야기를 할 때는 인증과 인가 외에도 여러 보안 이슈가 함께 언급됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대표적으로는 다음과 같은 것들이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CSRF&lt;/li&gt;
&lt;li&gt;XSS&lt;/li&gt;
&lt;li&gt;CORS&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 CSRF와 XSS는 대표적인 웹 보안 취약점이고,&lt;br /&gt;CORS는 다른 출처 간 요청을 제어하는 정책으로 보안과 함께 자주 다뤄지는 개념입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, Spring Security를 공부한다는 것은 단순히 로그인 기능만 배우는 것이 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;웹 애플리케이션이 요청을 어떻게 신뢰하고, 어떻게 보호할지에 대한 흐름을 함께 이해하는 과정&lt;/b&gt;&lt;/span&gt;이라고 느꼈습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Spring Security는 어떤 구조로 동작할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security는 요청이 들어왔을 때&lt;br /&gt;그 요청을 바로 Controller로 보내는 것이 아니라,&lt;br /&gt;먼저 여러 보안 필터를 거치도록 만듭니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 사용자의 요청은 애플리케이션에 도달하기 전에&lt;br /&gt;&lt;span&gt;&lt;b&gt;Security Filter Chain&lt;/b&gt;&lt;/span&gt;을 통과하면서 인증과 인가 관련 검사를 받게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로그인 요청 기준으로 보면 흐름은 대략 이렇게 이해할 수 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #000000; 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;/li&gt;
&lt;li&gt;인증 매니저가 적절한 인증 제공자에게 인증을 맡긴다&lt;/li&gt;
&lt;li&gt;인증 제공자는 사용자 정보를 조회하고 비밀번호를 검증한다&lt;/li&gt;
&lt;li&gt;인증에 성공하면 인증 정보를 SecurityContext에 저장한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 로그인은 단순히 Controller에서 if문으로 비밀번호를 비교하는 것이 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;필터 &amp;rarr; 매니저 &amp;rarr; 프로바이더 &amp;rarr; 사용자 조회 &amp;rarr; 인증 저장&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;흐름으로 처리됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조를 알고 나니 Spring Security가 왜 처음에는 어렵게 느껴지는지도 조금 이해됐습니다.&lt;br /&gt;실제로 내부에서 여러 객체가 역할을 나눠서 움직이기 때문입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;주요 객체들은 각각 어떤 역할을 할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security의 구조를 볼 때 자주 등장하는 객체들이 있습니다.&lt;br /&gt;처음에는 이름이 비슷해서 헷갈렸는데, 역할을 기준으로 나눠서 보면 조금 더 이해하기 쉬웠습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;1. AuthenticationManager&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인증 과정을 관리하는 중심 객체입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 로그인 요청을 보내면,&lt;br /&gt;AuthenticationManager가 이 인증 요청을 어떻게 처리할지 판단합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 &amp;ldquo;이 요청을 처리할 수 있는 인증 방식이 무엇인지 찾고, 적절한 곳에 위임하는 역할&amp;rdquo;에 가깝습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;2. AuthenticationProvider&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제로 인증 로직을 처리하는 객체입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 이메일/비밀번호 기반 로그인이라면&lt;br /&gt;사용자 정보를 조회하고 비밀번호가 맞는지 검증하는 과정이 이 단계에서 일어납니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, AuthenticationManager가 흐름을 조율하는 역할이라면&lt;br /&gt;AuthenticationProvider는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;실제 인증 실행 담당자&lt;/b&gt;&lt;/span&gt;라고 볼 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;3. UserDetailsService&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자 정보를 조회하는 서비스입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보통 데이터베이스에서 사용자를 조회해서&lt;br /&gt;Spring Security가 이해할 수 있는 형태의&lt;span&gt;&amp;nbsp;&lt;/span&gt;UserDetails&lt;span&gt;&amp;nbsp;&lt;/span&gt;객체로 반환합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;이 이메일을 가진 사용자가 누구인지&amp;rdquo;를 찾아오는 역할이라고 보면 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;폼 로그인을 구현할 때는 보통&lt;span&gt;&amp;nbsp;&lt;/span&gt;UserDetailsService를 직접 구현해서&lt;br /&gt;DB에 저장된 회원 정보를 조회하도록 만듭니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;4. SecurityContext&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인증이 완료된 사용자 정보를 저장하는 공간입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로그인에 성공하면 인증 정보가 SecurityContext에 들어가고,&lt;br /&gt;이후 요청 처리 과정에서 현재 사용자가 누구인지 참조할 수 있게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, Spring Security는 인증 결과를 어딘가에 저장해두고&lt;br /&gt;필요할 때 꺼내서 쓰는데, 그 저장소 역할을 하는 것이 SecurityContext입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;5. SecurityContextHolder&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SecurityContext에 접근할 수 있도록 도와주는 객체입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보통 현재 로그인한 사용자 정보를 꺼낼 때 아래처럼 접근하게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 애플리케이션 어디에서든 현재 인증된 사용자 정보를 확인할 수 있게 해주는 진입점이라고 볼 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Filter Chain은 왜 중요할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security를 이해할 때 빠질 수 없는 개념이 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;Filter Chain&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;필터는 쉽게 말해, 요청이 애플리케이션 내부 로직에 도달하기 전에&lt;br /&gt;앞에서 한 번씩 검사하고 처리하는 관문 역할을 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;커피 필터를 떠올리면 조금 이해하기 편합니다.&lt;br /&gt;들어온 요청이 여러 필터를 순서대로 지나면서 필요한 작업을 거치고,&lt;br /&gt;조건에 맞지 않으면 중간에서 막히기도 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대표적인 필터를 간단히 보면 다음과 같습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;1. UsernamePasswordAuthenticationFilter&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;폼 로그인 요청을 처리합니다.&lt;br /&gt;사용자가 제출한 username, password를 바탕으로 인증을 시도합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;2. AnonymousAuthenticationFilter&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인증되지 않은 요청에 대해 익명 사용자 정보를 부여합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;3. ExceptionTranslationFilter&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인증 실패나 인가 실패 같은 보안 예외를 적절한 HTTP 응답으로 변환합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;4. FilterSecurityInterceptor&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재 사용자가 요청한 리소스에 접근할 권한이 있는지 최종적으로 확인합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, Spring Security는 단일 객체가 보안을 처리하는 것이 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;여러 필터가 역할을 나눠 요청을 검사하는 구조&lt;/b&gt;&lt;/span&gt;로 동작합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 부분을 이해하고 나니, 인증 실패 시 왜 Controller까지 오지 않고&lt;br /&gt;중간에서 바로 응답이 바뀌는지도 더 자연스럽게 이해됐습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;폼 로그인을 어떻게 구현할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 실제로 Spring Security를 적용해 폼 로그인을 구현하는 흐름을 정리해보겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전체 흐름은 대략 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Spring Security 의존성을 추가한다&lt;/li&gt;
&lt;li&gt;SecurityConfig&lt;span&gt;를 만든다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;어떤 API를 공개할지, 어떤 API를 보호할지 설정한다&lt;/li&gt;
&lt;li&gt;&lt;span&gt;비밀번호 암호화를 위한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;PasswordEncoder&lt;span&gt;를 등록한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;UserDetails&lt;span&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;UserDetailsService&lt;span&gt;를 구현한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;로그인 성공 후 인증 정보가 저장되도록 확인한다&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;1. 의존성 추가&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 Spring Security 의존성을 추가해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;sml&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 의존성을 추가하면 Spring Security 관련 기본 기능들을 사용할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;2. SecurityConfig 설정&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그다음에는 보안 정책을 정의할&lt;span&gt;&amp;nbsp;&lt;/span&gt;SecurityConfig를 만듭니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;@EnableWebSecurity
@Configuration
public class SecurityConfig {

    private final String[] allowUris = {
            &quot;/swagger-ui/**&quot;,
            &quot;/swagger-resources/**&quot;,
            &quot;/v3/api-docs/**&quot;,
            &quot;/auth/**&quot;
    };

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(requests -&amp;gt; requests
                        .requestMatchers(allowUris).permitAll()
                        .anyRequest().authenticated()
                )
                .formLogin(form -&amp;gt; form
                        .defaultSuccessUrl(&quot;/swagger-ui/index.html&quot;, true)
                        .permitAll()
                )
                .logout(logout -&amp;gt; logout
                        .logoutUrl(&quot;/logout&quot;)
                        .logoutSuccessUrl(&quot;/login?logout&quot;)
                        .permitAll()
                );

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 설정에서 핵심은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/auth/**&lt;span&gt;, Swagger 관련 경로는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;Public API&lt;/b&gt;&lt;/span&gt;&lt;span&gt;로 허용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;그 외의 요청은 모두&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;인증 필요&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;폼 로그인을 사용&lt;/li&gt;
&lt;li&gt;로그아웃 경로를 설정&lt;/li&gt;
&lt;li&gt;&lt;span&gt;비밀번호 암호화를 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;BCryptPasswordEncoder&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;등록&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 이 설정 하나로&lt;br /&gt;&amp;ldquo;어떤 요청은 누구나 접근 가능하고, 어떤 요청은 로그인해야만 가능하다&amp;rdquo;는 보안 정책을 정의할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Public API와 Private API를 나누는 이유&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제로 서비스를 만들 때는 모든 API가 같은 성격을 가지지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원가입 API&lt;/li&gt;
&lt;li&gt;로그인 API&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;같은 것은 로그인하지 않은 사용자도 접근할 수 있어야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반면에&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;같은 기능은 로그인한 사용자만 접근해야 자연스럽습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 보통은 API를 &lt;span&gt;&lt;b&gt;Public API&lt;/b&gt;&lt;/span&gt;&lt;span&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;Private API&lt;/b&gt;&lt;/span&gt;&lt;span&gt;로 나누어 관리하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 구분을 직접 Controller마다 if문으로 처리하는 것이 아니라&lt;br /&gt;Spring Security 설정에서 일관되게 처리한다는 점이 꽤 편리하다고 느꼈습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;비밀번호는 왜 BCrypt로 암호화할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;회원가입을 구현할 때 중요한 것 중 하나가 비밀번호 저장 방식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;비밀번호를 그대로 저장하는 것은 당연히 위험하기 때문에 보통은 해시 기반 암호화를 사용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;PasswordEncoder를 통해 이를 처리할 수 있고,&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대표적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;BCryptPasswordEncoder를 많이 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 Bean으로 등록해두면 회원가입 로직에서 DI 받아 사용할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 사용자가 입력한 비밀번호를 그대로 저장하는 것이 아니라&lt;br /&gt;암호화된 형태로 저장하고, 로그인 시에는 입력값을 같은 방식으로 검증하게 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;UserDetails와 UserDetailsService는 왜 구현할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;폼 로그인에서 핵심이 되는 부분이 바로 &lt;span&gt;&lt;b&gt;Spring Security가 사용자 정보를 어떻게 이해할 것인가&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security는 사용자를 다룰 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;UserDetails&lt;span&gt;&amp;nbsp;&lt;/span&gt;타입을 사용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 보통 우리 서비스의 회원 정보를 감싸는&lt;span&gt;&amp;nbsp;&lt;/span&gt;AuthMember&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 클래스를 만들고,&lt;br /&gt;이 클래스가&lt;span&gt;&amp;nbsp;&lt;/span&gt;UserDetails를 구현하도록 합니다. 그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;UserDetailsService를 구현해서&lt;br /&gt;이메일 같은 식별자를 기준으로 DB에서 회원을 조회한 뒤 UserDetails&lt;span&gt;&amp;nbsp;&lt;/span&gt;객체를 반환하게 만듭니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;흐름을 단순하게 보면 이렇습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인 요청이 들어온다&lt;/li&gt;
&lt;li&gt;Spring Security가&lt;span&gt;&amp;nbsp;&lt;/span&gt;UserDetailsService를 호출한다&lt;/li&gt;
&lt;li&gt;DB에서 회원 정보를 조회한다&lt;/li&gt;
&lt;li&gt;UserDetails&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;형태로 반환한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;비밀번호를 검증한다&lt;/li&gt;
&lt;li&gt;&lt;span&gt;인증에 성공하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;SecurityContext&lt;span&gt;에 저장한다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;UserDetailsService&lt;span&gt;는&lt;br /&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;Spring Security와 우리 회원 테이블을 이어주는 다리 역할&lt;/b&gt;&lt;/span&gt;&lt;span&gt;을 한다고 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;로그인이 실제로 성공하면 어떤 일이 일어날까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;폼 로그인 페이지에서 이메일과 비밀번호를 입력하고 요청을 보내면&lt;br /&gt;Spring Security가 내부적으로 인증을 시도합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때&lt;span&gt;&amp;nbsp;&lt;/span&gt;CustomUserDetailsService의&lt;span&gt;&amp;nbsp;&lt;/span&gt;loadUserByUsername()이 호출되고,&lt;br /&gt;DB에 저장된 사용자 정보를 조회한 뒤 비밀번호를 비교합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;비밀번호가 일치하면 인증이 성공하고,&lt;br /&gt;그 결과가&lt;span&gt;&amp;nbsp;&lt;/span&gt;SecurityContextHolder에 저장됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 로그인 성공 이후에는 현재 사용자가 누구인지에 대한 정보가 보안 컨텍스트에 저장되기 때문에&lt;br /&gt;이후 요청에서도 인증 상태를 참조할 수 있게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기까지 흐름을 정리하고 나니,&lt;br /&gt;왜 로그인을 한 뒤에는 별도의 사용자 확인 없이도 보호된 API를 호출할 수 있는지 조금 더 분명하게 보였습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;인증 실패와 인가 실패는 어떻게 다를까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security를 적용하다 보면&lt;br /&gt;로그인하지 않은 상태에서 Private API를 호출했을 때와,&lt;br /&gt;로그인은 했지만 권한이 부족한 경우를 구분해서 처리해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 두 상황은 비슷해 보이지만 다릅니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;1. 인증 실패&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아직 로그인하지 않았거나, 인증 정보가 유효하지 않은 경우입니다.&lt;br /&gt;보통&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;401 Unauthorized&lt;/b&gt;&lt;/span&gt;로 처리합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;2. 인가 실패&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로그인은 했지만 해당 리소스에 접근할 권한이 없는 경우입니다.&lt;br /&gt;보통&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;403 Forbidden&lt;/b&gt;&lt;/span&gt;으로 처리합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;401은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;누군지 확인되지 않음&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;403은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;누군지는 알지만 권한이 없음&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;으로 이해할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;기본 응답 대신 JSON으로 통일하려면?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security를 기본 설정으로 사용하면&lt;br /&gt;인증되지 않은 요청이 들어왔을 때 HTML 로그인 페이지 응답이 내려오는 경우가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 API 서버에서는 보통 HTML보다&lt;br /&gt;&lt;span&gt;&lt;b&gt;JSON 형태의 에러 응답&lt;/b&gt;&lt;/span&gt;을 일관되게 내려주는 것이 더 자연스럽습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 사용하는 것이 다음 두 가지입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AuthenticationEntryPoint&lt;/li&gt;
&lt;li&gt;AccessDeniedHandler&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;1. AuthenticationEntryPoint&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인증 실패 상황에서 호출됩니다.&lt;br /&gt;즉, 로그인하지 않은 사용자가 보호된 리소스에 접근했을 때 401 응답을 내려주는 역할을 합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;2. AccessDeniedHandler&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인가 실패 상황에서 호출됩니다.&lt;br /&gt;즉, 로그인은 했지만 권한이 부족할 때 403 응답을 내려주는 역할을 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 아래처럼 구현할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;public class CustomEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(
            HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException authException
    ) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        response.setContentType(&quot;application/json;charset=UTF-8&quot;);
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

        ApiResponse&amp;lt;Void&amp;gt; errorResponse = ApiResponse.onFailure(GeneralErrorCode.UNAUTHORIZED, null);
        objectMapper.writeValue(response.getOutputStream(), errorResponse);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;public class CustomAccessDenied implements AccessDeniedHandler {

    @Override
    public void handle(
            HttpServletRequest request,
            HttpServletResponse response,
            AccessDeniedException accessDeniedException
    ) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        response.setContentType(&quot;application/json;charset=UTF-8&quot;);
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);

        ApiResponse&amp;lt;Void&amp;gt; errorResponse = ApiResponse.onFailure(GeneralErrorCode.FORBIDDEN, null);
        objectMapper.writeValue(response.getOutputStream(), errorResponse);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 해두면 인증/인가 실패 시에도&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우리가 사용하는 공통 응답 형식으로 내려줄 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, Spring Security를 적용하더라도&lt;br /&gt;전체 API 응답 형식을 서비스에 맞게 통일할 수 있다는 점이 중요했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;SecurityConfig에 예외 처리까지 연결하면&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위에서 만든&lt;span&gt;&amp;nbsp;&lt;/span&gt;AuthenticationEntryPoint&lt;span&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;AccessDeniedHandler&lt;span&gt;는&lt;br /&gt;&lt;/span&gt;SecurityConfig&lt;span&gt;에 등록해서 사용하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, Spring Security 필터 체인에서 발생하는 인증/인가 예외를&lt;br /&gt;우리가 원하는 방식으로 처리하도록 연결하는 것입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 과정을 거치면 로그인하지 않은 요청이 들어왔을 때 HTML 페이지 대신 JSON 에러 응답을 내려줄 수 있고,&lt;br /&gt;권한이 없는 요청도 마찬가지로 일관된 형식으로 처리할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;API 서버를 만든다는 관점에서는 이 부분이 꽤 중요하다고 느꼈습니다.&lt;br /&gt;보안도 중요하지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;에러 응답의 일관성 역시 협업과 유지보수에서 큰 역할을 하기 때문&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;정리해보면&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 내용을 정리하면서 느낀 것은&lt;br /&gt;Spring Security는 단순히 &amp;ldquo;로그인 기능을 넣는 라이브러리&amp;rdquo;가 아니라는 점이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제로는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;li&gt;이후 요청에서 권한까지 확인하는 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;로 이루어져 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, Spring Security는 단순한 API 하나가 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;웹 애플리케이션의 보안 흐름 전체를 관리하는 구조&lt;/b&gt;&lt;/span&gt;라고 보는 것이 더 맞다고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 이번에 공부하면서 AuthenticationManager, AuthenticationProvider, UserDetailsService 같은 객체들이&lt;br /&gt;각자 어떤 책임을 가지는지 조금씩 분리해서 보게 되었고,&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;폼 로그인도 단순히 로그인 페이지를 띄우는 기능이 아니라&lt;br /&gt;그 뒤에 있는 인증 구조를 이해해야 제대로 사용할 수 있다는 점이 더 와닿았습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 저도 Spring Security가&lt;br /&gt;어노테이션 몇 개 붙이고 설정 클래스 하나 만들면 끝나는 줄 알았습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 내용을 정리해보니,&lt;br /&gt;그 안에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;인증과 인가의 차이&lt;/b&gt;&lt;/span&gt;&lt;span&gt;,&lt;br /&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;필터 체인의 흐름&lt;/b&gt;&lt;/span&gt;&lt;span&gt;,&lt;br /&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;사용자 정보를 조회하고 검증하는 구조&lt;/b&gt;&lt;/span&gt;&lt;span&gt;,&lt;br /&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;인증 실패와 인가 실패를 나누어 처리하는 방식&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;등, 생각보다 많은 개념이 들어 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 이번 내용을 공부하면서 느낀 것은&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security를 잘 활용하려면&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;이 요청이 어디서 막히는지&amp;rdquo;,&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&amp;ldquo;현재 인증 정보는 어디에 저장되는지&amp;rdquo;,&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&amp;ldquo;왜 401과 403을 다르게 처리해야 하는지&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;를 함께 이해해야 한다는 점이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞으로 시큐리티 관련 기능을 구현할 때는 단순히 로그인만 되는지 보는 데서 끝나지 않고,&lt;br /&gt;&lt;span&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;현재 요청이 인증 단계인지, 인가 단계인지&lt;/b&gt;&lt;/span&gt;,&lt;br /&gt;&lt;span&gt;&lt;b&gt;응답 형식은 일관되게 내려가고 있는지&lt;/b&gt;&lt;/span&gt;,&lt;br /&gt;&lt;span&gt;&lt;b&gt;보안 설정이 API 구조와 잘 맞는지&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;까지 함께 고민하면서 구현해야겠다고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>백엔드</category>
      <author>Archive_99</author>
      <guid isPermaLink="true">https://arch2ve-99.tistory.com/15</guid>
      <comments>https://arch2ve-99.tistory.com/15#entry15comment</comments>
      <pubDate>Wed, 13 May 2026 13:28:32 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot / 백엔드] 페이징 이해하기 - Pageable, Page, Slice, Cursor Pagination</title>
      <link>https://arch2ve-99.tistory.com/14</link>
      <description>&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;백엔드 개발을 하다 보면 데이터를 저장하는 것보다&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;조회 API를 어떻게 설계할지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;더 고민하게 되는 순간이 옵니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 저도 목록 조회 API를 만들 때 그냥 데이터를 전부 내려주면 되는 줄 알았습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 실제로 데이터를 다루다 보면, 조회 대상이 많아질수록 &amp;ldquo;어떻게 끊어서 보여줄지&amp;rdquo;가 훨씬 중요하다는 걸 느끼게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 리뷰가 200개, 게시글이 1,000개, 미션이 수만 개라면&lt;br /&gt;그걸 한 번에 전부 조회해서 내려주는 방식은 성능상으로도, 사용자 경험 측면에서도 비효율적입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이번 글에서는 워크북 내용을 바탕으로&lt;br /&gt;&lt;span&gt;&lt;b&gt;페이징이 왜 필요한지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;,&lt;br /&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;Spring Data JPA에서 Pageable, Page, Slice는 어떤 역할을 하는지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;,&lt;br /&gt;그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;오프셋 기반 페이지네이션과 커서 기반 페이지네이션은 어떻게 다르고 언제 사용하는지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;를 정리해보려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서 다룰 내용은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이징이란 무엇인가&lt;/li&gt;
&lt;li&gt;조회 API에서 페이징이 왜 중요한가&lt;/li&gt;
&lt;li&gt;Pageable은 무엇인가&lt;/li&gt;
&lt;li&gt;Page와 Slice의 차이&lt;/li&gt;
&lt;li&gt;오프셋 기반 페이지네이션&lt;/li&gt;
&lt;li&gt;커서 기반 페이지네이션&lt;/li&gt;
&lt;li&gt;페이지네이션 응답 DTO는 어떻게 설계할까&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;조회 API는 왜 더 까다로울까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 POST 요청처럼 데이터를 저장하는 API가 더 어려워 보일 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제 서비스에서는 조회(Read) 요청의 비중이 훨씬 더 높은 경우가 많습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자는 게시글을 읽고, 리뷰를 보고, 목록을 탐색하고, 검색 결과를 확인합니다.&lt;br /&gt;즉, 서비스는 데이터를 한 번 저장하는 것보다&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;계속 조회해서 보여주는 일&lt;/b&gt;&lt;/span&gt;을 더 많이 하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;문제는 조회 대상이 많아질수록 단순히&lt;span&gt;&amp;nbsp;&lt;/span&gt;select *처럼 전부 가져오는 방식으로는 감당이 어려워진다는 점입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 리뷰가 200개 있는데 이를 한 번에 전부 내려준다면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB는 많은 양의 데이터를 한 번에 조회해야 하고&lt;/li&gt;
&lt;li&gt;서버는 그 데이터를 응답 형태로 가공해야 하고&lt;/li&gt;
&lt;li&gt;클라이언트는 그 많은 데이터를 한 번에 렌더링해야 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국 응답 속도가 느려지고, 불필요한 데이터까지 계속 주고받게 됩니다.&lt;br /&gt;그래서 조회 API를 만들 때는 항상 아래 두 가지를 함께 고민하게 됩니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #000000; 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;/ol&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 등장하는 개념이 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;페이징(Paging)&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;페이징이란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;페이징은 많은 데이터를 한 번에 전부 가져오지 않고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;일정 개수씩 끊어서 조회하는 방식&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 게시글이 1,000개 있다고 해도 사용자는 첫 화면에서 그 1,000개를 모두 보지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보통은&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1페이지에 10개씩 보여주거나&lt;/li&gt;
&lt;li&gt;스크롤을 내릴 때마다 다음 목록을 추가로 불러오거나&lt;/li&gt;
&lt;li&gt;최신순으로 일부만 먼저 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하는 식으로 데이터를 보게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 페이징은 단순히 화면을 나누는 기능이 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;조회 성능을 관리하고 사용자 경험을 개선하기 위한 방식&lt;/b&gt;&lt;/span&gt;이라고 볼 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;JPA에서 Pageable은 무엇일까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Data JPA에서는 페이지네이션을 위한 기능을 기본적으로 제공합니다.&lt;br /&gt;이때 핵심이 되는 것이 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;Pageable&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Pageable&lt;span&gt;은 쉽게 말하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;페이지네이션에 필요한 정보들을 담고 있는 객체&lt;/b&gt;&lt;/span&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 정보들이 들어갑니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SQL로 페이징을 배웠다면 이런 쿼리를 떠올릴 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;-- 오프셋 기반 페이지네이션
select * from mission
order by mission_id desc
limit 10 offset 0;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;limit&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;은 가져올 데이터 수&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;offset&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;은 현재 페이지 위치&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;order by&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;는 정렬 기준&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JPA에서는 이런 정보를&lt;span&gt;&amp;nbsp;&lt;/span&gt;Pageable에 담아서 Repository에 전달합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 실제로 많이 사용하는 구현체가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;PageRequest&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;PageRequest는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Pageable을 구현한 객체이고,&lt;br /&gt;우리는 보통 이를 통해 &amp;ldquo;몇 페이지를 어떤 정렬 기준으로 몇 개 가져올지&amp;rdquo;를 지정하게 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;오프셋 기반 페이지네이션이란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;오프셋 기반 페이지네이션은 가장 익숙한 방식입니다.&lt;br /&gt;보통 &amp;ldquo;1페이지, 2페이지, 3페이지&amp;rdquo;처럼&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;페이지 번호를 기준으로 데이터를 조회하는 방식&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 10개씩 보여주는 게시글 목록이 있다면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1페이지는 0~9번째 데이터&lt;/li&gt;
&lt;li&gt;2페이지는 10~19번째 데이터&lt;/li&gt;
&lt;li&gt;3페이지는 20~29번째 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;를 가져오는 식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SQL로 보면 보통 이런 형태가 됩니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;select * from mission
order by mission_id desc
limit 10 offset 0;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring에서는 보통 Query Parameter로 이런 값을 받습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pageNumber&lt;/li&gt;
&lt;li&gt;pageSize&lt;/li&gt;
&lt;li&gt;sort&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 이를 바탕으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;PageRequest를 만들어 Repository에 전달합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;오프셋 기반 페이지네이션의 장점은 직관적이라는 점입니다.&lt;br /&gt;사용자도 &amp;ldquo;지금 3페이지를 보고 있다&amp;rdquo;는 개념을 이해하기 쉽고, 프론트엔드에서도 페이지 이동 UI를 만들기 편합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 단점도 있습니다.&lt;br /&gt;페이지가 뒤로 갈수록 앞부분 데이터를 계속 건너뛰어야 하기 때문에 데이터가 많아질수록 성능이 떨어질 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 오프셋 기반 페이지네이션은 구현과 사용은 편하지만 대용량 데이터에서는 비효율적일 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Page란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Data JPA에서 오프셋 기반 페이지네이션을 할 때 자주 사용하는 반환 타입이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;Page&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Page는 단순히 데이터 목록만 담고 있는 것이 아니라 페이지네이션에 필요한 여러 정보들을 함께 담고 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;li&gt;전체 페이지 수&lt;/li&gt;
&lt;li&gt;첫 페이지인지 여부&lt;/li&gt;
&lt;li&gt;마지막 페이지인지 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;같은 정보들을 포함할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Page는 프론트엔드가 페이지 UI를 만들기에 편리한 정보를 많이 제공합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다만 여기서 중요한 점이 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Page&lt;span&gt;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;전체 데이터 수&lt;/b&gt;&lt;/span&gt;&lt;span&gt;를 알아야 하기 때문에 보통&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;count 쿼리&lt;/b&gt;&lt;/span&gt;&lt;span&gt;가 함께 나갑니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 &amp;ldquo;전체 미션이 몇 개인가?&amp;rdquo;를 알아야 총 페이지 수를 계산할 수 있으니,&lt;br /&gt;목록 조회 쿼리 외에 총 개수를 세는 쿼리가 추가로 실행되는 것입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서&lt;span&gt;&amp;nbsp;&lt;/span&gt;Page는 정보가 풍부한 대신, 전체 개수를 매번 세야 한다는 비용이 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Slice란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Slice&lt;span&gt;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Page&lt;span&gt;와 비슷해 보이지만 조금 더 가벼운 개념입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Slice&lt;span&gt;도 목록 데이터와 정렬 정보, 현재 페이지 정보 등을 담을 수 있지만,&lt;br /&gt;&lt;/span&gt;Page&lt;span&gt;와 달리&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;전체 데이터 수를 포함하지 않습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;정도에 더 집중한 구조입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Slice의 핵심은 &amp;ldquo;다음 페이지가 있는지만 알면 된다&amp;rdquo;는 상황에 잘 맞는다는 점입니다. &amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보통 무한 스크롤 UI에서는 전체 페이지 수가 꼭 필요하지 않은 경우가 많습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자는 &amp;ldquo;총 몇 페이지인지&amp;rdquo;보다 &amp;ldquo;다음 데이터가 있으면 더 불러오기&amp;rdquo;만 하면 되기 때문입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이럴 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;Slice를 사용하면 전체 개수를 세는 count 쿼리를 줄일 수 있어 Page보다 가볍게 사용할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Page와 Slice는 어떻게 다를까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;둘 다 페이지네이션을 위한 객체이지만, 차이는 꽤 분명 합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;Page&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;일반적인 페이지 번호 기반 UI에 적합&lt;/li&gt;
&lt;li&gt;count 쿼리가 추가될 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;Slice&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;li&gt;count 쿼리 부담이 적음&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 전체 페이지 수까지 필요한 경우에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Page,&lt;br /&gt;&amp;ldquo;다음 데이터가 있는지&amp;rdquo; 정도만 알면 되는 경우에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Slice가 더 적합하다고 볼 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;그럼 응답을 그대로 내려주면 될까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음&lt;span&gt;&amp;nbsp;&lt;/span&gt;Page&lt;span&gt;&amp;nbsp;&lt;/span&gt;객체를 그대로 응답으로 내려보면 생각보다 프론트엔드 입장에서 필요하지 않은 정보까지 많이 포함되는 경우가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 내부적으로만 의미 있는 값이나, 클라이언트가 실제로 쓰지 않는 필드까지 함께 보일 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 실무에서는 보통&lt;span&gt;&amp;nbsp;&lt;/span&gt;Page&lt;span&gt;나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Slice&lt;span&gt;를 그대로 내려주기보다 &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;별도의 페이지네이션 응답 DTO를 만들어 필요한 정보만 정제해서 응답&lt;/b&gt;&lt;/span&gt;&lt;span&gt;하는 경우가 많습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 오프셋 기반 페이지네이션이라면 이런 정보들을 담을 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;page size&lt;/li&gt;
&lt;li&gt;total elements&lt;/li&gt;
&lt;li&gt;total pages&lt;/li&gt;
&lt;li&gt;마지막 페이지 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 페이지네이션도 결국 API 설계의 일부이기 때문에&lt;br /&gt;&amp;ldquo;JPA가 주는 값을 그대로 노출할지&amp;rdquo;,&lt;br /&gt;&amp;ldquo;클라이언트에게 필요한 형태로 가공할지&amp;rdquo;를 고민하는 것이 중요합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;커서 기반 페이지네이션이란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;커서 기반 페이지네이션은 페이지 번호 대신&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;특정 기준값(cursor)&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 이용해 다음 데이터를 조회하는 방식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 최신순으로 정렬된 데이터가 있을 때&lt;br /&gt;마지막으로 본 데이터의 ID를 기준으로 다음 데이터를 가져오는 식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SQL로 보면 이런 느낌입니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;select *
from mission
where store_id = ?
  and id &amp;lt; ?
order by id desc
limit 3;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이전 요청의 마지막 데이터 ID를 커서로 기억해두고&lt;/li&gt;
&lt;li&gt;다음 요청에서는 그 ID보다 작은 데이터만 조회하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 첫 요청에서는 최신 데이터 3개를 가져오고, 마지막 ID가 383이었다면&lt;br /&gt;다음 요청에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;id &amp;lt; 383&lt;span&gt;&amp;nbsp;&lt;/span&gt;조건으로 그 다음 3개를 가져오는 식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 방식은 오프셋처럼 앞의 데이터를 계속 건너뛰지 않기 때문에 데이터가 많아질수록 더 효율적으로 동작할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;커서 기반 페이지네이션은 왜 사용할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;커서 기반 페이지네이션은 특히&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;무한 스크롤&lt;/b&gt;&lt;/span&gt;에 잘 어울립니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자는 &amp;ldquo;2페이지로 이동&amp;rdquo;하는 것보다&lt;br /&gt;그냥 아래로 계속 내리면서 다음 데이터를 불러오는 경험을 더 자주 하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 중요한 것은 &amp;ldquo;지금 몇 페이지인지&amp;rdquo;보다&lt;br /&gt;&amp;ldquo;다음 데이터를 어디서부터 가져올지&amp;rdquo;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;커서 기반 페이지네이션의 장점은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다만 단점도 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구현이 오프셋 방식보다 복잡함&lt;/li&gt;
&lt;li&gt;페이지 번호 기반 UI에는 잘 맞지 않음&lt;/li&gt;
&lt;li&gt;정렬 기준과 커서 구조를 함께 설계해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 커서 기반 페이지네이션은 &amp;ldquo;더 고급스럽고 무조건 좋은 방식&amp;rdquo;이라기보다&lt;br /&gt;&lt;span&gt;&lt;b&gt;대량 데이터와 무한 스크롤에 더 적합한 방식&lt;/b&gt;&lt;/span&gt;이라고 이해하는 것이 좋습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Slice만 있으면 커서 기반 페이지네이션이 완성될까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 헷갈릴 수 있는 부분이 있습니다.&lt;br /&gt;Slice가 있다고 해서 커서 기반 페이지네이션이 자동으로 완성되는 것은 아닙니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Slice&lt;span&gt;는 어디까지나&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;같은 정보를 제공하는 역할에 가깝습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제 커서 기반 페이지네이션을 구현하려면 개발자가 직접&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;where 절에 어떤 조건을 넣을지 설계해야 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 ID 기준으로 내림차순 정렬한다면&lt;br /&gt;커서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;id&lt;span&gt;&amp;nbsp;&lt;/span&gt;값을 사용하면 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 별점, 생성일시처럼&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;중복될 수 있는 값&lt;/b&gt;&lt;/span&gt;을 기준으로 정렬한다면&lt;br /&gt;단순히 하나의 값만으로는 정확한 커서를 만들기 어렵습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이럴 때는 별점 + reviewId처럼 여러 값을 조합해서 커서를 설계해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Slice&lt;span&gt;는 커서 기반 페이지네이션에 잘 어울리는 도구이지만,&lt;br /&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;커서 자체를 설계하는 책임까지 대신해주지는 않습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;오프셋 기반과 커서 기반은 어떻게 다를까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;둘의 차이를 정리하면 이렇게 볼 수 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;오프셋 기반 페이지네이션&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;일반적인 게시판 UI에 적합&lt;/li&gt;
&lt;li&gt;뒤로 갈수록 성능이 떨어질 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;커서 기반 페이지네이션&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마지막 조회 기준값(cursor)으로 다음 데이터 조회&lt;/li&gt;
&lt;li&gt;무한 스크롤에 적합&lt;/li&gt;
&lt;li&gt;대량 데이터에서 더 효율적일 수 있음&lt;/li&gt;
&lt;li&gt;구현과 응답 설계가 조금 더 복잡함&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 둘 중 하나가 무조건 정답이라기보다&lt;br /&gt;&lt;span&gt;&lt;b&gt;서비스의 사용 방식에 따라 선택하는 것이 중요하다&lt;/b&gt;&lt;/span&gt;고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;페이지 번호가 중요한 서비스라면 오프셋 기반이 더 자연스럽고,&lt;br /&gt;계속 내려보는 피드형 서비스라면 커서 기반이 더 잘 맞을 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;페이지네이션 응답 DTO는 어떻게 설계할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;페이지네이션을 구현할 때는 데이터 목록만 내려주는 것보다&lt;br /&gt;클라이언트가 다음 동작을 할 수 있도록 필요한 메타데이터를 함께 주는 것이 중요합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 오프셋 기반 페이지네이션이라면 보통 이런 구조를 생각할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;{
  &quot;content&quot;: [
    {
      &quot;missionId&quot;: 1,
      &quot;point&quot;: 500,
      &quot;conditional&quot;: &quot;음료 포함 1만원 이상 주문&quot;
    }
  ],
  &quot;page&quot;: 0,
  &quot;size&quot;: 10,
  &quot;totalElements&quot;: 37,
  &quot;totalPages&quot;: 4,
  &quot;isLast&quot;: false
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반면 커서 기반 페이지네이션이라면&lt;br /&gt;전체 개수보다&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;다음 커서와 다음 데이터 존재 여부&lt;/b&gt;&lt;/span&gt;가 더 중요합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 느낌입니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;{
  &quot;content&quot;: [
    {
      &quot;missionId&quot;: 383,
      &quot;point&quot;: 500,
      &quot;conditional&quot;: &quot;음료 포함 1만원 이상 주문&quot;
    }
  ],
  &quot;listSize&quot;: 10,
  &quot;hasNext&quot;: true,
  &quot;nextCursor&quot;: &quot;ID:383&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 페이지네이션 응답도 단순히 데이터를 감싸는 것이 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;프론트엔드가 다음 요청을 어떻게 보내야 하는지까지 고려해서 설계해야 한다&lt;/b&gt;&lt;/span&gt;는 점이 중요합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;정리해보면&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 내용을 정리하면서 느낀 것은, 조회 API는 단순히 데이터를 가져오는 작업이 아니라&lt;br /&gt;&lt;span&gt;&lt;b&gt;많은 데이터를 어떻게 효율적으로, 그리고 사용하기 좋은 형태로 보여줄지 설계하는 작업&lt;/b&gt;&lt;/span&gt;이라는 점이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Data JPA에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Pageable,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Page,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Slice&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 도구를 제공하기 때문에&lt;br /&gt;SQL을 직접 다루지 않더라도 페이지네이션을 꽤 편하게 구현할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다만 도구를 쓰는 것과 설계를 잘하는 것은 조금 다른 문제라고 느꼈습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;페이지 번호 기반 UI인가?&lt;/li&gt;
&lt;li&gt;무한 스크롤 구조인가?&lt;/li&gt;
&lt;li&gt;응답은 어떤 형태로 내려줘야 프론트가 쓰기 편한가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이런 질문들을 함께 고민해야&lt;br /&gt;비로소 서비스에 맞는 페이지네이션을 설계할 수 있다고 생각했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 저도 페이징을 단순히 &amp;ldquo;데이터를 조금씩 끊어오는 기능&amp;rdquo; 정도로만 생각했습니다.&lt;br /&gt;그런데 정리해보니 페이징은 단순한 구현 포인트가 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;조회 API 설계의 핵심 요소&lt;/b&gt;&lt;/span&gt;에 가깝다고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히&lt;span&gt;&amp;nbsp;&lt;/span&gt;Page와&lt;span&gt;&amp;nbsp;&lt;/span&gt;Slice의 차이,&lt;br /&gt;오프셋 기반 페이지네이션과 커서 기반 페이지네이션의 차이,&lt;br /&gt;그리고 필요한 정보만 담아서 응답 DTO를 설계하는 과정까지 보면서&lt;br /&gt;&amp;ldquo;조회 API는 생각보다 더 많은 선택지를 포함하고 있구나&amp;rdquo;를 다시 느끼게 됐습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞으로 조회 API를 만들 때는 그냥 목록을 내려주는 데서 끝나지 않고,&lt;br /&gt;&lt;span&gt;&lt;b&gt;이 API는 어떤 페이징 방식이 더 적합할까?&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;프론트엔드가 실제로 필요한 정보는 무엇일까?&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;를 함께 고민하면서 설계해야겠다고 느꼈습니다&lt;/p&gt;</description>
      <category>백엔드</category>
      <category>pagable</category>
      <category>page</category>
      <category>Slice</category>
      <category>spring boot</category>
      <category>페이징</category>
      <author>Archive_99</author>
      <guid isPermaLink="true">https://arch2ve-99.tistory.com/14</guid>
      <comments>https://arch2ve-99.tistory.com/14#entry14comment</comments>
      <pubDate>Thu, 7 May 2026 17:35:18 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot / 백엔드] JPA란? - ORM, 영속성 컨텍스트, 연관관계 기초 정리</title>
      <link>https://arch2ve-99.tistory.com/13</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vNsGu/dJMcabxeNdn/NPCfgzvOlIqPXoQT6xGdI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vNsGu/dJMcabxeNdn/NPCfgzvOlIqPXoQT6xGdI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vNsGu/dJMcabxeNdn/NPCfgzvOlIqPXoQT6xGdI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvNsGu%2FdJMcabxeNdn%2FNPCfgzvOlIqPXoQT6xGdI1%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;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;백엔드 개발을 공부하다 보면 API 요청과 응답 흐름, Controller와 Service의 역할, 공통 응답 구조 같은 것들을 먼저 익히게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 어느 정도 API를 만들기 시작하면 자연스럽게 다음 단계의 고민이 생깁니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;그래서 이 데이터를 DB에 어떻게 저장하고, 다시 어떻게 객체로 가져오는 걸까?&amp;rdquo;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저도 처음에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;repository.save()나&lt;span&gt;&amp;nbsp;&lt;/span&gt;findById()&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 메서드를 그냥 자연스럽게 사용했지만, 정작 그 내부에서 어떤 일이 벌어지는지까지는 깊게 생각하지 않았습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 JPA를 공부하다 보니, 단순히 편하게 데이터를 저장하고 조회하는 도구를 넘어서,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;객체 지향 언어와 관계형 데이터베이스 사이의 차이를 메워주는 핵심 기술&lt;/b&gt;&lt;/span&gt;이라는 점이 보이기 시작했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 영속성 컨텍스트, 1차 캐시, 변경 감지, 지연 로딩 같은 개념은 처음에는 추상적으로 느껴졌지만, 흐름을 이해하고 나니 왜 JPA가 이렇게 많이 사용되는지 조금씩 이해할 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이번 글에서는 워크북 내용을 바탕으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;JPA가 왜 필요한지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;영속성 컨텍스트는 무엇인지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;엔티티와 연관관계는 어떻게 다뤄야 하는지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;, 그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;JPQL은 어떤 식으로 사용하는지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;를 정리해보려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서 다룰 내용은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체 지향 언어와 관계형 데이터베이스의 차이&lt;/li&gt;
&lt;li&gt;JPA와 ORM은 무엇인가&lt;/li&gt;
&lt;li&gt;영속성 컨텍스트란 무엇인가&lt;/li&gt;
&lt;li&gt;1차 캐시, 변경 감지, 쓰기 지연 SQL 저장소&lt;/li&gt;
&lt;li&gt;엔티티와 BaseEntity 설정&lt;/li&gt;
&lt;li&gt;연관관계 매핑과 지연 로딩&lt;/li&gt;
&lt;li&gt;JPQL과 Repository 기반 조회 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;객체 지향 언어와 관계형 데이터베이스는 왜 잘 안 맞을까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자바는 객체 지향 언어입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;객체 지향 언어는 캡슐화, 상속, 다형성처럼&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;객체의 구조와 역할&lt;/b&gt;&lt;/span&gt;을 중심으로 설계됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반면 우리가 실제 데이터를 저장할 때 많이 사용하는 관계형 데이터베이스(RDBMS)는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;테이블 구조와 데이터 정합성&lt;/b&gt;&lt;/span&gt;을 중심으로 설계됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 둘은 출발점부터 다릅니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자바에서는 객체가 다른 객체를 참조하면서 관계를 맺습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 데이터베이스에서는 외래 키(FK)를 통해 테이블과 테이블이 연결됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 DB에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;store_id&lt;span&gt;&amp;nbsp;&lt;/span&gt;하나로 가게와 미션을 연결할 수 있지만,&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자바에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Mission&lt;span&gt;&amp;nbsp;&lt;/span&gt;안에&lt;span&gt;&amp;nbsp;&lt;/span&gt;Store를 참조로 들고 있어야 하고, 반대로&lt;span&gt;&amp;nbsp;&lt;/span&gt;Store에서도&lt;span&gt;&amp;nbsp;&lt;/span&gt;Mission을 보고 싶다면 또 별도의 컬렉션을 선언해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;548&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckJSrT/dJMcagFmDg2/DRQdm4CgrOCpWbg10va8D0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckJSrT/dJMcagFmDg2/DRQdm4CgrOCpWbg10va8D0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckJSrT/dJMcagFmDg2/DRQdm4CgrOCpWbg10va8D0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckJSrT%2FdJMcagFmDg2%2FDRQdm4CgrOCpWbg10va8D0%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;395&quot; height=&quot;500&quot; data-origin-width=&quot;548&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 데이터베이스에서는 하나의 조인 키로 관계를 표현할 수 있지만, 객체에서는 관계를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;어느 방향으로 볼지&lt;/b&gt;&lt;/span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;누가 주인이 될지&lt;/b&gt;&lt;/span&gt;를 따로 설계해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 차이는 CRUD를 직접 작성할 때 더 크게 느껴집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;순수 JDBC로 회원가입 로직을 짠다고 생각해보면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중복 회원 체크를 위해 SELECT를 작성하고&lt;/li&gt;
&lt;li&gt;회원가입을 위해 INSERT를 작성하고&lt;/li&gt;
&lt;li&gt;PreparedStatement에 값을 하나씩 바인딩하고&lt;/li&gt;
&lt;li&gt;ResultSet을 다시 객체로 매핑해야 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 과정은 단순히 번거로운 정도가 아니라, 기능 하나를 만들 때마다 개발자가 직접 SQL과 객체 변환 작업을 반복해야 한다는 점에서 꽤 피로합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 공부하면서 느낀 건, 객체 지향 언어와 RDBMS는 둘 다 중요하지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;서로를 그대로 이해하지는 못한다&lt;/b&gt;&lt;/span&gt;는 점이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 바로 그 사이를 연결해주는 기술이 ORM이고, 자바 진영에서는 JPA가 그 중심에 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;JPA란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JPA는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Java Persistence API&lt;span&gt;의 줄임말입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 자바에서 ORM 기술을 사용하기 위한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;표준 인터페이스&lt;/b&gt;&lt;/span&gt;라고 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 중요한 건 JPA가 곧바로 구현체 그 자체는 아니라는 점입니다.&lt;br /&gt;JPA는 규약이고, 그 규약을 실제로 구현한 대표적인 오픈소스가 Hibernate입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA는 표준&lt;/li&gt;
&lt;li&gt;Hibernate는 구현체&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 이해하면 훨씬 자연스럽습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개인적으로 JPA를 처음 접했을 때는 그냥 &amp;ldquo;자바에서 DB를 쉽게 다루게 해주는 기술&amp;rdquo; 정도로 생각했는데,&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;정리하면서 보니 더 정확히는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;객체와 테이블을 매핑해주는 표준적인 방법&lt;/b&gt;&lt;/span&gt;이라고 보는 게 맞았습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;ORM이란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ORM은&lt;span&gt;&amp;nbsp;&lt;/span&gt;Object Relational Mapping&lt;span&gt;의 줄임말입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;말 그대로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;객체와 관계형 데이터베이스를 매핑하는 기술&lt;/b&gt;&lt;/span&gt;&lt;span&gt;입니다.&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;562&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/diUkjV/dJMcaarA8KJ/ctUBYCN7xyR2WdLkH7rkaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/diUkjV/dJMcaarA8KJ/ctUBYCN7xyR2WdLkH7rkaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/diUkjV/dJMcaarA8KJ/ctUBYCN7xyR2WdLkH7rkaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdiUkjV%2FdJMcaarA8KJ%2FctUBYCN7xyR2WdLkH7rkaK%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;431&quot; height=&quot;500&quot; data-origin-width=&quot;562&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 자바 클래스와 DB 테이블을 연결해두면, 개발자가 매번 SQL을 직접 짜지 않아도 객체 중심으로 데이터를 다룰 수 있게 도와줍니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;span&gt;&amp;nbsp;&lt;/span&gt;Member&lt;span&gt;&amp;nbsp;&lt;/span&gt;엔티티가&lt;span&gt;&amp;nbsp;&lt;/span&gt;member&lt;span&gt;&amp;nbsp;&lt;/span&gt;테이블과 매핑되어 있다면, 우리는 객체를 생성해서&lt;span&gt;&amp;nbsp;&lt;/span&gt;save()만 호출해도 JPA가 알아서 INSERT SQL을 만들어 실행해줍니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;조회할 때도 마찬가지입니다, DB의 row를 일일이 파싱해서 자바 객체로 변환할 필요 없이, JPA가 엔티티 객체로 매핑해줍니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 ORM은 단순히 &amp;ldquo;쿼리를 덜 쓰게 해주는 기술&amp;rdquo;이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;데이터를 객체 중심으로 다룰 수 있게 만드는 방식&lt;/b&gt;&lt;/span&gt;에 가깝다고 느꼈습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;JPA 구조에서 가장 중요한 것: 영속성 컨텍스트&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JPA를 이해할 때 가장 핵심이 되는 개념 중 하나가 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;영속성 컨텍스트(Persistence Context)&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음 이 단어를 들으면 꽤 어렵게 느껴집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저도 처음에는 &amp;ldquo;영속적이라는 게 무슨 의미지?&amp;rdquo; 하고 막연하게만 받아들였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;정리하자면 영속성 컨텍스트는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;엔티티 객체를 관리하는 메모리상의 논리적 공간&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bS7Ut1/dJMcah5krko/xSY25Af8qdDQeBsSJ9JHA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bS7Ut1/dJMcah5krko/xSY25Af8qdDQeBsSJ9JHA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bS7Ut1/dJMcah5krko/xSY25Af8qdDQeBsSJ9JHA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbS7Ut1%2FdJMcah5krko%2FxSY25Af8qdDQeBsSJ9JHA0%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;347&quot; height=&quot;500&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, JPA는 엔티티를 바로 DB에만 맡기는 것이 아니라, 먼저 이 영속성 컨텍스트 안에서 관리합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쉽게 말하면, 애플리케이션과 데이터베이스 사이에 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;가상의 중간 저장소&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 역할을 한다고 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 이 영속성 컨텍스트에 접근하는 주체가 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;EntityManager입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;EntityManager가 엔티티를 관리하고, 변경 내용을 추적하고, 필요한 시점에 DB에 반영하는 구조입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;영속 상태란 무엇일까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JPA에서는 엔티티의 상태를 크게 네 가지로 나눕니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;li&gt;삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 중에서 가장 중요한 것은 역시&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;영속 상태&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;영속 상태란, 엔티티가 영속성 컨텍스트 안에서 관리되고 있는 상태를 말합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 단순히 객체를 만든 것만으로는 영속이 아닙니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JPA가 이 객체를 &amp;ldquo;내가 관리하고 있는 엔티티&amp;rdquo;라고 인식해야 영속 상태가 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;왜 이게 중요할까요?&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;엔티티가 영속 상태가 되면 JPA가 그 객체의 변화를 추적할 수 있기 때문입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 개발자가 직접 UPDATE SQL을 작성하지 않아도, 영속성 컨텍스트 안에 있는 객체가 수정되면 JPA가 그 차이를 감지해서 DB에 반영해줄 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 부분이 처음에는 굉장히 신기하게 느껴졌습니다.&lt;br /&gt;객체를 수정했을 뿐인데, 커밋 시점에 SQL이 자동으로 나간다는 흐름이 바로 JPA가 가지는 강력한 장점 중 하나라고 생각했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;영속성 컨텍스트의 장점: 1차 캐시&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트가 주는 대표적인 장점 중 하나는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;1차 캐시&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;엔티티를 한 번 조회하면, 그 객체는 영속성 컨텍스트 안에 저장됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 상태에서 같은 트랜잭션 안에서 동일한 엔티티를 다시 조회하면, JPA는 DB에 다시 쿼리를 날리는 것이 아니라 먼저 영속성 컨텍스트 안에 있는 객체를 반환합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 같은 데이터를 여러 번 조회하더라도 매번 DB에 가지 않아도 되는 것입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 어떤&lt;span&gt;&amp;nbsp;&lt;/span&gt;Member를 한 번 조회한 뒤 같은 ID로 또 조회하면, 두 번째 조회는 DB가 아니라 영속성 컨텍스트의 1차 캐시에서 처리될 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 덕분에 성능적으로도 이점이 있고, 같은 트랜잭션 안에서는 같은 엔티티를 일관된 상태로 다룰 수 있다는 장점도 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;영속성 컨텍스트의 장점: 변경 감지(Dirty Checking)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;408&quot; data-origin-height=&quot;505&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/piX0A/dJMcah5krKU/3MKzm3aF837GzZ7L6PYCG1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/piX0A/dJMcah5krKU/3MKzm3aF837GzZ7L6PYCG1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/piX0A/dJMcah5krKU/3MKzm3aF837GzZ7L6PYCG1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpiX0A%2FdJMcah5krKU%2F3MKzm3aF837GzZ7L6PYCG1%2Fimg.jpg&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;336&quot; height=&quot;416&quot; data-origin-width=&quot;408&quot; data-origin-height=&quot;505&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JPA의 대표 기능 중 하나가 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;변경 감지(Dirty Checking)&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 기능 덕분에 영속 상태의 엔티티는 값을 수정하기만 해도, 트랜잭션이 끝나는 시점에 JPA가 변경 내용을 자동으로 감지합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 어떤 회원 엔티티를 조회한 뒤 이름을 바꿨다고 해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;Member member = em.find(Member.class, 1L);
member.setName(&quot;UMC10기최고&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 시점에서는 아직 DB에 바로 UPDATE가 나가지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 트랜잭션이 커밋되는 순간, JPA는 영속성 컨텍스트 안에 저장된 원본 상태와 현재 상태를 비교하고, 변경된 부분이 있으면 UPDATE SQL을 생성해서 실행합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 개발자는 객체를 수정했을 뿐인데, JPA는 그걸&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;DB 반영 대상 변경 사항&lt;/b&gt;&lt;/span&gt;으로 인식하는 것입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이걸 보고 나니 왜 JPA가 객체 지향적으로 데이터를 다룬다고 하는지 조금 더 이해가 됐습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;직접 SQL을 호출하는 방식이라기보다,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;객체 상태의 변화를 중심으로 DB를 업데이트하는 방식&lt;/b&gt;&lt;/span&gt;에 더 가깝기 때문입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;영속성 컨텍스트의 장점: 쓰기 지연 SQL 저장소&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트는 1차 캐시와 함께&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;쓰기 지연 SQL 저장소&lt;/b&gt;&lt;/span&gt;라는 개념도 가지고 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이름만 보면 조금 복잡하지만, 의미는 비교적 단순합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JPA는 엔티티를 저장하거나 수정할 때마다 즉시 DB에 SQL을 보내지 않고, 트랜잭션 커밋 시점까지 SQL을 모아둡니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;flush()&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;또는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;commit()&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;시점에 한꺼번에 실행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체는 먼저 영속성 컨텍스트에서 관리되고&lt;/li&gt;
&lt;li&gt;SQL은 쓰기 지연 저장소에 모였다가&lt;/li&gt;
&lt;li&gt;나중에 한 번에 DB로 나갑니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 JPA는 더 효율적으로 SQL을 관리할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;정리하면서 느낀 건, 1차 캐시와 쓰기 지연 저장소는 따로 떨어진 개념이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;영속성 컨텍스트가 객체를 관리하는 방식의 일부&lt;/b&gt;&lt;/span&gt;라는 점이었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;엔티티를 만들 때 왜 BaseEntity를 둘까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;엔티티를 설계하다 보면 여러 테이블에서 공통적으로 들어가는 필드들이 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;같은 필드들입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이걸 엔티티마다 계속 반복해서 작성하면 코드가 불필요하게 길어지고 관리도 어려워집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 보통은 이런 공통 필드를&lt;span&gt;&amp;nbsp;&lt;/span&gt;BaseEntity&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 상위 클래스로 분리해서 사용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 각 엔티티가 이를 상속받도록 설계합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 자주 함께 사용하는 어노테이션이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@MappedSuperclass&lt;/li&gt;
&lt;li&gt;@EntityListeners(AuditingEntityListener.class)&lt;/li&gt;
&lt;li&gt;@CreatedDate&lt;/li&gt;
&lt;li&gt;@LastModifiedDate&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이런 설정을 해두면 엔티티 저장이나 수정 시점에 생성일, 수정일을 자동으로 채울 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히&lt;span&gt;&amp;nbsp;&lt;/span&gt;@EnableJpaAuditing까지 함께 설정하면, 반복적으로 작성해야 하는 공통 필드를 훨씬 깔끔하게 관리할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4ySQ2/dJMcafNehXj/kR0AmiHDeb5qreD2w3CF70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4ySQ2/dJMcafNehXj/kR0AmiHDeb5qreD2w3CF70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4ySQ2/dJMcafNehXj/kR0AmiHDeb5qreD2w3CF70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4ySQ2%2FdJMcafNehXj%2FkR0AmiHDeb5qreD2w3CF70%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;360&quot; height=&quot;746&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 부분은 실제 프로젝트를 하다 보면 꽤 실용적으로 느껴집니다.&lt;br /&gt;작은 코드 차이 같지만, 엔티티가 많아질수록 공통 필드를 분리해두는 구조의 장점이 분명해지기 때문입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;엔티티 연관관계는 어떻게 설정할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JPA를 공부하면서 가장 헷갈리는 부분 중 하나가 연관관계 매핑입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 처음에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;@ManyToOne&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;@OneToMany&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;@OneToOne&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;mappedBy&lt;span&gt;, 연관관계의 주인 같은 개념이 한꺼번에 나와서 꽤 복잡하게 느껴집니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기본적으로 많이 사용하는 관계는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@ManyToOne&lt;/li&gt;
&lt;li&gt;@OneToMany&lt;/li&gt;
&lt;li&gt;@OneToOne&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 외래 키를 실제로 관리하는 쪽에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;@JoinColumn을 설정합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 중간 테이블이 있고, 그 테이블이 회원과 음식 카테고리를 참조한다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;@ManyToOne으로 두 엔티티를 연결할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 중요한 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;연관관계의 주인&lt;/b&gt;&lt;/span&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;데이터베이스에서는 FK가 있는 쪽이 관계를 관리하지만, 객체에서는 단순히 참조만 있다고 해서 자동으로 관계가 저장되는 것이 아닙니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 객체 세계에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;누가 이 관계를 실제로 DB에 반영할 것인지&lt;/b&gt;&lt;/span&gt;를 정해줘야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 지점이 객체와 테이블이 가장 다르게 느껴지는 부분 중 하나였습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;즉시 로딩과 지연 로딩&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;연관관계를 설정할 때 함께 고민해야 하는 것이 바로 로딩 방식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대표적으로 두 가지가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FetchType.EAGER&lt;/li&gt;
&lt;li&gt;FetchType.LAZY&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buLxPB/dJMb997hoEm/WOssHMBmqwtKtqARshnKgK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buLxPB/dJMb997hoEm/WOssHMBmqwtKtqARshnKgK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buLxPB/dJMb997hoEm/WOssHMBmqwtKtqARshnKgK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuLxPB%2FdJMb997hoEm%2FWOssHMBmqwtKtqARshnKgK%2Fimg.jpg&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;367&quot; height=&quot;508&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;즉시 로딩(EAGER)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉시 로딩은 말 그대로 연관된 객체를 한 번에 같이 조회하는 방식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;span&gt;&amp;nbsp;&lt;/span&gt;MemberFood&lt;span&gt;를 조회할 때, 연관된&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Member&lt;span&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Food&lt;span&gt;도 함께 가져옵니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 한 번에 다 가져오니까 편해 보일 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 필요하지 않은 데이터까지 함께 불러오게 되면 쿼리가 무거워질 수 있고, 상황에 따라 N+1 문제도 발생할 수 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;지연 로딩(LAZY)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지연 로딩은 연관 객체를 바로 조회하지 않고, 실제로 사용할 때 조회하는 방식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 처음에는 프록시 객체만 들고 있다가, 정말 필요한 시점에 DB 조회가 일어납니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 일반적으로는 즉시 로딩보다는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;지연 로딩을 기본으로 사용하는 것&lt;/b&gt;&lt;/span&gt;이 더 권장됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개인적으로도 공부하면서 느낀 건, 즉시 로딩은 편해 보여도 예상치 못한 쿼리를 만들 수 있어서 조심해야 하고, 지연 로딩은 의도를 더 명확하게 가져갈 수 있다는 점이었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;양방향 매핑은 왜 할까?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;연관관계는 단방향으로만 설정할 수도 있고, 양방향으로도 설정할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;양방향 매핑을 하면 장점이 분명히 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;348&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvtE58/dJMcahRK4Fc/3VZdkxauIktGC3v3TXKdy1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvtE58/dJMcahRK4Fc/3VZdkxauIktGC3v3TXKdy1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvtE58/dJMcahRK4Fc/3VZdkxauIktGC3v3TXKdy1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvtE58%2FdJMcahRK4Fc%2F3VZdkxauIktGC3v3TXKdy1%2Fimg.jpg&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;296&quot; height=&quot;432&quot; data-origin-width=&quot;348&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;첫 번째는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;그래프 탐색이 쉬워진다&lt;/b&gt;&lt;/span&gt;는 점입니다.&lt;br /&gt;즉, A에서 B를 보고, B에서도 다시 A를 볼 수 있게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두 번째는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;cascade 설정이 가능하다&lt;/b&gt;&lt;/span&gt;&lt;span&gt;는 점입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 회원이 삭제될 때 그 회원과 연결된 중간 엔티티들도 함께 삭제하고 싶다면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;cascade = CascadeType.REMOVE같은 설정을이용할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이런 경우에는 회원 하나만 삭제해도 연관된 데이터가 함께 정리됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다만 여기서 중요한 점은, 양방향 매핑과 cascade를 너무 쉽게 남용하면 안 된다는 것입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;잘못 설정하면 유저를 삭제했을 때 유저가 만든 게시글, 댓글, 연관된 다른 데이터까지 예상치 못하게 함께 삭제될 수 있기 때문입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 연관관계는 무조건 양방향으로 두는 것이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;정말 함께 관리되어야 하는 관계에만 신중하게 설정하는 것&lt;/b&gt;&lt;/span&gt;이 중요하다고 느꼈습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;JPQL이란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JPA를 사용하다 보면&lt;span&gt;&amp;nbsp;&lt;/span&gt;save()나&lt;span&gt;&amp;nbsp;&lt;/span&gt;findById()&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 기본 메서드만으로는 부족한 순간이 옵니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;조금 더 조건이 있는 조회나, 복잡한 검색이 필요해질 때 사용하는 것이 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;JPQL&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;317&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIDSQN/dJMcajaW586/4HmCMFMXSKpWo1HBrGePRk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIDSQN/dJMcajaW586/4HmCMFMXSKpWo1HBrGePRk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIDSQN/dJMcajaW586/4HmCMFMXSKpWo1HBrGePRk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIDSQN%2FdJMcajaW586%2F4HmCMFMXSKpWo1HBrGePRk%2Fimg.jpg&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;271&quot; height=&quot;434&quot; data-origin-width=&quot;317&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JPQL은 SQL과 비슷하지만, 차이가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SQL은 테이블을 대상으로 쿼리를 작성합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반면 JPQL은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;엔티티 객체를 대상으로 쿼리를 작성합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, DB 테이블 중심이 아니라 객체 중심의 쿼리 언어라고 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 부분이 JPA의 철학과도 잘 맞습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;데이터베이스를 직접 다루기보다, 엔티티를 중심으로 조회를 설계하는 방식이기 때문입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Repository에서 JPQL을 사용하는 방법&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Data JPA에서는 JPQL을 두 가지 방식으로 자주 사용합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;1. 메서드 이름으로 쿼리 생성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Data JPA는 메서드 이름만 보고도 쿼리를 생성해줍니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 이름이 &amp;ldquo;마크&amp;rdquo;이고, 삭제되지 않은 회원을 조회하고 싶다면 이런 식으로 메서드를 만들 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;findByNameAndDeletedAtIsNull&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러면 Spring Data JPA가 이 이름을 해석해서 조건에 맞는 JPQL을 자동으로 만들어줍니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 방식은 간단한 조회에서는 굉장히 편리합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;2. @Query 어노테이션 사용&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;조금 더 복잡한 조건이 필요하거나, 직접 쿼리를 제어하고 싶다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Query를 사용할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 방식은 조건이 복잡하거나 성능 최적화가 필요한 경우에 더 유리합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한&lt;span&gt;&amp;nbsp;&lt;/span&gt;nativeQuery = true를 사용하면 JPQL이 아니라 실제 SQL을 직접 실행할 수도 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, Repository는 단순히 기본 CRUD만 제공하는 것이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;메서드 이름 기반 조회와 직접 쿼리 작성까지 함께 지원하는 계층&lt;/b&gt;&lt;/span&gt;이라고 볼 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;이번 내용을 정리하며 느낀 점&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 JPA 내용을 정리하면서 가장 크게 느낀 점은, JPA는 단순히 SQL을 덜 쓰게 해주는 도구가 아니라는 점이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;save(),&lt;span&gt;&amp;nbsp;&lt;/span&gt;findById()가 편하다는 정도로만 느껴졌는데, 조금 더 들여다보니 그 안에는 객체와 데이터베이스 사이의 차이를 줄이기 위한 꽤 많은 설계가 들어 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 인상 깊었던 부분은 세 가지였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;첫 번째는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;영속성 컨텍스트&lt;/b&gt;&lt;/span&gt;입니다.&lt;br /&gt;처음엔 추상적이었지만, 1차 캐시와 변경 감지, 쓰기 지연이 모두 여기서 출발한다는 걸 이해하고 나니 JPA의 핵심이 훨씬 잘 보였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두 번째는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;지연 로딩의 중요성&lt;/b&gt;&lt;/span&gt;입니다.&lt;br /&gt;연관 객체를 무조건 함께 조회하는 것이 아니라, 필요한 시점에 가져오는 설계가 실무에서 왜 중요한지 조금 더 이해할 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;세 번째는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;연관관계 매핑이 생각보다 조심해야 할 부분이 많다&lt;/b&gt;&lt;/span&gt;는 점입니다.&lt;br /&gt;양방향 매핑과 cascade는 분명 편리하지만, 관계를 잘못 설계하면 예상치 못한 결과를 만들 수 있다는 점이 꽤 인상 깊었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 JPA가 왜 필요한지부터 시작해서, ORM의 개념, 영속성 컨텍스트, 1차 캐시와 변경 감지, 연관관계 매핑, 그리고 JPQL까지 기본적인 내용을 정리해보았습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;정리하고 보니 JPA는 단순히 데이터를 쉽게 저장하고 조회하는 기술이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;객체 지향 애플리케이션과 관계형 데이터베이스 사이의 간극을 줄여주는 핵심 기술&lt;/b&gt;&lt;/span&gt;이라고 느껴졌습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 영속성 컨텍스트를 중심으로 엔티티를 관리하고, 변경을 감지하고, 필요한 시점에 SQL을 실행하는 구조를 이해하니 그동안 무심코 사용했던&lt;span&gt;&amp;nbsp;&lt;/span&gt;save()나&lt;span&gt;&amp;nbsp;&lt;/span&gt;findById()&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 메서드도 조금 다르게 보이기 시작했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞으로 JPA를 사용할 때도 단순히 편리한 기능으로만 받아들이기보다,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;왜 이런 구조로 동작하는지&lt;/b&gt;&lt;/span&gt;를 함께 이해하면서 사용하는 습관을 가져야겠다고 생각했습니다.&lt;/p&gt;</description>
      <category>백엔드</category>
      <category>jpa</category>
      <category>ORM</category>
      <category>spring boot</category>
      <category>영속성 컨텍스트</category>
      <author>Archive_99</author>
      <guid isPermaLink="true">https://arch2ve-99.tistory.com/13</guid>
      <comments>https://arch2ve-99.tistory.com/13#entry13comment</comments>
      <pubDate>Wed, 29 Apr 2026 16:20:55 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot / 백엔드] API 요청부터 응답까지 - 회원가입 흐름으로 보는 Controller, Service, Repository</title>
      <link>https://arch2ve-99.tistory.com/12</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgTyMf/dJMcaiJTGXh/5IkjiXysqXBjCKJYUwsbTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgTyMf/dJMcaiJTGXh/5IkjiXysqXBjCKJYUwsbTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgTyMf/dJMcaiJTGXh/5IkjiXysqXBjCKJYUwsbTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgTyMf%2FdJMcaiJTGXh%2F5IkjiXysqXBjCKJYUwsbTk%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;1536&quot; height=&quot;1024&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;/b&gt;백엔드 개발을 하다 보면 Controller, Service, Repository를 나누는 이유는 어느 정도 익숙해집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 막상 API 하나를 구현하고 나면 이런 생각이 들 때가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;그래서 실제 요청이 들어오면, 이 계층들은 어떤 순서로 동작하는 걸까?&amp;rdquo;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저도 처음에는 Controller가 요청을 받고 Service가 로직을 처리하고 Repository가 저장한다는 정도로만 이해했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 회원가입 API처럼 요청값 검증도 들어가고, 여러 테이블 저장도 필요하고, 마지막에는 공통 응답 형식으로 감싸서 반환하는 흐름을 하나씩 따라가다 보니 각 계층이 왜 분리되어 있는지 훨씬 선명하게 보였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이번 글에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;회원가입 API 하나를 기준으로&lt;/b&gt;&lt;/span&gt;, 클라이언트가 요청을 보낸 순간부터 DB 저장, DTO 변환, 최종 응답 반환까지 어떤 흐름으로 이어지는지 정리해보려고 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서 다룰 내용은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트의 회원가입 요청이 어디로 들어오는가&lt;/li&gt;
&lt;li&gt;Spring은 어떤 컨트롤러 메서드를 실행하는가&lt;/li&gt;
&lt;li&gt;JSON 요청은 어떻게 DTO로 바뀌는가&lt;/li&gt;
&lt;li&gt;@Valid&lt;span&gt;는 어떤 방식으로 요청을 검증하는가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Service에서는 어떤 비즈니스 로직이 실행되는가&lt;/li&gt;
&lt;li&gt;Repository는 어떤 데이터를 저장하고 조회하는가&lt;/li&gt;
&lt;li&gt;응답 DTO와 ApiResponse는 어떻게 만들어지는가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;기준이 되는 회원가입 API&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서 기준으로 볼 API는 아래 메서드입니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;@PostMapping(&quot;/signup&quot;)
public ResponseEntity&amp;lt;ApiResponse&amp;lt;MemberSignUpResponse&amp;gt;&amp;gt; signUp(
        @Valid @RequestBody MemberSignUpRequest request
) {
    MemberSignUpResponse response = memberService.signUp(request);
    return ResponseEntity.status(GeneralSuccessCode.CREATED.getHttpStatus())
            .body(ApiResponse.onSuccess(GeneralSuccessCode.CREATED, response));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;겉으로 보면 코드가 아주 길지는 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이 짧은 메서드 안에는 요청 매핑, JSON 변환, 유효성 검증, 서비스 호출, 공통 응답 생성까지 API의 핵심 흐름이 모두 들어 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번에는 이 흐름을 위에서 아래로, 순서대로 따라가보겠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;회원가입 요청의 전체 흐름부터 먼저 보기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;회원가입 요청이 왔을 때 큰 흐름은 이렇게 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클라이언트&lt;/b&gt;&lt;span&gt;&lt;br /&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;MemberController&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;br /&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;MemberService&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;br /&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;MemberRepository / FoodCategoryRepository / MemberFoodPreferenceRepository&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;br /&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;DB 저장&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;br /&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;MemberConverter&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;br /&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;ApiResponse&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;br /&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;클라이언트 응답&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2766&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caPaPH/dJMcajonjb5/UvpBpUikJ7CvvQC8SFaDeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caPaPH/dJMcajonjb5/UvpBpUikJ7CvvQC8SFaDeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caPaPH/dJMcajonjb5/UvpBpUikJ7CvvQC8SFaDeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaPaPH%2FdJMcajonjb5%2FUvpBpUikJ7CvvQC8SFaDeK%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;892&quot; height=&quot;138&quot; data-origin-width=&quot;2766&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조를 보면 요청이 한 번에 DB로 가는 것이 아니라, 각 계층을 지나면서 역할이 분리되어 있다는 걸 알 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller는 요청과 응답을 담당하고&lt;/li&gt;
&lt;li&gt;Service는 회원가입 로직을 수행하고&lt;/li&gt;
&lt;li&gt;Repository는 DB와 소통하고&lt;/li&gt;
&lt;li&gt;Converter는 응답 DTO를 만들고&lt;/li&gt;
&lt;li&gt;ApiResponse는 공통 응답 형식으로 감싸는 역할을 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, API 흐름을 이해한다는 것은 단순히 메서드 호출 순서를 외우는 것이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;각 계층이 어느 시점에 어떤 책임을 수행하는지 이해하는 것&lt;/b&gt;&lt;/span&gt;에 가깝다고 생각했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;1. 클라이언트가 회원가입 요청을 보낸다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 클라이언트는 회원가입을 위해 다음 주소로 POST 요청을 보냅니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;POST /api/v1/members/signup&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;요청 body에는 JSON 데이터가 담깁니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;{
  &quot;email&quot;: &quot;test@example.com&quot;,
  &quot;password&quot;: &quot;password123&quot;,
  &quot;nickname&quot;: &quot;mingyu&quot;,
  &quot;gender&quot;: &quot;MALE&quot;,
  &quot;birthDate&quot;: &quot;2000-01-01&quot;,
  &quot;address&quot;: &quot;Seoul&quot;,
  &quot;foodCategoryIds&quot;: [1, 2]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 주소가 만들어지는 방식도 한 번 보면 이해가 쉽습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클래스의&lt;span&gt;&amp;nbsp;&lt;/span&gt;@RequestMapping&lt;span&gt;에서 공통 경로가 정해지고,&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;@RequestMapping(&quot;/api/v1/members&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;메서드의&lt;span&gt;&amp;nbsp;&lt;/span&gt;@PostMapping&lt;span&gt;에서 세부 경로가 붙습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;@PostMapping(&quot;/signup&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 최종 주소는&lt;span&gt;&amp;nbsp;&lt;/span&gt;/api/v1/members/signup&lt;span&gt;이 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 부분은 사소해 보이지만, 실제로 API를 읽을 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;공통 경로는 클래스&lt;/b&gt;&lt;/span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;세부 경로는 메서드&lt;/b&gt;&lt;/span&gt;에서 결정된다는 흐름을 알고 있으면 컨트롤러 구조를 훨씬 빨리 파악할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;2. Spring이 요청을 받을 컨트롤러를 찾는다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트가 요청을 보내면 그다음은 Spring이 처리합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring은 요청의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;HTTP Method&lt;/b&gt;&lt;/span&gt;&lt;span&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;URL&lt;/b&gt;&lt;/span&gt;&lt;span&gt;을 보고 어떤 메서드를 실행할지 찾습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 요청은&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Method:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;POST&lt;/li&gt;
&lt;li&gt;&lt;span&gt;URL:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;/api/v1/members/signup&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이므로 아래 메서드와 매칭됩니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;@PostMapping(&quot;/signup&quot;)
public ResponseEntity&amp;lt;ApiResponse&amp;lt;MemberSignUpResponse&amp;gt;&amp;gt; signUp(...)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, Spring은 이 요청을&lt;span&gt;&amp;nbsp;&lt;/span&gt;MemberController.signUp()이 받아야 한다고 판단하고, 이제 메서드를 실행할 준비를 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 과정을 보면 Controller는 단순히 코드를 모아두는 클래스가 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;HTTP 요청과 자바 메서드를 연결해주는 진입점&lt;/b&gt;&lt;/span&gt;이라는 걸 알 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;3. JSON 요청을 DTO로 변환한다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;컨트롤러의 파라미터를 보면 이렇게 되어 있습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;@Valid @RequestBody MemberSignUpRequest request&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 먼저 동작하는 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;@RequestBody&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@RequestBody는 요청 body에 담긴 JSON을 자바 객체로 변환해주는 역할을 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 아까 클라이언트가 보낸 이 JSON이&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;{
  &quot;email&quot;: &quot;test@example.com&quot;,
  &quot;password&quot;: &quot;password123&quot;,
  &quot;nickname&quot;: &quot;mingyu&quot;,
  &quot;gender&quot;: &quot;MALE&quot;,
  &quot;birthDate&quot;: &quot;2000-01-01&quot;,
  &quot;address&quot;: &quot;Seoul&quot;,
  &quot;foodCategoryIds&quot;: [1, 2]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 객체로 바뀌는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;MemberSignUpRequest request&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이후 컨트롤러나 서비스에서는 이런 식으로 값을 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;request.email()
request.password()
request.nickname()
request.gender()
request.birthDate()
request.address()
request.foodCategoryIds()&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 흐름을 보고 나면 DTO가 왜 필요한지도 더 잘 보입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DTO는 단순히 데이터를 담는 객체가 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;클라이언트가 어떤 구조의 데이터를 보내야 하는지 명확하게 정의하는 역할&lt;/b&gt;&lt;/span&gt;을 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;4. 요청값 검증이 진행된다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DTO 변환이 끝나면&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Valid가 동작합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;회원가입 요청 DTO는 다음과 같이 정의되어 있습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;public record MemberSignUpRequest(
        @NotBlank
        @Email
        @Size(max = 100)
        String email,

        @NotBlank
        @Size(min = 8, max = 255)
        String password,

        @NotBlank
        @Size(max = 20)
        String nickname,

        @NotNull
        MemberGender gender,

        @NotNull
        @Past
        LocalDate birthDate,

        @NotBlank
        @Size(max = 100)
        String address,

        @NotEmpty
        List&amp;lt;Long&amp;gt; foodCategoryIds
) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 안에는 각 필드에 대한 검증 조건이 들어 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;email은 비어 있으면 안 되고, 이메일 형식이어야 하며, 100자 이하여야 합니다.&lt;/li&gt;
&lt;li&gt;password는 비어 있으면 안 되고, 8자 이상이어야 합니다.&lt;/li&gt;
&lt;li&gt;nickname은 비어 있으면 안 되고, 20자 이하여야 합니다.&lt;/li&gt;
&lt;li&gt;birthDate는 null이면 안 되고, 과거 날짜여야 합니다.&lt;/li&gt;
&lt;li&gt;foodCategoryIds는 null도 안 되고, 비어 있어도 안 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 컨트롤러 메서드 내부로 들어가기 전에 Spring이 먼저 &amp;ldquo;이 요청이 유효한가?&amp;rdquo;를 검사하는 것입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 이런 요청은 검증에 실패합니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;{
  &quot;email&quot;: &quot;wrong-email&quot;,
  &quot;password&quot;: &quot;123&quot;,
  &quot;nickname&quot;: &quot;&quot;,
  &quot;gender&quot;: null,
  &quot;birthDate&quot;: &quot;2999-01-01&quot;,
  &quot;address&quot;: &quot;&quot;,
  &quot;foodCategoryIds&quot;: []
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;signUp()&lt;span&gt;&amp;nbsp;&lt;/span&gt;메서드 내부는 아예 실행되지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대신&lt;span&gt;&amp;nbsp;&lt;/span&gt;MethodArgumentNotValidException&lt;span&gt;이 발생하고, 전역 예외 처리기인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;GlobalExceptionHandler&lt;span&gt;가 이를 처리하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결과적으로 클라이언트는 400 Bad Request 응답을 받습니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;{
  &quot;isSuccess&quot;: false,
  &quot;code&quot;: &quot;COMMON400&quot;,
  &quot;message&quot;: &quot;잘못된 요청입니다.&quot;,
  &quot;result&quot;: null
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 부분은 API 흐름에서 꽤 중요합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;검증은 Service에 들어가기 전에 끝나기 때문에, Service는 이미&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;기본적으로 유효한 요청이 들어온다는 전제&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;위에서 비즈니스 로직에 집중할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;5. 검증이 끝나면 컨트롤러 메서드가 실행된다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;검증에 성공하면 이제 컨트롤러 메서드 본문이 실행됩니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;MemberSignUpResponse response = memberService.signUp(request);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;컨트롤러는 여기서 직접 DB에 저장하지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제 회원가입 비즈니스 로직은&lt;span&gt;&amp;nbsp;&lt;/span&gt;MemberService에게 맡깁니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 이 시점에서 컨트롤러가 하는 일은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;li&gt;응답 반환하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조를 보고 나면 Controller는 정말로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;입구와 출구 역할&lt;/b&gt;&lt;/span&gt;에 가깝다는 걸 알 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;비즈니스 로직이 길어질수록 Controller가 아니라 Service가 중심이 되어야 하는 이유도 여기서 드러납니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;6. MemberService에서 실제 회원가입 로직이 실행된다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서비스의 회원가입 메서드는 대략 이런 구조입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public MemberSignUpResponse signUp(MemberSignUpRequest request) {
    Member member = Member.create(
            request.email(),
            request.password(),
            request.nickname(),
            request.gender(),
            request.birthDate(),
            request.address()
    );
    Member savedMember = memberRepository.save(member);

    List&amp;lt;FoodCategory&amp;gt; foodCategories = foodCategoryRepository.findAllById(request.foodCategoryIds());
    if (foodCategories.size() != request.foodCategoryIds().size()) {
        throw new FoodCategoryException(FoodCategoryErrorCode.FOOD_CATEGORY_NOT_FOUND);
    }

    List&amp;lt;MemberFoodPreference&amp;gt; preferences = foodCategories.stream()
            .map(foodCategory -&amp;gt; MemberFoodPreference.create(savedMember, foodCategory))
            .toList();
    memberFoodPreferenceRepository.saveAll(preferences);
    savedMember.getFoodPreferences().addAll(preferences);

    return memberConverter.toSignUpResponse(savedMember);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 메서드에서 먼저 눈에 띄는 부분은&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Transactional입니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;@Transactional&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 어노테이션 덕분에 회원 저장과 음식 선호 저장이 하나의 트랜잭션으로 묶입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 중간에 예외가 발생하면 앞에서 실행한 작업도 모두 롤백됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 회원은 저장했는데 음식 카테고리 검증에서 실패했다면, 회원 데이터도 최종적으로는 저장되지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 점이 중요한 이유는 회원가입이 단순히 member 테이블 하나만 저장하는 작업이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;여러 저장 작업이 하나의 논리적 동작으로 묶여 있기 때문&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;7. 먼저 Member 엔티티를 만든다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서비스는 가장 먼저 전달받은 request DTO를 회원 엔티티로 변환합니다.&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;Member member = Member.create(
        request.email(),
        request.password(),
        request.nickname(),
        request.gender(),
        request.birthDate(),
        request.address()
);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서&lt;span&gt;&amp;nbsp;&lt;/span&gt;Member.create()&lt;span&gt;는 정적 팩토리 메서드입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static Member create(
        String email,
        String password,
        String nickname,
        MemberGender gender,
        LocalDate birthDate,
        String address
) {
    Member member = new Member();
    member.email = email;
    member.password = password;
    member.nickname = nickname;
    member.gender = gender;
    member.birthDate = birthDate;
    member.address = address;
    member.totalPoint = 0;
    member.status = MemberStatus.ACTIVE;
    return member;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 메서드 안에서는 요청값을 엔티티에 채워 넣는 것뿐 아니라, 회원의 기본 상태도 함께 설정합니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;member.totalPoint = 0;
member.status = MemberStatus.ACTIVE;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 신규 회원은 가입 시점에 자동으로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;totalPoint = 0&lt;/li&gt;
&lt;li&gt;status = ACTIVE&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;상태로 시작하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 과정을 보면 Service는 단순히 저장 버튼만 누르는 계층이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;회원가입이라는 비즈니스 규칙을 실제 엔티티에 반영하는 계층&lt;/b&gt;&lt;/span&gt;이라는 점이 잘 드러납니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;8. memberRepository가 회원을 DB에 저장한다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그다음 서비스는 memberRepository를 통해 회원을 저장합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Member savedMember = memberRepository.save(member);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;memberRepository&lt;span&gt;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;JpaRepository&amp;lt;Member, Long&amp;gt;&lt;span&gt;를 상속한 인터페이스입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서&lt;span&gt;&amp;nbsp;&lt;/span&gt;save()를 호출하면 JPA가 내부적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;member&lt;span&gt;&amp;nbsp;&lt;/span&gt;테이블에 INSERT를 실행합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이후 저장이 끝나면 DB에서 생성된 기본 키가&lt;span&gt;&amp;nbsp;&lt;/span&gt;savedMember&lt;span&gt;&amp;nbsp;&lt;/span&gt;안에 들어가게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 데이터가 저장될 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;member_id: 1&lt;/li&gt;
&lt;li&gt;email: test@example.com&lt;/li&gt;
&lt;li&gt;password: password123&lt;/li&gt;
&lt;li&gt;nickname: mingyu&lt;/li&gt;
&lt;li&gt;gender: MALE&lt;/li&gt;
&lt;li&gt;birth_date: 2000-01-01&lt;/li&gt;
&lt;li&gt;address: Seoul&lt;/li&gt;
&lt;li&gt;total_point: 0&lt;/li&gt;
&lt;li&gt;status: ACTIVE&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 단계에서 중요한 점은 Repository가 단순히 &amp;ldquo;데이터를 보관하는 곳&amp;rdquo;이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;Service가 만든 엔티티를 실제 DB와 연결해주는 계층&lt;/b&gt;&lt;/span&gt;이라는 점입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;9. 음식 카테고리가 실제로 존재하는지 확인한다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;회원가입 요청에는 음식 카테고리 ID 목록도 들어 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;&quot;foodCategoryIds&quot;: [1, 2]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서비스는 이 값들이 실제 DB에 존재하는지 확인해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서&lt;span&gt;&amp;nbsp;&lt;/span&gt;foodCategoryRepository.findAllById(...)&lt;span&gt;를 호출합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;List&amp;lt;FoodCategory&amp;gt; foodCategories =
        foodCategoryRepository.findAllById(request.foodCategoryIds());&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 요청한 개수와 실제 조회된 개수를 비교합니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;if (foodCategories.size() != request.foodCategoryIds().size()) {
    throw new FoodCategoryException(FoodCategoryErrorCode.FOOD_CATEGORY_NOT_FOUND);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 요청은&lt;span&gt;&amp;nbsp;&lt;/span&gt;[1, 2, 999]였는데 DB에는 1, 2만 존재한다면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청한 ID 개수는 3개&lt;/li&gt;
&lt;li&gt;실제 조회된 카테고리는 2개&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가 되므로, 없는 카테고리가 포함되었다고 판단하고 예외를 던집니다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;throw new FoodCategoryException(FoodCategoryErrorCode.FOOD_CATEGORY_NOT_FOUND);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 예외는 도메인 예외이기 때문에 전역 예외 처리기에서 받아 처리합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;응답은 대략 이런 형식이 됩니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;{
  &quot;isSuccess&quot;: false,
  &quot;code&quot;: &quot;FOOD404&quot;,
  &quot;message&quot;: &quot;Food category not found&quot;,
  &quot;result&quot;: null
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Transactional&lt;span&gt;&amp;nbsp;&lt;/span&gt;덕분에 앞에서 저장했던 회원 정보도 롤백됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 부분을 보면 검증은 두 단계로 나뉜다는 걸 알 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;형식 검증은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;@Valid&lt;/li&gt;
&lt;li&gt;비즈니스 검증은 Service&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;빈 값이냐&amp;rdquo; 같은 기본 검증은 DTO에서 하고, &amp;ldquo;실제로 존재하는 음식 카테고리냐&amp;rdquo; 같은 도메인 검증은 Service에서 하는 구조입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;10. 회원과 음식 카테고리를 연결하는 엔티티를 만든다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;음식 카테고리가 모두 존재한다면, 이제 회원과 음식 카테고리를 연결하는 엔티티를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;List&amp;lt;MemberFoodPreference&amp;gt; preferences = foodCategories.stream()
        .map(foodCategory -&amp;gt; MemberFoodPreference.create(savedMember, foodCategory))
        .toList();&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서&lt;span&gt;&amp;nbsp;&lt;/span&gt;MemberFoodPreference는 회원과 음식 카테고리 사이를 연결하는 역할을 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 회원이 어떤 음식 취향을 선택했는지 저장하는 중간 엔티티라고 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 회원 ID가 1이고, 카테고리 ID가 1과 2라면 이런 관계가 만들어집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;member_id: 1, food_category_id: 1&lt;/li&gt;
&lt;li&gt;member_id: 1, food_category_id: 2&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 흐름은 &amp;ldquo;회원은 여러 음식 카테고리를 선호할 수 있다&amp;rdquo;는 비즈니스 규칙이 실제 DB 구조로 어떻게 표현되는지를 보여주는 좋은 예라고 생각했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;11. 회원 음식 선호 데이터를 DB에 저장한다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;생성한 연결 엔티티들은&lt;span&gt;&amp;nbsp;&lt;/span&gt;saveAll()로 한 번에 저장합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;memberFoodPreferenceRepository.saveAll(preferences);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;saveAll()은 여러 엔티티를 한 번에 저장하는 메서드입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 회원이 선택한 카테고리 개수만큼&lt;span&gt;&amp;nbsp;&lt;/span&gt;member_food_preference&lt;span&gt;&amp;nbsp;&lt;/span&gt;테이블에 row가 생성됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 단계까지 오면 비로소 회원가입에 필요한 주요 데이터 저장이 완료된 셈입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;12. 메모리 안의 연관관계도 맞춰준다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그다음 코드가 조금 흥미롭습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;savedMember.getFoodPreferences().addAll(preferences);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 코드는 DB 저장과는 별개로, 현재 자바 메모리 안에 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;savedMember&lt;span&gt;&amp;nbsp;&lt;/span&gt;객체에도 방금 저장한 음식 선호 목록을 넣어주는 역할을 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;왜 필요할까요?&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;바로 다음 단계에서 응답 DTO를 만들 때 이 값을 사용하기 때문입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #000000; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;return memberConverter.toSignUpResponse(savedMember);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제로 컨버터 내부에서는 이렇게 음식 카테고리 ID를 꺼냅니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;List&amp;lt;Long&amp;gt; foodCategoryIds = member.getFoodPreferences().stream()
        .map(MemberFoodPreference::getFoodCategory)
        .map(foodCategory -&amp;gt; foodCategory.getId())
        .toList();&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, DB에는 저장이 되어 있어도 현재 메모리 안의&lt;span&gt;&amp;nbsp;&lt;/span&gt;savedMember&lt;span&gt;&amp;nbsp;&lt;/span&gt;객체에 연관 목록이 비어 있다면 응답 DTO를 만들 때 문제가 생길 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이 코드는 단순한 덧붙이기가 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;응답 변환 시점에 필요한 연관관계를 메모리에서도 맞춰주는 작업&lt;/b&gt;&lt;/span&gt;이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개인적으로는 이 부분이 API 흐름을 볼 때 꽤 중요하다고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DB 저장만 끝났다고 모든 게 끝나는 것이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;현재 객체 상태도 응답 생성에 맞게 정리해야 한다&lt;/b&gt;&lt;/span&gt;는 점이 드러나기 때문입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;13. MemberConverter가 응답 DTO를 만든다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이제 서비스는&lt;span&gt;&amp;nbsp;&lt;/span&gt;MemberConverter를 사용해 엔티티를 응답 DTO로 바꿉니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;return memberConverter.toSignUpResponse(savedMember);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;컨버터는 대략 이런 식으로 동작합니다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;public MemberSignUpResponse toSignUpResponse(Member member) {
    List&amp;lt;Long&amp;gt; foodCategoryIds = member.getFoodPreferences().stream()
            .map(MemberFoodPreference::getFoodCategory)
            .map(foodCategory -&amp;gt; foodCategory.getId())
            .toList();

    return new MemberSignUpResponse(
            member.getId(),
            member.getEmail(),
            member.getNickname(),
            member.getGender(),
            member.getBirthDate(),
            member.getAddress(),
            foodCategoryIds,
            member.getStatus(),
            member.getTotalPoint()
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 엔티티를 그대로 응답으로 내보내지 않는다는 것입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이유는 다음과 같습니다&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 클라이언트에게 필요한 값만 내려주기 위해&lt;br /&gt;2. DB 구조와 API 응답 구조를 분리하기 위해&lt;br /&gt;3. 민감한 값이나 불필요한 연관관계가 노출되는 것을 막기 위해&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제로 현재 응답 DTO에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;password가 없습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 회원가입에 비밀번호를 사용했더라도 응답에는 포함되지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 부분을 보면 DTO와 Converter가 왜 필요한지도 더 분명해집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Entity는 DB 중심 객체&lt;/li&gt;
&lt;li&gt;DTO는 API 중심 객체&lt;/li&gt;
&lt;li&gt;Converter는 그 둘을 연결하는 객체&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 역할 분리가 있기 때문에 응답 구조를 더 안전하게 제어할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;14. 다시 컨트롤러로 돌아와 공통 응답 형식으로 감싼다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서비스가&lt;span&gt;&amp;nbsp;&lt;/span&gt;MemberSignUpResponse를 반환하면, 흐름은 다시 컨트롤러로 돌아옵니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;MemberSignUpResponse response = memberService.signUp(request);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그다음 컨트롤러는 이 응답 DTO를 공통 응답 형식으로 감쌉니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;return ResponseEntity.status(GeneralSuccessCode.CREATED.getHttpStatus())
        .body(ApiResponse.onSuccess(GeneralSuccessCode.CREATED, response));&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서&lt;span&gt;&amp;nbsp;&lt;/span&gt;GeneralSuccessCode.CREATED는 대략 이런 의미를 가집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP Status: 201 Created&lt;/li&gt;
&lt;li&gt;code: COMMON201&lt;/li&gt;
&lt;li&gt;message: 생성되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 컨트롤러는 서비스가 만들어준 실제 응답 데이터를&lt;span&gt;&amp;nbsp;&lt;/span&gt;ApiResponse에 넣고, 동시에 HTTP 상태 코드도&lt;span&gt;&amp;nbsp;&lt;/span&gt;201 Created로 설정하는 것입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 흐름을 보면 ApiResponse는 응답 구조를 통일하는 역할을 하고, ResponseEntity는 HTTP 상태 코드를 제어하는 역할을 한다는 것도 함께 이해할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;15. 최종적으로 클라이언트는 이런 응답을 받는다&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;회원가입이 성공하면 클라이언트는 이런 응답을 받게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;{
  &quot;isSuccess&quot;: true,
  &quot;code&quot;: &quot;COMMON201&quot;,
  &quot;message&quot;: &quot;생성되었습니다.&quot;,
  &quot;result&quot;: {
    &quot;memberId&quot;: 1,
    &quot;email&quot;: &quot;test@example.com&quot;,
    &quot;nickname&quot;: &quot;mingyu&quot;,
    &quot;gender&quot;: &quot;MALE&quot;,
    &quot;birthDate&quot;: &quot;2000-01-01&quot;,
    &quot;address&quot;: &quot;Seoul&quot;,
    &quot;foodCategoryIds&quot;: [1, 2],
    &quot;status&quot;: &quot;ACTIVE&quot;,
    &quot;totalPoint&quot;: 0
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 응답을 보면 지금까지의 흐름이 한 번에 정리됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;isSuccess&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;code&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;message&lt;span&gt;는 공통 응답 포맷&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;result&lt;span&gt;는 회원가입 결과 데이터&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;password&lt;span&gt;는 응답에 포함되지 않음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;상태 코드는 201 Created&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, API는 단순히 저장만 하고 끝나는 것이 아니라, 마지막까지&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;클라이언트가 사용하기 좋은 형태로 데이터를 정리해서 반환하는 과정&lt;/b&gt;&lt;/span&gt;이라고 볼 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;예외가 발생하면 흐름은 어떻게 달라질까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;회원가입 API는 성공 흐름도 중요하지만, 실패 흐름도 함께 봐야 더 잘 이해됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 잘못된 이메일 형식이나 빈 닉네임처럼 요청 자체가 잘못된 경우에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Valid&lt;span&gt;&amp;nbsp;&lt;/span&gt;단계에서 걸립니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 경우에는 Service까지 가지 않고 바로 예외가 발생하며, 전역 예외 처리기가 400 응답을 반환합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반대로 요청 형식은 맞지만 존재하지 않는 음식 카테고리 ID가 들어온 경우에는 Service 내부에서 예외가 발생합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 경우에는 이미 member 저장이 시도되었더라도&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Transactional&lt;span&gt;&amp;nbsp;&lt;/span&gt;덕분에 전체 작업이 롤백됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 실패 흐름도 단계에 따라 나뉩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;형식이 잘못된 요청 &amp;rarr; 컨트롤러 진입 전 검증 실패&lt;/li&gt;
&lt;li&gt;비즈니스 규칙 위반 &amp;rarr; 서비스 내부 예외 발생&lt;/li&gt;
&lt;li&gt;둘 다 최종적으로는 공통 에러 응답 형식으로 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 부분을 보면 예외 처리도 API 흐름의 바깥이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;흐름 안에 포함된 중요한 설계 요소&lt;/b&gt;&lt;/span&gt;라는 생각이 들었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;전체 흐름을 다시 한 번 정리하면&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;회원가입 API의 전체 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;클라이언트가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;POST /api/v1/members/signup&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;요청을 보낸다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Spring이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;MemberController.signUp()&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메서드를 찾는다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;@RequestBody&lt;span&gt;가 JSON을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;MemberSignUpRequest&lt;span&gt;로 변환한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;@Valid&lt;span&gt;가 DTO 필드 검증을 수행한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;검증에 성공하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;memberService.signUp(request)&lt;span&gt;를 호출한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Member.create()&lt;span&gt;로 회원 엔티티를 생성한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;memberRepository.save(member)&lt;span&gt;로 회원을 저장한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;foodCategoryRepository.findAllById(...)&lt;span&gt;로 음식 카테고리를 조회한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;요청한 카테고리 개수와 조회된 개수가 다르면 예외를 발생시키고 롤백한다&lt;/li&gt;
&lt;li&gt;MemberFoodPreference.create(...)&lt;span&gt;로 회원-카테고리 연결 엔티티를 만든다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;memberFoodPreferenceRepository.saveAll(...)&lt;span&gt;로 선호 카테고리를 저장한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;savedMember.getFoodPreferences().addAll(...)&lt;span&gt;로 메모리 안의 연관 목록도 맞춘다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;memberConverter.toSignUpResponse(...)&lt;span&gt;로 응답 DTO를 만든다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;ApiResponse.onSuccess(...)&lt;span&gt;로 공통 응답 body를 만든다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;ResponseEntity.status(201)&lt;span&gt;로 HTTP 상태 코드를 지정한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;클라이언트에게 JSON 응답을 반환한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 흐름을 따라가 보니 Controller, Service, Repository를 왜 나누는지가 훨씬 선명해졌습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;이번 내용을 정리하며 느낀 점&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 회원가입 API 흐름을 정리하면서 가장 크게 느낀 점은, API 하나가 생각보다 많은 단계를 거쳐 동작한다는 점이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 Controller에서 요청을 받고 Service에서 저장하면 끝이라고 단순하게 생각했는데, 실제로는 그 사이에&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JSON &amp;rarr; DTO 변환&lt;/li&gt;
&lt;li&gt;유효성 검증&lt;/li&gt;
&lt;li&gt;트랜잭션 처리&lt;/li&gt;
&lt;li&gt;도메인 검증&lt;/li&gt;
&lt;li&gt;여러 Repository 호출&lt;/li&gt;
&lt;li&gt;Entity &amp;rarr; DTO 변환&lt;/li&gt;
&lt;li&gt;공통 응답 포맷 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;같은 단계들이 차례대로 이어지고 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 인상 깊었던 부분은 두 가지였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;첫 번째는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;검증이 두 단계로 나뉜다&lt;/b&gt;&lt;/span&gt;는 점입니다.&lt;br /&gt;형식 검증은&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Valid가 처리하고, 비즈니스 검증은 Service가 처리한다는 점이 API 구조를 훨씬 깔끔하게 만들어준다고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두 번째는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;응답도 마지막까지 설계의 일부라는 점&lt;/b&gt;&lt;/span&gt;입니다.&lt;br /&gt;DB 저장이 끝났다고 API가 끝나는 것이 아니라, DTO 변환과 공통 응답 포맷 적용까지 완료되어야 비로소 하나의 API가 완성된다는 점이 더 선명하게 보였습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 회원가입 API를 기준으로, 클라이언트의 요청이 들어온 순간부터 최종 응답이 반환되기까지의 전체 흐름을 정리해보았습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Controller는 요청과 응답을 담당하고, Service는 회원가입 비즈니스 로직을 수행하고, Repository는 DB 저장과 조회를 담당합니다.&lt;br /&gt;그리고 Entity는 DB와 연결되는 객체이고, DTO는 API 요청과 응답을 위한 객체이며, Converter는 그 둘을 안전하게 이어주는 역할을 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국 API 흐름을 이해한다는 것은 단순히 &amp;ldquo;어느 메서드가 먼저 실행되는가&amp;rdquo;를 외우는 것이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;각 계층이 어떤 책임을 가지고 협력하는지 이해하는 것&lt;/b&gt;&lt;/span&gt;이라고 생각합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;회원가입 API 하나만 제대로 따라가 봐도, 스프링 백엔드의 전체 구조가 왜 이렇게 설계되는지 훨씬 더 잘 보이는 것 같습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>백엔드</category>
      <category>API</category>
      <category>Controller</category>
      <category>repository</category>
      <category>service</category>
      <category>springboot</category>
      <author>Archive_99</author>
      <guid isPermaLink="true">https://arch2ve-99.tistory.com/12</guid>
      <comments>https://arch2ve-99.tistory.com/12#entry12comment</comments>
      <pubDate>Thu, 23 Apr 2026 10:38:39 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot / 백엔드] 전역 에러 핸들러 정리 - 예외 처리와 응답 형식 통일하기</title>
      <link>https://arch2ve-99.tistory.com/11</link>
      <description>&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9wNzw/dJMcadaBfMq/qH2xZiTjxDCGlPT2ViL93K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9wNzw/dJMcadaBfMq/qH2xZiTjxDCGlPT2ViL93K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9wNzw/dJMcadaBfMq/qH2xZiTjxDCGlPT2ViL93K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9wNzw%2FdJMcadaBfMq%2FqH2xZiTjxDCGlPT2ViL93K%2Fimg.jpg&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;1685&quot; height=&quot;1128&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;백엔드에서 API를 구현하다 보면 정상적인 요청만 들어오는 경우보다, 예상하지 못한 예외 상황을 더 자주 마주하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저도 처음에는 API가 잘 동작하는지만 확인하는 데 집중해서, 예외 상황은 나중에 생각해도 된다고 여겼습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 실제로는 사용자가 잘못된 값을 보내거나, DB에서 데이터를 찾지 못하거나, 서버 내부에서 예상치 못한 에러가 발생하는 경우가 꽤 자주 생깁니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;문제는 이런 예외가 발생했을 때 아무 설정 없이 두면, 우리가 평소에 응답 통일을 위해 만들어둔 형식과 전혀 다른 형태로 응답이 나갈 수 있다는 점입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 성공했을 때는 공통 응답 객체로 잘 내려가다가도, 예외가 발생하면 갑자기 스프링 기본 에러 응답이 나가버리는 상황이 생길 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이번 글에서는 워크북 내용을 바탕으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;왜 전역 에러 핸들러가 필요한지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;예외를 어떤 구조로 관리할 수 있는지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;, 그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;예외가 발생해도 응답 형식을 통일하는 방법&lt;/b&gt;&lt;/span&gt;&lt;span&gt;을 정리해보려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서 다룰 내용은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;@RestControllerAdvice&lt;span&gt;는 어떤 역할을 하는가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;프로젝트 공통 예외와 도메인 예외를 왜 나누는가&lt;/li&gt;
&lt;li&gt;에러 코드와 함께 예외를 처리하는 방식은 무엇인가&lt;/li&gt;
&lt;li&gt;예외가 발생해도 응답 형식을 통일해야 하는 이유는 무엇인가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;왜 에러 핸들링이 필요할까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;API를 만들 때는 보통 &amp;ldquo;정상 요청이 들어왔을 때 어떤 데이터를 반환할까?&amp;rdquo;를 먼저 생각하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제 서비스에서는 정상 흐름만 존재하지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 이런 상황들이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;존재하지 않는 회원 ID로 조회를 시도하는 경우&lt;/li&gt;
&lt;li&gt;필수 요청값이 누락된 경우&lt;/li&gt;
&lt;li&gt;지원하지 않는 HTTP 메서드로 요청한 경우&lt;/li&gt;
&lt;li&gt;서버 내부에서 예상하지 못한 오류가 발생한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이런 상황에서 예외 처리를 따로 하지 않으면, 스프링이 기본적으로 제공하는 에러 응답이 그대로 내려갈 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러면 앞에서 공통 응답 객체로 맞춰둔 구조와 달라지게 되고, 프론트엔드는 성공 응답과 실패 응답을 서로 다른 방식으로 해석해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국 에러 핸들링은 단순히 예외를 막는 작업이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;예외 상황에서도 우리가 정한 규칙대로 응답하게 만드는 과정&lt;/b&gt;&lt;/span&gt;이라고 생각했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;예외가 발생하면 어떤 문제가 생길까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 예외가 발생하면 그냥 500 에러가 뜨는 정도로 생각할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제로는 그보다 더 큰 문제가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 평소 응답이 다음과 같이 통일되어 있다고 해보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;{
  &quot;isSuccess&quot;: false,
  &quot;code&quot;: &quot;MEMBER404&quot;,
  &quot;message&quot;: &quot;존재하지 않는 회원입니다.&quot;,
  &quot;result&quot;: null
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 예외가 발생했을 때는 갑자기 스프링 기본 응답이 이렇게 내려올 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;{
  &quot;timestamp&quot;: &quot;2026-04-17T10:00:00&quot;,
  &quot;status&quot;: 500,
  &quot;error&quot;: &quot;Internal Server Error&quot;,
  &quot;path&quot;: &quot;/api/v1/users/me&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 되면 프론트엔드 입장에서는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;어떤 응답은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;isSuccess&lt;span&gt;를 보고 판단하고&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;어떤 응답은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;status&lt;span&gt;를 보고 판단해야 하는&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;상황이 생깁니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 예외를 따로 처리하지 않으면 결국&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;응답 통일이 성공 케이스에서만 적용되고, 실패 케이스에서는 깨지는 구조&lt;/b&gt;&lt;/span&gt;가 되어버립니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 에러 핸들러는 응답 통일의 연장선에서 꼭 필요한 요소라고 느꼈습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;전역 에러 핸들러란?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전역 에러 핸들러는 프로젝트 전체에서 발생하는 예외를 한곳에서 받아 처리하는 구조입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;스프링에서는 보통&lt;span&gt;&amp;nbsp;&lt;/span&gt;@RestControllerAdvice를 사용해서 이를 구현합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 어노테이션의 역할을 간단히 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 예외가 발생했을 때 각 Controller마다 try-catch를 반복해서 작성하는 대신,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;공통 예외 처리 로직을 한곳에 모아두는 방식&lt;/b&gt;&lt;/span&gt;이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; 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;첫 번째는 예외 처리 코드가 여러 컨트롤러에 흩어지지 않음&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; letter-spacing: 0px;&quot;&gt;두 번째는 프로젝트 전체에서 같은 기준으로 에러 응답을 만들 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000; letter-spacing: 0px;&quot;&gt;세 번째는 새로운 예외가 생겨도 한곳에서 정책을 관리하기 쉬움&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;@RestControllerAdvice는 왜 사용할까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1752&quot; data-origin-height=&quot;1568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WnjJD/dJMcajhBBZr/J8ti5nfajfJwIiuT34SBp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WnjJD/dJMcajhBBZr/J8ti5nfajfJwIiuT34SBp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WnjJD/dJMcajhBBZr/J8ti5nfajfJwIiuT34SBp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWnjJD%2FdJMcajhBBZr%2FJ8ti5nfajfJwIiuT34SBp0%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;722&quot; height=&quot;646&quot; data-origin-width=&quot;1752&quot; data-origin-height=&quot;1568&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@RestControllerAdvice&lt;span&gt;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;@ControllerAdvice&lt;span&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;@ResponseBody&lt;span&gt;가 합쳐진 형태라고 이해하면 쉽습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 예외를 잡아서 뷰를 반환하는 것이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;JSON 응답으로 바로 내려주는 전역 예외 처리기&lt;/b&gt;&lt;/span&gt;라고 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우리가 REST API 서버를 만들고 있다면, 예외 상황에서도 HTML 페이지를 보여주는 것이 아니라 JSON 형식의 응답을 반환해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 전역 예외 처리에서도&lt;span&gt;&amp;nbsp;&lt;/span&gt;@RestControllerAdvice를 사용하는 것이 자연스럽습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개인적으로는 이 어노테이션이 &amp;ldquo;예외를 잡는 도구&amp;rdquo;라기보다,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;예외 상황을 API 응답 규칙 안으로 다시 끌고 들어오는 장치&lt;/b&gt;&lt;/span&gt;처럼 느껴졌습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;예외는 어떻게 나눠서 관리할까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예외를 처리하려면 먼저 예외를 어떤 기준으로 만들고 관리할지도 정해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;워크북에서는 이 부분을 꽤 구조적으로 다루고 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;핵심은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 프로젝트 전체에서 사용할&lt;span&gt;&amp;nbsp;&lt;/span&gt;ProjectException이 있고, 회원 도메인에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;MemberException이 이를 상속받는 방식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조의 장점은 도메인형 아키텍처와도 잘 맞는다는 점입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 담당자가 member, mission, review처럼 나뉘어 있다면, 각 도메인은 자기 영역에 맞는 예외만 만들면 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 전역 에러 핸들러에서는 상위 예외인&lt;span&gt;&amp;nbsp;&lt;/span&gt;ProjectException만 잡아도 하위 도메인 예외까지 함께 처리할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 예외 구조를 잘 설계하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;예외가 많아져도 처리 방식은 단순하게 유지할 수 있습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;왜 에러 코드를 함께 사용해야 할까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예외를 단순히&lt;span&gt;&amp;nbsp;&lt;/span&gt;throw new RuntimeException(&quot;에러&quot;)처럼 던질 수도 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 하면 예외가 발생한 이유를 구조적으로 관리하기가 어렵습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 회원 조회 실패라는 상황 하나만 해도&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;를 서로 다르게 구분할 필요가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 보통은 예외 객체 안에&lt;span&gt;&amp;nbsp;&lt;/span&gt;BaseErrorCode&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 공통 인터페이스를 구현한 에러 코드를 함께 담아 관리합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 흐름입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MemberErrorCode.MEMBER_NOT_FOUND&lt;/li&gt;
&lt;li&gt;GeneralErrorCode.BAD_REQUEST&lt;/li&gt;
&lt;li&gt;GeneralErrorCode.INTERNAL_SERVER_ERROR&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 코드와 메시지, 상태값을 함께 묶어두면 전역 에러 핸들러는 예외를 받았을 때 그 안에 들어 있는 에러 코드를 꺼내 공통 응답 객체로 변환해주면 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 방식이 좋은 이유는 예외 처리 로직이 단순해진다는 점입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예외마다 응답을 하나씩 수동으로 만들 필요 없이,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;에러 코드가 응답의 기준점 역할을 하게 되는 것&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;전역 에러 핸들러는 어떻게 동작할까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전체 흐름으로 보면 다음과 같이 이해할 수 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Controller 또는 Service에서 예외가 발생한다&lt;/li&gt;
&lt;li&gt;스프링이 해당 예외를 감지한다&lt;/li&gt;
&lt;li&gt;@RestControllerAdvice&lt;span&gt;가 예외를 받아 처리한다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;예외 안의 에러 코드를 꺼낸다&lt;/li&gt;
&lt;li&gt;&lt;span&gt;공통 응답 객체&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;ApiResponse.onFailure(...)&lt;span&gt;로 감싼다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;정해진 HTTP 상태 코드와 함께 응답한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 회원 조회 중 회원이 없을 때 Service에서 이렇게 예외를 던질 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;throw new MemberException(MemberErrorCode.MEMBER_NOT_FOUND);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러면 전역 에러 핸들러에서는 대략 이런 식으로 처리할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;@RestControllerAdvice
public class GeneralExceptionAdvice {

    @ExceptionHandler(ProjectException.class)
    public ResponseEntity&amp;lt;ApiResponse&amp;lt;Void&amp;gt;&amp;gt; handleProjectException(ProjectException e) {
        BaseErrorCode errorCode = e.getErrorCode();
        return ResponseEntity.status(errorCode.getStatus())
                .body(ApiResponse.onFailure(errorCode, null));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity&amp;lt;ApiResponse&amp;lt;String&amp;gt;&amp;gt; handleException(Exception e) {
        BaseErrorCode errorCode = GeneralErrorCode.INTERNAL_SERVER_ERROR;
        return ResponseEntity.status(errorCode.getStatus())
                .body(ApiResponse.onFailure(errorCode, e.getMessage()));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조를 보면 의도가 꽤 명확합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;예상 가능한 예외와 예상하지 못한 예외를 분리해서 대응하는 구조&lt;/b&gt;&lt;/span&gt;&lt;span&gt;라고 이해할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;@ExceptionHandler는 어떤 역할을 할까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전역 에러 핸들러 안에서 핵심적으로 사용되는 것이&lt;span&gt;&amp;nbsp;&lt;/span&gt;@ExceptionHandler입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 어노테이션은 특정 타입의 예외가 발생했을 때 어떤 메서드가 그것을 처리할지 지정해줍니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@ExceptionHandler(ProjectException.class)는 우리가 정의한 커스텀 예외를 처리하고&lt;/li&gt;
&lt;li&gt;@ExceptionHandler(Exception.class)는 그 외의 일반 예외를 처리하는 식입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;@ExceptionHandler는 예외 처리 로직을 예외 타입별로 나누는 역할을 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이걸 통해 &amp;ldquo;회원 관련 예외는 이렇게&amp;rdquo;, &amp;ldquo;정의되지 않은 서버 에러는 이렇게&amp;rdquo;처럼 응답 전략을 구분할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;왜 공통 예외와 일반 예외를 둘 다 처리해야 할까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 프로젝트에서 사용하는 예외만 잘 처리하면 충분해 보일 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 실제로는 우리가 직접 던진 예외 외에도 다양한 예외가 발생할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잘못된 요청 형식&lt;/li&gt;
&lt;li&gt;JSON 파싱 실패&lt;/li&gt;
&lt;li&gt;지원하지 않는 HTTP 메서드 요청&lt;/li&gt;
&lt;li&gt;NullPointerException 같은 예상치 못한 런타임 오류&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이런 예외들은 모두 우리가 직접 만든&lt;span&gt;&amp;nbsp;&lt;/span&gt;ProjectException만으로는 처리되지 않을 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 마지막에&lt;span&gt;&amp;nbsp;&lt;/span&gt;Exception.class를 잡는 핸들러를 하나 두면, 최소한 어떤 예외가 발생하더라도 응답 형식이 완전히 깨지는 일은 막을 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;물론 모든 예외를 무조건 하나로 처리하는 것은 아쉽습니다. 하지만 기본적인 안전장치로는 꽤 중요하다고 느꼈습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;예외가 발생해도 응답 형식을 통일해야 하는 이유&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞선 글에서 API 응답 통일의 중요성을 정리했지만, 사실 그 구조가 진짜 의미를 가지려면 실패 상황까지 포함해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;성공했을 때만 형식이 맞고, 실패했을 때는 제각각이라면 프론트엔드 입장에서는 여전히 응답 처리 로직이 복잡해집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반대로 예외 상황도 다음과 같은 형식으로 통일되어 있다면 훨씬 다루기 쉬워집니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;{
  &quot;isSuccess&quot;: false,
  &quot;code&quot;: &quot;MEMBER404&quot;,
  &quot;message&quot;: &quot;존재하지 않는 회원입니다.&quot;,
  &quot;result&quot;: null
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 되면 프론트엔드는 성공이든 실패든 항상 같은 구조를 기준으로 응답을 파싱할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 전역 에러 핸들러는 단순히 서버에서 발생한 에러를 정리해주는 것이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;실패 응답도 성공 응답과 같은 규칙 안에서 관리하도록 만드는 핵심 요소&lt;/b&gt;&lt;/span&gt;라고 생각했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;직접 느낀 장점&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 내용을 정리하면서 느낀 전역 에러 핸들러의 장점은 크게 세 가지였습니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;&lt;b&gt;1. 예외 처리 기준이 한곳에 모인다&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;각 Controller마다 try-catch를 반복하지 않아도 되기 때문에 코드가 훨씬 깔끔해집니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;&lt;b&gt;2. 응답 형식을 유지할 수 있다&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;성공뿐 아니라 실패 상황까지 공통 응답 객체 형식으로 맞출 수 있습니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;&lt;b&gt;3. 도메인별 예외 관리가 쉬워진다&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;도메인형 구조에서는 각 도메인이 자기 예외 코드를 관리하고, 전역 에러 핸들러는 그것을 공통 방식으로 처리하면 되기 때문에 역할 분리가 자연스럽습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국 전역 에러 핸들러는 단순히 편의 기능이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;프로젝트 전체의 예외 처리 정책을 일관되게 만드는 설계 요소&lt;/b&gt;&lt;/span&gt;라고 느꼈습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;이번 내용을 정리하며 느낀 점&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 주차 내용을 보면서 가장 크게 느낀 점은, 예외 처리도 결국 설계의 일부라는 점이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 예외가 발생하면 그냥 에러 메시지만 잘 보이면 된다고 생각했는데, 실제로는 어떤 형식으로 응답할지, 어떤 코드로 구분할지, 어디에서 공통 처리할지를 함께 고민해야 훨씬 안정적인 구조가 만들어집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 인상 깊었던 부분은 두 가지였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;첫 번째는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;예외를 도메인별로 나누되, 공통 상위 예외로 묶어 관리하는 방식&lt;/b&gt;&lt;/span&gt;입니다.&lt;br /&gt;이 구조는 도메인형 아키텍처와도 잘 어울리고, 예외가 늘어나도 전역 처리 방식은 단순하게 유지할 수 있다는 점에서 꽤 실용적으로 느껴졌습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두 번째는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;응답 통일은 성공 응답만으로 끝나는 것이 아니라는 점&lt;/b&gt;&lt;/span&gt;입니다.&lt;br /&gt;실패 응답까지 같은 형식으로 관리해야 비로소 진짜 의미의 응답 통일이 된다고 느꼈습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 전역 에러 핸들러가 왜 필요한지와, 예외를 어떤 구조로 관리하면 좋은지 정리해보았습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;API를 구현하다 보면 정상적인 흐름만큼이나 예외 상황도 자주 마주하게 됩니다. 그리고 그 예외를 어떻게 처리하느냐에 따라 프로젝트의 안정성과 협업 난이도가 꽤 크게 달라집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히&lt;span&gt;&amp;nbsp;&lt;/span&gt;@RestControllerAdvice와&lt;span&gt;&amp;nbsp;&lt;/span&gt;@ExceptionHandler를 이용하면 프로젝트 전체의 예외를 한곳에서 관리할 수 있고, 공통 에러 코드와 공통 응답 객체를 함께 사용하면 실패 상황까지 일관된 형식으로 응답할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국 전역 에러 핸들러는 단순히 에러를 막는 도구가 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;예외 상황까지 포함해서 API의 규칙을 유지하게 해주는 구조&lt;/b&gt;&lt;/span&gt;라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>백엔드</category>
      <author>Archive_99</author>
      <guid isPermaLink="true">https://arch2ve-99.tistory.com/11</guid>
      <comments>https://arch2ve-99.tistory.com/11#entry11comment</comments>
      <pubDate>Tue, 21 Apr 2026 00:01:27 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot / 백엔드] API 응답 통일하기 - 공통 응답 객체와 DTO 설계</title>
      <link>https://arch2ve-99.tistory.com/10</link>
      <description>&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tsxvm/dJMcagyplF5/k4YV4keHoRriIHQTXoJTc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tsxvm/dJMcagyplF5/k4YV4keHoRriIHQTXoJTc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tsxvm/dJMcagyplF5/k4YV4keHoRriIHQTXoJTc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftsxvm%2FdJMcagyplF5%2Fk4YV4keHoRriIHQTXoJTc0%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;803&quot; height=&quot;452&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;백엔드에서 API를 만들다 보면 단순히 &amp;ldquo;값을 잘 반환하는가&amp;rdquo;만 생각하기 쉽습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저도 처음에는 요청이 오면 필요한 데이터를 꺼내서 JSON으로 내려주면 된다고 생각했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 API가 하나둘 늘어나기 시작하면 생각보다 금방 문제가 보입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어떤 API는 문자열만 반환하고, 어떤 API는 JSON 객체를 반환하고, 또 어떤 API는 성공했을 때와 실패했을 때 응답 형식이 완전히 다르게 내려오는 식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 큰 문제가 아닌 것처럼 보여도, 프론트엔드와 함께 작업하거나 API 개수가 많아질수록 이런 차이는 점점 불편해집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 프론트엔드 입장에서는 API마다 응답 형식을 따로 해석해야 하고, 어떤 경우에는 성공 여부를 판단하는 방식조차 달라질 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이번 글에서는 워크북 내용을 바탕으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;왜 API 응답을 통일해야 하는지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;공통 응답 객체는 어떤 구조로 만들 수 있는지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;, 그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;응답 DTO와 함께 관리하는 이유는 무엇인지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;를 정리해보려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서 다룰 내용은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API 응답을 왜 통일해야 하는가&lt;/li&gt;
&lt;li&gt;공통 응답 객체는 어떤 형태로 구성되는가&lt;/li&gt;
&lt;li&gt;code, message, result는 각각 어떤 역할을 하는가&lt;/li&gt;
&lt;li&gt;성공 응답 코드를 왜 따로 관리하는가&lt;/li&gt;
&lt;li&gt;응답 DTO를 함께 사용하는 이유는 무엇인가&lt;/li&gt;
&lt;li&gt;Controller에서 공통 응답 객체를 어떻게 사용하는가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;왜 API 응답을 통일해야 할까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QvaGA/dJMcacivfPV/2AV23E9NxKNLdbgoWvDIyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QvaGA/dJMcacivfPV/2AV23E9NxKNLdbgoWvDIyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QvaGA/dJMcacivfPV/2AV23E9NxKNLdbgoWvDIyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQvaGA%2FdJMcacivfPV%2F2AV23E9NxKNLdbgoWvDIyk%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;1024&quot; height=&quot;374&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;API를 처음 몇 개만 만들 때는 응답 형식이 조금씩 달라도 크게 불편하지 않을 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 어떤 API는 그냥&lt;span&gt;&amp;nbsp;&lt;/span&gt;&quot;성공&quot;이라는 문자열을 반환하고, 어떤 API는&lt;span&gt;&amp;nbsp;&lt;/span&gt;{ &quot;name&quot;: &quot;홍길동&quot; }&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 객체를 반환해도 일단 동작은 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 프로젝트가 커지면 이런 방식은 금방 한계를 드러냅니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 프론트엔드가 여러 API를 호출한다고 해보겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 API는 성공 여부를 HTTP 상태 코드로만 판단하고&lt;/li&gt;
&lt;li&gt;어떤 API는 message 문자열을 보고 판단하고&lt;/li&gt;
&lt;li&gt;어떤 API는 result가 있으면 성공이라고 가정하고&lt;/li&gt;
&lt;li&gt;어떤 API는 실패했을 때 아예 다른 구조를 내려준다면&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프론트엔드 입장에서는 API마다 대응 로직을 따로 작성해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 백엔드가 응답을 제각각 보내기 시작하면 결국&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;클라이언트가 그 복잡함을 대신 감당하게 되는 구조&lt;/b&gt;&lt;/span&gt;가 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 API 응답 통일은 단순히 보기 좋게 만들기 위한 작업이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;클라이언트와의 약속을 명확하게 맞추는 과정&lt;/b&gt;&lt;/span&gt;이라고 생각했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;보통 어떤 형식으로 통일할까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트마다 세부 구조는 다를 수 있지만, 보통은 다음과 같은 형태를 많이 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;{
  &quot;isSuccess&quot;: true,
  &quot;code&quot;: &quot;COMMON200&quot;,
  &quot;message&quot;: &quot;요청에 성공했습니다.&quot;,
  &quot;result&quot;: {
    &quot;name&quot;: &quot;홍길동&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조를 보면 응답이 크게 네 부분으로 나뉘어 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;isSuccess&lt;/li&gt;
&lt;li&gt;code&lt;/li&gt;
&lt;li&gt;message&lt;/li&gt;
&lt;li&gt;result&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 형식의 장점은 응답을 받는 쪽에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;항상 같은 틀로 결과를 해석할 수 있다&lt;/b&gt;&lt;/span&gt;는 점입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 어떤 API든 먼저&lt;span&gt;&amp;nbsp;&lt;/span&gt;isSuccess&lt;span&gt;를 보고 성공 여부를 확인하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;code&lt;span&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;message&lt;span&gt;를 통해 결과의 의미를 파악하고, 실제 데이터는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;result&lt;span&gt;에서 꺼내 쓰면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 기준이 정해져 있으면 API를 추가하더라도 응답 구조 자체는 흔들리지 않게 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;isSuccess는 왜 필요할까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 &amp;ldquo;어차피 HTTP 상태 코드가 있는데 굳이 성공 여부를 또 넣어야 하나?&amp;rdquo;라는 생각이 들 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 실제로는 응답 본문 안에 성공 여부가 명확하게 들어가 있으면 클라이언트에서 처리하기가 훨씬 편합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 공통 응답 객체를 기준으로 파싱할 때는, 응답 구조를 열자마자 성공인지 실패인지 바로 확인할 수 있다는 점이 꽤 직관적입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;물론 프로젝트에 따라&lt;span&gt;&amp;nbsp;&lt;/span&gt;isSuccess&lt;span&gt;&amp;nbsp;&lt;/span&gt;없이 설계할 수도 있습니다. 하지만 응답을 일관되게 다루고 싶다면 이런 식의 명시적인 필드는 꽤 유용하다고 느꼈습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;code는 왜 따로 둘까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;code는 HTTP 상태 코드와는 조금 다른 역할을 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;HTTP 상태 코드는 요청이 전체적으로 성공했는지, 잘못되었는지, 서버 에러가 났는지를 나타내는 큰 범주의 신호라면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;code는 그 안에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;조금 더 구체적인 결과를 표현하는 값&lt;/b&gt;&lt;/span&gt;에 가깝습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 같은 400번대 에러라고 해도&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;는 서로 다른 의미를 가질 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이런 상황에서 code를 두면 프론트엔드나 다른 팀원이 결과를 훨씬 명확하게 이해할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;code&lt;span&gt;는 단순한 문자열이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;프로젝트 안에서 결과를 식별하는 공통 언어&lt;/b&gt;&lt;/span&gt;&lt;span&gt;라고 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;message는 어떤 역할을 할까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;message는 code를 사람이 읽기 쉬운 문장으로 풀어주는 역할을 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;span&gt;&amp;nbsp;&lt;/span&gt;MEMBER200_1&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 코드만 보면 개발자끼리는 약속된 의미를 알 수 있을지 몰라도, 바로 해석하기는 어렵습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 여기에&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;성공적으로 유저를 조회했습니다.&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;존재하지 않는 회원입니다.&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;잘못된 요청입니다.&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;같은 메시지가 함께 오면 응답 내용을 이해하기 훨씬 쉬워집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개인적으로는 이 필드가 디버깅할 때도 꽤 유용하다고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Swagger나 Postman으로 테스트할 때도 결과를 한눈에 확인하기 편하고, 예외 상황이 생겼을 때 어떤 문제가 발생했는지 빠르게 파악할 수 있기 때문입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;result에는 무엇이 들어갈까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;result는 실제로 클라이언트가 필요로 하는 데이터를 담는 영역입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 회원 조회 API라면 이름, 이메일, 포인트 같은 데이터가 들어갈 수 있고, 목록 조회 API라면 리스트 형태의 데이터가 들어갈 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 공통 응답 객체에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;isSuccess&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;code&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;message&lt;span&gt;는 공통 메타 정보에 가깝고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;result&lt;span&gt;가 실제 비즈니스 데이터라고 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 나누면 응답의 바깥 구조는 항상 유지하면서도, API마다 필요한 데이터만&lt;span&gt;&amp;nbsp;&lt;/span&gt;result에 유연하게 넣을 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 점이 공통 응답 객체의 가장 큰 장점 중 하나라고 생각했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;왜 공통 응답 객체를 따로 만들어야 할까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;응답을 통일하려면 결국 모든 API가 공통적으로 사용할 수 있는 객체가 필요합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 식의 구조를 생각할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;public class ApiResponse&amp;lt;T&amp;gt; {

    private final Boolean isSuccess;
    private final String code;
    private final String message;
    private final T result;

    // 생성자, 정적 팩토리 메서드 등
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은&lt;span&gt;&amp;nbsp;&lt;/span&gt;result가 제네릭 타입이라는 것입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;왜냐하면 어떤 API는 문자열을 반환할 수도 있고, 어떤 API는 DTO를 반환할 수도 있고, 어떤 API는 리스트를 반환할 수도 있기 때문입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;ApiResponse&amp;lt;T&amp;gt;로 만들면 공통 구조는 유지하면서도 내부 데이터 타입은 유연하게 바꿀 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;정리해보면 공통 응답 객체는 단순히 클래스를 하나 더 만드는 작업이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;모든 API가 같은 언어로 응답하도록 기준을 세우는 작업&lt;/b&gt;&lt;/span&gt;이라고 볼 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;성공 코드와 실패 코드를 왜 enum으로 관리할까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;응답 통일을 하다 보면 자연스럽게 코드와 메시지를 어디에서 관리할지도 고민하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 Controller나 Service 안에 문자열을 직접 적어도 동작은 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이런 방식은 API가 많아질수록 관리가 어려워집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 여러 곳에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&quot;성공적으로 조회했습니다&quot;&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 문장을 직접 작성하다 보면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 보통은 성공 코드와 실패 코드를 enum으로 분리해서 관리합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GeneralSuccessCode&lt;/li&gt;
&lt;li&gt;MemberSuccessCode&lt;/li&gt;
&lt;li&gt;GeneralErrorCode&lt;/li&gt;
&lt;li&gt;MemberErrorCode&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처럼 나누면 도메인별로 관리하기도 쉽고, 코드 체계도 더 명확해집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 도메인형 구조를 사용하는 프로젝트에서는 각 도메인 담당자가 자기 영역의 코드만 관리할 수 있다는 점도 장점이라고 느꼈습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;응답 DTO는 왜 따로 둘까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;공통 응답 객체가 있으면 모든 API 응답을 한 번에 통일할 수 있습니다. 그런데 여기서 한 가지 더 중요한 포인트가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;result 안에 들어가는 데이터도 DTO로 따로 관리하는 것이 좋다&lt;/b&gt;&lt;/span&gt;&lt;span&gt;는 점입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 회원 조회 API가 있다고 할 때, 그냥 엔티티를 그대로 result에 넣어버릴 수도 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 하면 문제가 생길 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;API 명세와 실제 응답이 어긋날 수 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반대로 응답 DTO를 따로 만들면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;클라이언트에게 보여줄 데이터만 선택해서 응답할 수 있습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 식입니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;public class MemberResDTO {

    @Builder
    public record MyPageResponse(
            String name,
            String profileUrl,
            String email,
            String phoneNumber,
            Integer point
    ) {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 해두면 result에 어떤 데이터가 들어가는지 더 명확해지고, 응답 스펙도 훨씬 읽기 쉬워집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 공통 응답 객체와 응답 DTO는 서로 대체 관계가 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;함께 써야 더 안정적인 구조&lt;/b&gt;&lt;/span&gt;라고 생각했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;Controller에서는 어떻게 사용할까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;공통 응답 객체를 만들었다면 Controller에서는 이를 감싸서 반환하면 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 회원 조회 API라면 대략 이런 형태가 됩니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;@PostMapping(&quot;/v1/users/me&quot;)
public ApiResponse&amp;lt;MemberResDTO.MyPageResponse&amp;gt; getMyPage(
        @RequestBody MemberReqDTO.MyPageRequest request
) {
    MemberResDTO.MyPageResponse result = memberService.getMyPage(request);
    return ApiResponse.onSuccess(MemberSuccessCode.MEMBER_FOUND, result);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조를 보면 흐름이 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller는 요청 DTO를 받는다&lt;/li&gt;
&lt;li&gt;Service는 응답 DTO를 만든다&lt;/li&gt;
&lt;li&gt;Controller는 그 DTO를&lt;span&gt;&amp;nbsp;&lt;/span&gt;ApiResponse로 감싸서 반환한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 실제 데이터는 DTO로 관리하고, 전체 응답 형식은 ApiResponse가 담당하는 구조입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 방식의 장점은 API마다 result 내용은 달라도, 바깥 응답 틀은 항상 같다는 점입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;응답을 통일하면 무엇이 좋아질까?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;직접 정리해보면서 느낀 장점은 크게 세 가지였습니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;&lt;b&gt;1. 프론트엔드와의 약속이 명확해진다&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프론트엔드는 모든 API 응답을 같은 방식으로 해석할 수 있습니다.&lt;br /&gt;성공 여부, 코드, 메시지, 실제 데이터를 어디서 꺼내야 하는지가 항상 같기 때문입니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;&lt;b&gt;2. API 문서와 테스트가 더 명확해진다&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Swagger에서 응답을 확인할 때도 구조가 통일되어 있으면 훨씬 읽기 쉽습니다.&lt;br /&gt;어떤 API가 어떤 형태로 데이터를 내려주는지 한눈에 파악하기 편합니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;&lt;b&gt;3. 유지보수가 쉬워진다&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;응답 형식을 수정해야 할 일이 생겼을 때도 공통 응답 객체를 중심으로 관리할 수 있습니다.&lt;br /&gt;API마다 제각각 수정하는 것보다 훨씬 안정적입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국 API 응답 통일은 단순한 형식 맞추기가 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;협업과 유지보수를 위한 기본 설계&lt;/b&gt;&lt;/span&gt;라고 느꼈습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;이번 내용을 정리하며 느낀 점&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 내용을 정리하면서 가장 크게 느낀 점은, API 응답은 단순히 데이터를 보내는 문제가 아니라는 점이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;응답을 어떻게 설계하느냐에 따라 프론트엔드가 데이터를 해석하는 방식이 달라지고, 협업의 난이도도 꽤 많이 달라집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 인상 깊었던 부분은 공통 응답 객체와 응답 DTO의 역할이 생각보다 분명하다는 점이었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공통 응답 객체는 전체 형식을 통일하고&lt;/li&gt;
&lt;li&gt;응답 DTO는 실제 비즈니스 데이터를 명확하게 표현합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 둘을 함께 사용하면 응답 구조가 훨씬 안정적으로 느껴졌습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이전에는 그냥 JSON만 잘 내려주면 된다고 생각했는데, 이번에 정리하면서 보니 결국 중요한 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;클라이언트가 예측 가능한 방식으로 응답을 받을 수 있게 만드는 것&lt;/b&gt;&lt;/span&gt;이라는 생각이 들었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 API 응답을 왜 통일해야 하는지와 공통 응답 객체를 어떤 방식으로 설계할 수 있는지 정리해보았습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트 초반에는 응답 형식이 조금씩 달라도 큰 문제가 없어 보일 수 있지만, API가 늘어나고 협업이 시작되면 통일된 응답 구조의 중요성이 훨씬 크게 느껴집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히&lt;span&gt;&amp;nbsp;&lt;/span&gt;isSuccess&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;code&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;message&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;result&lt;span&gt;처럼 공통된 틀을 두고, 실제 데이터는 DTO로 분리해서 관리하면 응답을 훨씬 더 명확하고 안정적으로 다룰 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국 API 응답 통일은 보기 좋은 형식을 맞추는 작업이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;클라이언트와 서버가 같은 기준으로 데이터를 주고받기 위한 약속을 만드는 과정&lt;/b&gt;&lt;/span&gt;이라고 생각합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음 글에서는 이번 흐름에서 이어서, 예외가 발생했을 때도 같은 응답 형식을 유지할 수 있도록 돕는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;전역 에러 핸들러&lt;/b&gt;&lt;/span&gt;에 대해 정리해보려고 합니다.&lt;/p&gt;</description>
      <category>백엔드</category>
      <category>API Response</category>
      <category>DTO</category>
      <category>spring boot</category>
      <author>Archive_99</author>
      <guid isPermaLink="true">https://arch2ve-99.tistory.com/10</guid>
      <comments>https://arch2ve-99.tistory.com/10#entry10comment</comments>
      <pubDate>Fri, 17 Apr 2026 20:19:13 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot / 백엔드] 스프링 부트 계층 구조 이해하기 - Controller, Service, Repository 정리</title>
      <link>https://arch2ve-99.tistory.com/9</link>
      <description>&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRlynW/dJMcaibXS2K/drkOAm7dK6GdzhQFdQJoc1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRlynW/dJMcaibXS2K/drkOAm7dK6GdzhQFdQJoc1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRlynW/dJMcaibXS2K/drkOAm7dK6GdzhQFdQJoc1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cRlynW/dJMcaibXS2K/drkOAm7dK6GdzhQFdQJoc1/img.gif&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;1080&quot; height=&quot;500&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트 구조를 잡고 나면 그다음으로 자연스럽게 드는 생각이 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;ldquo;그래서 이 파일들 안에는 대체 어떤 코드가 들어가야 하지?&amp;rdquo;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음 스프링을 공부할 때는 저도 Controller, Service, Repository를 일단 만들어두고, 필요한 코드를 그때그때 넣으면 된다고 생각했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 실제로 API를 하나씩 구현해보니 이 세 계층은 단순히 관습적으로 나누는 것이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;각자 맡아야 하는 역할이 분명히 다르다&lt;/b&gt;&lt;/span&gt;는 걸 알게 됐습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;요청을 받는 곳, 실제 로직을 처리하는 곳, 그리고 DB와 소통하는 곳이 뒤섞이기 시작하면 코드는 금방 복잡해지고, 나중에는 어디를 수정해야 하는지도 헷갈리게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 이번 글에서는 워크북 내용을 바탕으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Controller, Service, Repository가 각각 어떤 역할을 하는지&lt;/b&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;왜 이렇게 나누는지&lt;/b&gt;&lt;span&gt;, 그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;실제로 요청이 들어왔을 때 어떤 흐름으로 동작하는지&lt;/b&gt;&lt;span&gt;를 정리해보려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서 다룰 내용은 다음과 같습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller는 어떤 역할을 하는가&lt;/li&gt;
&lt;li&gt;Service는 어떤 역할을 하는가&lt;/li&gt;
&lt;li&gt;Repository는 어떤 역할을 하는가&lt;/li&gt;
&lt;li&gt;Request Body를 받을 때 DTO를 왜 사용하는가&lt;/li&gt;
&lt;li&gt;계층을 나누는 것이 왜 중요한가&lt;/li&gt;
&lt;li&gt;HTTP 요청부터 DB 조회, 응답까지 어떤 흐름으로 이어지는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;왜 Controller, Service, Repository를 나눌까?&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;스프링 프로젝트를 처음 보면 Controller, Service, Repository라는 이름이 너무 당연하게 등장해서 &amp;ldquo;원래 이렇게 하는 건가 보다&amp;rdquo; 하고 넘어가기 쉽습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이 구조는 단순한 관습이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;관심사를 분리하기 위한 기본적인 설계 방식&lt;/b&gt;&lt;/span&gt;이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자의 정보를 조회하는 API가 있다고 해보겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 한 클래스 안에서&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 요청을 받고&lt;/li&gt;
&lt;li&gt;요청값을 검증하고&lt;/li&gt;
&lt;li&gt;비즈니스 로직을 처리하고&lt;/li&gt;
&lt;li&gt;DB를 조회하고&lt;/li&gt;
&lt;li&gt;응답 JSON까지 직접 만들기 시작하면&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 빨리 구현되는 것처럼 보여도, 기능이 늘어날수록 수정이 어려워집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반대로 역할을 나누면 흐름이 훨씬 명확해집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Controller&lt;/b&gt;&lt;/span&gt;는 요청과 응답을 담당하고&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Service&lt;/b&gt;&lt;/span&gt;는 실제 비즈니스 로직을 수행하고&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Repository&lt;/b&gt;&lt;/span&gt;는 DB와의 통신을 담당합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 이 구조의 핵심은 코드를 예쁘게 나누는 데 있는 것이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;각 계층이 해야 할 일만 맡도록 만드는 것&lt;/b&gt;&lt;/span&gt;에 있다고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Controller란?&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2102&quot; data-origin-height=&quot;1576&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dn4WpO/dJMcafff0xw/VnwKWwrk9k9dZ2cOlp16gK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dn4WpO/dJMcafff0xw/VnwKWwrk9k9dZ2cOlp16gK/img.png&quot; data-alt=&quot;Spring boot의 Controller 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dn4WpO/dJMcafff0xw/VnwKWwrk9k9dZ2cOlp16gK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdn4WpO%2FdJMcafff0xw%2FVnwKWwrk9k9dZ2cOlp16gK%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;2102&quot; height=&quot;1576&quot; data-origin-width=&quot;2102&quot; data-origin-height=&quot;1576&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Spring boot의 Controller 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Controller는 말 그대로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;클라이언트의 요청이 가장 먼저 도착하는 계층&lt;/b&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;브라우저, 앱, 프론트엔드 서버가 어떤 URI로 요청을 보내면 가장 먼저 그 요청을 받아주는 곳이 바로 Controller입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Controller가 하는 일은 크게 세 가지로 정리할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트에게 HTTP 요청을 받는다&lt;/li&gt;
&lt;li&gt;필요한 값을 꺼내서 Service에 전달한다&lt;/li&gt;
&lt;li&gt;Service가 반환한 결과를 다시 응답으로 돌려준다&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, Controller의 핵심 역할은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;요청과 응답의 입구 역할&lt;/b&gt;&lt;/span&gt;이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;중요한 점은 Controller가 너무 많은 일을 하면 안 된다는 것입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;비즈니스 로직까지 Controller에 들어가기 시작하면 요청 처리 코드와 실제 로직이 섞이게 되고, 나중에는 테스트나 수정도 어려워집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 Controller는 최대한 가볍게 두고, 필요한 값만 정리해서 Service로 넘기는 형태가 가장 깔끔합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Controller에서 자주 보는 어노테이션&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;스프링에서 Controller를 작성할 때 자주 보는 어노테이션들이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5mxyL/dJMcad2Ix2b/hxMovlL4IlRkdYKKFW2Rb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5mxyL/dJMcad2Ix2b/hxMovlL4IlRkdYKKFW2Rb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5mxyL/dJMcad2Ix2b/hxMovlL4IlRkdYKKFW2Rb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5mxyL%2FdJMcad2Ix2b%2FhxMovlL4IlRkdYKKFW2Rb0%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;433&quot; height=&quot;97&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@RestController&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;@RestController&lt;/span&gt;는 JSON 형식의 응답을 반환하는 컨트롤러를 만들 때 사용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존의 서버 사이드 렌더링 방식에서는 서버가 HTML을 만들어서 반환하는 경우가 많았기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;@Controller&lt;/span&gt;를 사용했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 요즘처럼 프론트엔드와 백엔드가 분리된 구조에서는 백엔드가 HTML을 직접 조립하기보다&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;데이터를 JSON 형태로 전달&lt;/b&gt;&lt;/span&gt;하는 경우가 훨씬 많습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 REST API를 만드는 상황에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;@Controller&lt;/span&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;@ResponseBody&lt;/span&gt;가 합쳐진&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;@RestController&lt;/span&gt;를 사용하는 것이 일반적입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 부분은 처음엔 &amp;ldquo;그냥 REST API니까 붙이는 어노테이션&amp;rdquo; 정도로 생각했는데, 정리하면서 보니 결국&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;응답 형태가 HTML이 아니라 JSON이라는 점을 명확히 드러내는 역할&lt;/b&gt;&lt;/span&gt;이라고 이해하면 훨씬 쉬웠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@RequiredArgsConstructor&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 어노테이션은 생성자 주입을 자동으로 만들어주는 역할을 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 Controller에서 Service를 주입받아야 할 때,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;private final&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;필드를 선언하면 필요한 생성자를 자동으로 생성해줍니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;직접 생성자를 작성하지 않아도 되기 때문에 코드가 더 간결해지고, final 필드를 통해 의존성이 고정된다는 점에서도 장점이 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@RequestMapping&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;@RequestMapping(&quot;/auth&quot;)&lt;/span&gt;처럼 사용하면 해당 컨트롤러가 처리할 URI의 공통 경로를 지정할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 컨트롤러 내부의 메서드들이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;/auth&lt;/span&gt;로 시작하는 요청을 처리하도록 만드는 방식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 공통 경로를 묶어두면 API 구조를 이해하기도 쉽고, 엔드포인트를 관리하기도 더 편해집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;클라이언트의 요청값은 어떻게 받을까?&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Controller는 클라이언트가 보내는 여러 종류의 값을 받아야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대표적으로는 다음과 같은 방식이 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@RequestParam&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1740&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mqmpH/dJMcadPaVVU/rQFW1oWBWtIOIgKYIOnfP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mqmpH/dJMcadPaVVU/rQFW1oWBWtIOIgKYIOnfP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mqmpH/dJMcadPaVVU/rQFW1oWBWtIOIgKYIOnfP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmqmpH%2FdJMcadPaVVU%2FrQFW1oWBWtIOIgKYIOnfP0%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;769&quot; height=&quot;306&quot; data-origin-width=&quot;1740&quot; data-origin-height=&quot;692&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쿼리 파라미터를 받을 때 사용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 요청이 있다고 하면&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;GET /members?page=1&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;page=1&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 값을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;@RequestParam&lt;/span&gt;으로 받을 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@RequestBody&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1810&quot; data-origin-height=&quot;258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZrayz/dJMcaibXUrK/DXIIKsRkTKKdSr3FH3zQfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZrayz/dJMcaibXUrK/DXIIKsRkTKKdSr3FH3zQfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZrayz/dJMcaibXUrK/DXIIKsRkTKKdSr3FH3zQfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZrayz%2FdJMcaibXUrK%2FDXIIKsRkTKKdSr3FH3zQfK%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;1810&quot; height=&quot;258&quot; data-origin-width=&quot;1810&quot; data-origin-height=&quot;258&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;position: absolute;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;position: absolute;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;요청 본문에 담긴 JSON 데이터를 받을 때 사용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;회원가입, 로그인, 게시글 작성처럼 클라이언트가 JSON 데이터를 본문에 담아 보내는 경우에 주로 사용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@PathVariable&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1534&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bg9pv9/dJMcacCOkfv/ItkyVIn8EH1Ive7lH9KvxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bg9pv9/dJMcacCOkfv/ItkyVIn8EH1Ive7lH9KvxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bg9pv9/dJMcacCOkfv/ItkyVIn8EH1Ive7lH9KvxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbg9pv9%2FdJMcacCOkfv%2FItkyVIn8EH1Ive7lH9KvxK%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;1534&quot; height=&quot;198&quot; data-origin-width=&quot;1534&quot; data-origin-height=&quot;198&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;URI 경로 자체에 포함된 값을 받을 때 사용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;GET /members/1&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;은 Path Variable로 받을 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@RequestHeader&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;262&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bweClz/dJMcahjPPAN/dVeLxkE6UctejWOenSzNB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bweClz/dJMcahjPPAN/dVeLxkE6UctejWOenSzNB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bweClz/dJMcahjPPAN/dVeLxkE6UctejWOenSzNB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbweClz%2FdJMcahjPPAN%2FdVeLxkE6UctejWOenSzNB0%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;710&quot; height=&quot;142&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;262&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;헤더 값을 읽을 때 사용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;추후 JWT 토큰 같은 인증 정보를 헤더에서 받을 때 자주 사용하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 정리하고 보니 Controller는 단순히 &amp;ldquo;API 메서드가 있는 클래스&amp;rdquo;가 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;HTTP 요청의 다양한 구성 요소를 읽고 해석하는 계층&lt;/b&gt;&lt;/span&gt;이라고 볼 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Request Body를 받을 때 DTO를 사용하는 이유&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1368&quot; data-origin-height=&quot;1476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qFI3i/dJMcaduRTba/DRNVA6238j7xFGKDhqBTY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qFI3i/dJMcaduRTba/DRNVA6238j7xFGKDhqBTY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qFI3i/dJMcaduRTba/DRNVA6238j7xFGKDhqBTY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqFI3i%2FdJMcaduRTba%2FDRNVA6238j7xFGKDhqBTY0%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;482&quot; height=&quot;520&quot; data-origin-width=&quot;1368&quot; data-origin-height=&quot;1476&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;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Controller에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;@RequestBody&lt;/span&gt;를 사용할 때는 보통 DTO를 함께 사용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개인적으로 이 부분이 꽤 중요하다고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 그냥 엔티티를 바로 받아도 되지 않을까 생각할 수 있습니다. 하지만 요청을 받을 때 DTO를 사용하면 장점이 분명합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 요청 구조를 명확히 할 수 있다&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 클라이언트가 보내야 하는 값이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;stringTest&lt;/span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;longTest&lt;/span&gt;라고 정해져 있다면, DTO를 통해 어떤 형태의 데이터가 들어와야 하는지 명확하게 표현할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 요청 스펙이 코드로 드러납니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 안정적으로 데이터를 받을 수 있다&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서비스 계층은 이미 정해진 구조의 데이터를 전달받게 되므로, 들어오는 요청값을 더 안정적으로 다룰 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 엔티티와 요청 객체를 분리할 수 있다&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;엔티티는 DB와 매핑되는 객체이고, 요청 DTO는 클라이언트가 보내는 데이터를 담는 객체입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 둘은 역할이 완전히 다르기 때문에 분리하는 것이 훨씬 안전합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Service란?&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2106&quot; data-origin-height=&quot;1458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ut69a/dJMcafM5ocQ/ClpuvGkSuDAkZcDAhKiWG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ut69a/dJMcafM5ocQ/ClpuvGkSuDAkZcDAhKiWG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ut69a/dJMcafM5ocQ/ClpuvGkSuDAkZcDAhKiWG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUt69a%2FdJMcafM5ocQ%2FClpuvGkSuDAkZcDAhKiWG0%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;785&quot; height=&quot;543&quot; data-origin-width=&quot;2106&quot; data-origin-height=&quot;1458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Service는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;실제 비즈니스 로직이 수행되는 계층&lt;/b&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Controller가 요청을 받았다면, 이제 그 요청값을 넘겨받아 &amp;ldquo;이 API가 실제로 해야 하는 일&amp;rdquo;을 처리하는 곳이 바로 Service입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Service가 하는 일은 보통 다음과 같습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Controller에게 전달받은 데이터를 받는다&lt;/li&gt;
&lt;li&gt;필요한 비즈니스 로직을 수행한다&lt;/li&gt;
&lt;li&gt;Repository를 통해 DB를 조회하거나 저장한다&lt;/li&gt;
&lt;li&gt;응답 DTO를 만들어 Controller에 돌려준다&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 회원 정보를 조회하는 API라면, Service에서는&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전달받은 ID로 회원을 찾고&lt;/li&gt;
&lt;li&gt;회원이 없으면 예외를 발생시키고&lt;/li&gt;
&lt;li&gt;회원이 있으면 응답 DTO로 변환해서 반환하는&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;방식으로 동작하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, Service는 요청을 직접 받지도 않고, DB를 직접 구현하지도 않지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;전체 로직의 중심에서 실제 기능을 수행하는 역할&lt;/b&gt;&lt;/span&gt;을 맡습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;응답 DTO와 Converter를 분리하는 이유&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;1382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjQtOK/dJMcadPaW7Q/mM50TgfiAfzvStKXS9qTjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjQtOK/dJMcadPaW7Q/mM50TgfiAfzvStKXS9qTjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjQtOK/dJMcadPaW7Q/mM50TgfiAfzvStKXS9qTjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjQtOK%2FdJMcadPaW7Q%2FmM50TgfiAfzvStKXS9qTjK%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;675&quot; height=&quot;571&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;1382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Service에서는 종종 응답 DTO를 생성하는 작업도 함께 하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 방법은 크게 두 가지가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DTO 안에 변환 로직을 넣는 방법&lt;/li&gt;
&lt;li&gt;Converter를 따로 두는 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;워크북에서는 Converter를 분리하는 방식을 사용하고 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 Entity를 받아서 Response DTO로 바꿔주는 로직을 Converter에 따로 두는 식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 방식의 장점은 역할이 더 분명해진다는 점입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Service는 로직을 수행하고&lt;/li&gt;
&lt;li&gt;Converter는 객체를 변환하고&lt;/li&gt;
&lt;li&gt;DTO는 데이터를 담는 역할만 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;물론 파일이 하나 더 생긴다는 점은 조금 번거로울 수 있습니다. 그래도 프로젝트 규모가 커질수록 변환 로직이 여기저기 퍼지는 것보다 한곳에 모여 있는 편이 훨씬 관리하기 쉽다고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;왜 응답도 DTO로 보내는 걸까?&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;요청을 받을 때 DTO를 쓰는 이유는 어느 정도 직관적입니다. 그런데 응답도 굳이 DTO로 보내야 할까 하는 생각이 들 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개인적으로는 이 부분도 꽤 중요하다고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 Service에서 그냥 문자열이나 엔티티를 그대로 반환한다고 가정해보겠습니다. 그렇게 되면 나중에 응답 형식이 바뀌었을 때 수정 범위가 커질 수 있고, 불필요한 데이터까지 노출될 가능성도 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반대로 응답 DTO를 따로 두면&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;이후 요구사항이 바뀌어도 DTO 기준으로 관리할 수 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국 응답 DTO는 단순히 &amp;ldquo;예쁘게 감싸는 객체&amp;rdquo;가 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;클라이언트와 주고받는 데이터를 명확히 설계하기 위한 장치&lt;/b&gt;&lt;/span&gt;라고 이해하는 게 더 맞다고 생각했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Repository란?&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Repository는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;DB와 직접 소통하는 계층&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Controller가 요청을 받고, Service가 로직을 수행한다면, 실제로 데이터를 조회하거나 저장하는 일은 Repository가 맡습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;스프링에서는 보통 JPA를 사용할 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;JpaRepository&lt;/span&gt;를 상속받아 Repository를 선언합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같은 형태입니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 선언하면 기본적으로 다음과 같은 메서드들을 사용할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;save()&lt;/li&gt;
&lt;li&gt;findById()&lt;/li&gt;
&lt;li&gt;delete()&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 필요하다면 메서드 이름 규칙을 활용해 커스텀 조회 메서드도 만들 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;findByEmail()&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 방식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음엔 Repository가 단순히 &amp;ldquo;DB 관련 코드를 모아두는 곳&amp;rdquo; 정도로 느껴졌는데, 정리하고 보니 더 정확히는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;데이터 접근 책임을 분리하는 계층&lt;/b&gt;&lt;/span&gt;이라고 보는 게 맞았습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;비즈니스 로직이 Service에 있어야 하듯, DB 접근 로직은 Repository에 있어야 책임이 섞이지 않습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Entity와 함께 자주 보이는 어노테이션&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1120&quot; data-origin-height=&quot;1634&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNzy4s/dJMcabqndqG/RRdPNQoHK3aIe7bYSTswjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNzy4s/dJMcabqndqG/RRdPNQoHK3aIe7bYSTswjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNzy4s/dJMcabqndqG/RRdPNQoHK3aIe7bYSTswjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNzy4s%2FdJMcabqndqG%2FRRdPNQoHK3aIe7bYSTswjK%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;518&quot; height=&quot;756&quot; data-origin-width=&quot;1120&quot; data-origin-height=&quot;1634&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Repository를 이해하려면 함께 다니는 JPA 어노테이션들도 어느 정도 익숙해질 필요가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대표적으로는 다음과 같습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@Entity&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 객체가 DB 테이블과 매핑되는 엔티티라는 것을 나타냅니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@Table(name = &amp;ldquo;member&amp;rdquo;)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어떤 테이블과 연결되는지 명시할 때 사용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@Id&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기본 키를 나타냅니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@GeneratedValue(strategy = GenerationType.IDENTITY)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기본 키 생성 전략을 설정할 때 사용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@Column&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;컬럼 이름이나 제약 조건 등을 설정할 때 사용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@Enumerated(EnumType.STRING)&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;enum 값을 DB에 어떤 방식으로 저장할지 설정합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;@Transactional&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DB의 생성, 수정, 삭제 작업처럼 트랜잭션이 필요한 작업에서 사용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이런 어노테이션들을 처음부터 완벽하게 이해하기는 쉽지 않지만, 적어도 Repository 계층은 결국&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;엔티티를 기반으로 DB와 연결되는 부분&lt;/b&gt;&lt;/span&gt;이라는 흐름을 잡는 것이 중요하다고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;전체 흐름으로 보면 더 이해가 쉽다&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Controller, Service, Repository는 따로 보면 각각의 역할이 보이지만, 실제로는 하나의 흐름으로 연결됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 회원 조회 API를 기준으로 보면 대략 이런 순서로 진행됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트가 HTTP 요청을 보낸다&lt;/li&gt;
&lt;li&gt;Controller가 요청값을 받는다&lt;/li&gt;
&lt;li&gt;Controller가 Service에 필요한 값을 전달한다&lt;/li&gt;
&lt;li&gt;Service가 비즈니스 로직을 수행한다&lt;/li&gt;
&lt;li&gt;필요하면 Repository를 통해 DB를 조회한다&lt;/li&gt;
&lt;li&gt;Service가 응답 DTO를 만든다&lt;/li&gt;
&lt;li&gt;Controller가 그 결과를 클라이언트에게 반환한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 흐름을 이해하고 나니 각 계층이 왜 필요한지 훨씬 명확해졌습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국 중요한 것은 Controller, Service, Repository라는 이름 자체보다,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;요청 처리 &amp;rarr; 로직 수행 &amp;rarr; 데이터 접근 &amp;rarr; 응답 반환&lt;/b&gt;&lt;/span&gt;의 흐름이 자연스럽게 분리되어 있다는 점이라고 생각합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이번 내용을 정리하며 느낀 점&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 주차 내용을 정리하면서 가장 크게 느낀 점은 Controller, Service, Repository를 나누는 이유가 단순히 &amp;ldquo;스프링에서 원래 그렇게 하니까&amp;rdquo;가 아니라는 점이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;각 계층은 정말로 맡아야 하는 책임이 다르고, 그 책임이 섞이기 시작하면 코드가 빠르게 복잡해집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 인상 깊었던 부분은 두 가지였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;첫 번째는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Controller는 생각보다 많은 일을 하면 안 된다&lt;/b&gt;&lt;span&gt;는 점입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;요청을 받고 응답을 돌려주는 역할에 집중해야 이후 구조가 깔끔해집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;두 번째는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Service와 Repository의 경계를 분명히 해야 한다&lt;/b&gt;&lt;span&gt;는 점입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;비즈니스 로직과 데이터 접근 로직이 분리되어야 수정도 쉽고, 테스트도 쉬워집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또 DTO를 요청과 응답에 나눠서 사용하는 이유도 조금 더 선명하게 이해할 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이전에는 그냥 관습처럼 사용했던 구조였는데, 이번에 정리하면서 보니 결국 이 모든 분리는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;유지보수와 협업을 위한 설계&lt;/b&gt;&lt;/span&gt;라는 생각이 들었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Controller, Service, Repository가 각각 어떤 역할을 하는지 정리해보았습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 이 세 계층이 단순히 스프링 프로젝트의 기본 템플릿처럼 느껴질 수 있지만, 실제로는 요청 처리와 비즈니스 로직, 데이터 접근을 분리해서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;코드를 더 읽기 쉽고, 수정하기 쉬운 구조로 만들기 위한 장치&lt;/b&gt;&lt;/span&gt;라고 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 API를 하나 구현하는 과정을 따라가다 보면 Controller는 입력과 출력을 담당하고, Service는 기능의 중심이 되며, Repository는 데이터를 다루는 역할을 맡는다는 점이 자연스럽게 보입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국 좋은 구조는 거창한 설계보다도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;각자 해야 할 일을 정확히 나누는 것&lt;/b&gt;&lt;/span&gt;에서 시작된다고 생각합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음 글에서는 이번 흐름에서 한 걸음 더 나아가, 왜 API 응답 형식을 통일해야 하는지와 공통 응답 객체를 어떻게 설계할 수 있는지를 정리해보려고 합니다.&lt;/p&gt;</description>
      <category>백엔드</category>
      <category>Controller</category>
      <category>repository</category>
      <category>service</category>
      <category>spring boot</category>
      <author>Archive_99</author>
      <guid isPermaLink="true">https://arch2ve-99.tistory.com/9</guid>
      <comments>https://arch2ve-99.tistory.com/9#entry9comment</comments>
      <pubDate>Fri, 17 Apr 2026 15:19:18 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot / 백엔드] 스프링 부트 프로젝트 세팅하기 - 아키텍처 구조와 Swagger 정리</title>
      <link>https://arch2ve-99.tistory.com/8</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;589&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sPO2h/dJMcaflZWsl/tF9JBKNkFb9PswtZFLKyDk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sPO2h/dJMcaflZWsl/tF9JBKNkFb9PswtZFLKyDk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sPO2h/dJMcaflZWsl/tF9JBKNkFb9PswtZFLKyDk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsPO2h%2FdJMcaflZWsl%2FtF9JBKNkFb9PswtZFLKyDk%2Fimg.jpg&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;576&quot; height=&quot;461&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;589&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 개발을 공부하다 보면 기능 구현 자체도 중요하지만, 그 전에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;프로젝트를 어떤 구조로 시작할지&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;고민하게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 저도 스프링 프로젝트를 생성하고, 필요한 클래스만 빠르게 추가하면 된다고 생각했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그런데 직접 프로젝트를 진행해보니 어떤 구조로 파일을 나누는지, 설정 파일을 어떻게 관리하는지, 그리고 API 문서를 어떤 방식으로 공유하는지에 따라 협업 난이도와 유지보수성이 꽤 크게 달라진다는 걸 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 프로젝트 규모가 커질수록 &amp;ldquo;기능이 돌아가기만 하는 코드&amp;rdquo;보다 &amp;ldquo;다른 사람이 봐도 이해하기 쉬운 구조&amp;rdquo;가 훨씬 중요하다는 점을 실감했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 이번 글에서는 워크북 내용을 바탕으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;아키텍처 구조가 왜 필요한지&lt;/b&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;도메인형 아키텍처는 어떤 특징이 있는지&lt;/b&gt;&lt;span&gt;, 그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;스프링 부트 프로젝트를 세팅하면서 Swagger까지 연결하는 과정&lt;/b&gt;&lt;span&gt;을 정리해보려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서 다룰 내용은 다음과 같습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;li&gt;스프링 부트 프로젝트 기본 세팅&lt;/li&gt;
&lt;li&gt;application.yml과 환경 변수 설정&lt;/li&gt;
&lt;li&gt;패키지 구조를 잡을 때 주의할 점&lt;/li&gt;
&lt;li&gt;Swagger란 무엇이고 왜 사용하는가&lt;/li&gt;
&lt;li&gt;Swagger 설정 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;아키텍처 구조란?&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아키텍처 구조는 쉽게 말하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;프로젝트의 뼈대를 어떻게 설계할지 정하는 것&lt;/b&gt;&lt;/span&gt;이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;조금 더 풀어서 말하면,&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;를 미리 정해두는 작업입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 작은 프로젝트라서 구조가 크게 중요하지 않아 보일 수 있습니다. 하지만 기능이 하나둘 추가되고, 기획이 바뀌고, 다른 사람과 협업하게 되면 구조가 잘 잡혀 있는 프로젝트와 그렇지 않은 프로젝트의 차이가 확실히 드러납니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결국 아키텍처 구조는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;지금 당장의 편의성보다 이후의 수정과 확장까지 고려하는 설계&lt;/b&gt;&lt;span&gt;라고 느꼈습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;왜 아키텍처 구조를 설계해야 할까?&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;683&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dSO3mz/dJMcabRnEAL/mcM3cnQaaI5KP3FLrak6v1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dSO3mz/dJMcabRnEAL/mcM3cnQaaI5KP3FLrak6v1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dSO3mz/dJMcabRnEAL/mcM3cnQaaI5KP3FLrak6v1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdSO3mz%2FdJMcabRnEAL%2FmcM3cnQaaI5KP3FLrak6v1%2Fimg.jpg&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;1024&quot; height=&quot;683&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;683&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아키텍처 구조를 설계하는 가장 큰 이유는 결국&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;유지보수&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;때문입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 처음에는 A 기능만 필요해서 코드를 한곳에 몰아서 작성했다고 가정해보겠습니다. 그런데 나중에 B 기능이 추가되고, A 기능 안의 세부 로직도 수정해야 하는 상황이 생기면 구조가 정리되지 않은 코드는 수정 범위가 점점 커지게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반대로 역할이 분리되어 있고 의존성이 낮은 구조라면 문제가 생겼을 때 해당 부분만 집중해서 수정할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 구조 설계를 잘하면 다음과 같은 장점이 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 이해하기 쉬워진다&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트를 처음 보는 사람도 &amp;ldquo;어디에 어떤 코드가 있을지&amp;rdquo; 예측하기 쉬워집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 관심사를 분리할 수 있다&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;요청을 처리하는 부분, 비즈니스 로직, 데이터 접근 로직을 나누면 각 부분의 책임이 명확해집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 수정과 확장에 유리하다&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;한 부분의 변경이 다른 부분에 큰 영향을 주지 않도록 만들 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국 좋은 아키텍처 구조는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;코드를 잘 짜는 기술&lt;/b&gt;&lt;/span&gt;이라기보다&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;변화에 강한 프로젝트를 만드는 방식&lt;/b&gt;&lt;/span&gt;에 가깝다고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;어떤 아키텍처 구조가 있을까?&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;워크북에서는 대표적으로 두 가지를 소개합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 계층형 구조&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;계층형 구조는 controller, service, repository, entity처럼&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;역할(계층)&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 기준으로 파일을 나누는 방식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같은 구조입니다.&lt;/p&gt;
&lt;pre class=&quot;vhdl&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;controller/
service/
repository/
entity/
dto/&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 방식은 처음 배우는 입장에서는 직관적이고 익숙합니다. 다만 프로젝트 규모가 커지면 기능별로 관련 파일이 흩어져서 하나의 도메인을 한 번에 파악하기 어려울 수도 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 도메인형 구조&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;도메인형 구조는 회원, 미션, 리뷰처럼&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;비즈니스 도메인&lt;/b&gt;&lt;/span&gt;을 기준으로 묶는 방식입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같은 식입니다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;domain/
  member/
  mission/
  review/&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 각 도메인 안에 다시 controller, service, repository, entity 등을 둡니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;회원 관련 코드&amp;rdquo;가 모두 member 아래에 모이기 때문에 도메인 단위로 코드를 이해하고 관리하기가 더 편해집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;676&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cinXhW/dJMcagFb2jO/7xhpeDARjzgaCwvOQIYN4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cinXhW/dJMcagFb2jO/7xhpeDARjzgaCwvOQIYN4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cinXhW/dJMcagFb2jO/7xhpeDARjzgaCwvOQIYN4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcinXhW%2FdJMcagFb2jO%2F7xhpeDARjzgaCwvOQIYN4k%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;431&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;676&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;도메인형 아키텍처를 선택한 이유&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 워크북에서는 데모데이 프로젝트 특성과 역할 분담을 고려해서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;도메인형 아키텍처 구조&lt;/b&gt;&lt;/span&gt;를 사용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개인적으로도 이 방식이 꽤 실용적이라고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 프로젝트에서 큰 도메인이 다음과 같다고 해보겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 구조를 다음처럼 나눌 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;java/com/example/umc10th/
├── domain
│   ├── member
│   │   ├── controller
│   │   ├── converter
│   │   ├── dto
│   │   ├── entity
│   │   ├── enums
│   │   ├── exception
│   │   ├── repository
│   │   └── service
│   ├── mission
│   └── review
├── global
└── Umc10thApplication&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조의 장점은 분명합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;물론 무조건 도메인형 구조가 정답은 아닙니다. 프로젝트 크기, 팀 규모, 복잡도에 따라 다를 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래도 이번처럼 도메인별 독립성이 중요하고, 기능 확장을 고려해야 하는 상황에서는 도메인형 구조가 꽤 잘 맞는다고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;아키텍처 구조를 설계할 때 중요한 원칙&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어떤 구조를 선택하든 공통적으로 중요한 원칙이 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 단일 책임 원칙&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;각 계층과 객체는 하나의 역할에 집중해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller는 요청과 응답 처리&lt;/li&gt;
&lt;li&gt;Service는 비즈니스 로직 처리&lt;/li&gt;
&lt;li&gt;Repository는 DB 접근 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처럼 역할이 분리되어야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 모듈화&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하나의 기능을 하나의 모듈로 관리하면 필요한 부분만 수정하거나 재사용하기 쉬워집니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 낮은 의존성&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;한 부분이 바뀌더라도 다른 부분에 영향이 최소화되도록 설계해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이런 원칙들은 결국 객체지향 설계의 기본 철학과도 닿아 있다고 생각합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;스프링 부트 프로젝트 세팅하기&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트 세팅은 크게 두 가지 방식으로 진행할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;start.spring.io를 이용하는 방법&lt;/li&gt;
&lt;li&gt;IntelliJ에서 직접 생성하는 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기본적으로는 둘 다 결과는 비슷하고, 필요한 의존성을 추가해서 프로젝트를 시작하면 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 워크북에서는 Gradle 기반으로 프로젝트를 생성하고, 이후 설정 파일을 application.properties 대신 application.yml로 변경합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 이유는 설정이 많아질수록 yml 형식이 계층 구조를 표현하기 더 편하기 때문입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;application.yml 설정하기&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;스프링 프로젝트를 생성한 뒤에는 DB 연결과 JPA 관련 설정을 추가해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예시는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;spring:
  application:
    name: &quot;프로젝트명&quot;

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: ${DB_URL}
    username: ${DB_USER}
    password: ${DB_PW}

  jpa:
    database: mysql
    database-platform: org.hibernate.dialect.MySQLDialect
    show-sql: true
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        format_sql: true&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 DB URL, USER, PASSWORD 같은 민감한 정보는 직접 코드에 하드코딩하지 않고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;환경 변수&lt;/b&gt;&lt;/span&gt;로 분리했다는 점입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이런 방식의 장점은 다음과 같습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;민감 정보는 왜 꼭 분리해야 할까?&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;워크북에서도 강조하는 부분이지만, 민감한 정보는 정말 조심해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 DB 계정 정보나 API 키를 실수로 퍼블릭 저장소에 올리면&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부에서 DB에 접근할 수 있고&lt;/li&gt;
&lt;li&gt;API 키가 무단 사용될 수 있고&lt;/li&gt;
&lt;li&gt;비용이 발생하거나 서비스에 문제가 생길 수도 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 환경 변수를 쓰지 않고 application.yml에 직접 값을 넣었다면 해당 파일을 .gitignore에 추가해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반대로 .env 파일이나 환경 변수로 분리했다면 그 파일을 깃에서 제외해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 프로젝트 세팅에서 가장 기본적이지만 중요한 습관은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;민감 정보를 코드와 분리하는 것&lt;/b&gt;&lt;/span&gt;이라고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;패키지 구조에서 주의할 점&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;스프링 부트에서 패키지 구조를 잡을 때 생각보다 중요한 포인트가 하나 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;메인 애플리케이션 클래스의 위치&lt;/b&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 메인 클래스가&lt;/p&gt;
&lt;pre class=&quot;css&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;com.example.umc10th.Umc10thApplication&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;에 있다면, 스프링은 기본적으로 이 패키지를 기준으로 하위 패키지를 스캔합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;에 있는 클래스들은 인식하지만, 상위나 전혀 다른 위치에 있는 클래스는 인식하지 못할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 Controller, Service, Repository 등을 만들 때는 반드시 메인 애플리케이션 클래스 기준으로 동일하거나 하위 패키지에 두어야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 부분은 처음에 실수하기 쉬운 포인트라서 프로젝트를 시작할 때 구조를 먼저 잡아두는 게 정말 중요하다고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Swagger란?&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Swagger는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;개발한 API 목록을 확인하고 테스트할 수 있게 도와주는 도구&lt;/b&gt;&lt;span&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;백엔드 개발을 하다 보면 API를 만들고 끝나는 것이 아니라 그 API를 다른 사람도 이해할 수 있게 설명해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 Swagger를 사용하면&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 API가 있는지&lt;/li&gt;
&lt;li&gt;어떤 요청값을 받는지&lt;/li&gt;
&lt;li&gt;어떤 응답을 반환하는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;를 한눈에 확인할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉, Swagger는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;API 문서를 자동화하고, 직접 테스트까지 해볼 수 있는 도구&lt;/b&gt;&lt;span&gt;라고 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Swagger는 왜 필요할까?&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개인적으로 Swagger의 가장 큰 장점은 &amp;ldquo;코드와 실제 문서 간의 거리&amp;rdquo;를 줄여준다는 점이라고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;문서를 수동으로 작성하면 초기에는 잘 정리되어 있어도 기능이 바뀔 때마다 계속 수정해줘야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 Swagger는 구현한 API를 기준으로 문서를 보여주기 때문에 실제 동작과 문서가 어긋날 가능성을 줄일 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한 프론트엔드나 팀원 입장에서는 직접 요청을 날려보면서 API를 확인할 수 있어서 협업에도 큰 도움이 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Swagger 설정하기&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Swagger를 적용하려면 먼저 의존성을 추가해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;build.gradle의 dependencies에 다음을 넣어줍니다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.1'&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그다음 전역 설정 파일을 만들어 Swagger 설정을 추가합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 global/config/SwaggerConfig.java에 다음처럼 작성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;@Configuration
public class SwaggerConfig {

    @Bean
    public OpenAPI swagger() {
        Info info = new Info()
                .title(&quot;UMC10th&quot;)
                .description(&quot;10기 Swagger&quot;)
                .version(&quot;0.0.1&quot;);

        String securityScheme = &quot;JWT TOKEN&quot;;
        SecurityRequirement securityRequirement = new SecurityRequirement().addList(securityScheme);

        Components components = new Components()
                .addSecuritySchemes(securityScheme, new SecurityScheme()
                        .name(securityScheme)
                        .type(SecurityScheme.Type.HTTP)
                        .scheme(&quot;Bearer&quot;)
                        .bearerFormat(&quot;JWT&quot;));

        return new OpenAPI()
                .info(info)
                .addServersItem(new Server().url(&quot;/&quot;))
                .addSecurityItem(securityRequirement)
                .components(components);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 눈에 띄는 부분은 JWT 관련 설정입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아직 인증 기능을 구현하지 않았더라도 추후 토큰 기반 인증을 사용할 예정이라면 이런 식으로 보안 스키마를 미리 정의해둘 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Swagger 실행하기&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;설정이 끝난 뒤 프로젝트를 실행하고 다음 주소로 접속하면 Swagger UI를 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot; style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;code&gt;http://localhost:8080/swagger-ui/index.html&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지에서는&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;등록된 API 목록 확인&lt;/li&gt;
&lt;li&gt;요청 파라미터 입력&lt;/li&gt;
&lt;li&gt;실제 API 호출 테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;까지 한 번에 해볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, Swagger는 단순 문서가 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;백엔드 개발 중간중간 API를 검증하는 도구&lt;/b&gt;&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;3014&quot; data-origin-height=&quot;1716&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oVu3q/dJMcadIoAxq/epCiwlLmTsr4y12Ok74cS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oVu3q/dJMcadIoAxq/epCiwlLmTsr4y12Ok74cS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oVu3q/dJMcadIoAxq/epCiwlLmTsr4y12Ok74cS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoVu3q%2FdJMcadIoAxq%2FepCiwlLmTsr4y12Ok74cS1%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;3014&quot; height=&quot;1716&quot; data-origin-width=&quot;3014&quot; data-origin-height=&quot;1716&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;프로젝트 세팅에서 느낀 핵심 포인트&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 워크북을 정리하면서 느낀 핵심은 프로젝트 세팅은 단순히 &amp;ldquo;처음 한 번 하는 초기 작업&amp;rdquo;이 아니라는 점이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음 구조를 어떻게 잡느냐에 따라&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; 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;li&gt;문서 관리가 훨씬 수월해질 수도 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 인상 깊었던 부분은 두 가지였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;첫 번째는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;도메인 중심으로 구조를 나누면 프로젝트를 비즈니스 기준으로 이해하기 쉬워진다&lt;/b&gt;&lt;span&gt;는 점입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;두 번째는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Swagger 같은 도구를 붙이면 API를 구현하고 검증하는 과정이 훨씬 명확해진다&lt;/b&gt;&lt;span&gt;는 점입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 내용을 정리하면서 느낀 것은 스프링 부트 프로젝트를 세팅하는 과정은 단순히 프로젝트를 생성하고 실행하는 데서 끝나지 않는다는 점이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어떤 아키텍처 구조를 선택할지, 어떻게 역할을 분리할지, 민감한 설정 정보를 어떻게 관리할지, 그리고 API를 어떤 방식으로 문서화할지까지 모두가 결국&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;좋은 프로젝트를 만들기 위한 기초 작업&lt;/b&gt;&lt;/span&gt;이라고 생각합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 controller, service, repository를 그냥 관습처럼 나누고 Swagger도 &amp;ldquo;문서 보는 도구&amp;rdquo; 정도로만 이해했는데, 이번 정리를 하면서 왜 이런 구조와 도구가 필요한지 조금 더 선명하게 이해할 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 백엔드 개발은 기능 구현만 잘한다고 끝나는 것이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;다른 사람이 이해하기 쉬운 구조를 만들고, 협업할 수 있는 환경을 함께 준비하는 것&lt;/b&gt;&lt;/span&gt;도 중요하다고 느꼈습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞으로 프로젝트를 진행할 때도 단순히 돌아가는 코드에만 집중하기보다 구조와 문서화까지 함께 신경 쓰는 습관을 들여야겠다고 생각했습니다.&lt;/p&gt;</description>
      <category>백엔드</category>
      <category>springboot</category>
      <category>swagger</category>
      <author>Archive_99</author>
      <guid isPermaLink="true">https://arch2ve-99.tistory.com/8</guid>
      <comments>https://arch2ve-99.tistory.com/8#entry8comment</comments>
      <pubDate>Wed, 15 Apr 2026 15:59:06 +0900</pubDate>
    </item>
  </channel>
</rss>