<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>shootingstar-1117 님의 블로그</title>
    <link>https://shootingstar-1117.tistory.com/</link>
    <description>shootingstar-1117 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Thu, 9 Apr 2026 18:44:40 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>shootingstar-1117</managingEditor>
    <item>
      <title>[회고] CI/CD 파이프라인 구축 및 자동화 배포 후기</title>
      <link>https://shootingstar-1117.tistory.com/4</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. CI/CD 파이프라인 설계 (Data Flow)&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Local (Dev Environment)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자는 로컬에서 &lt;code&gt;docker-compose up -d pace-db pace-redis&lt;/code&gt;를 통해 인프라를 띄우고, IntelliJ에서 서버를 실행하여 개발합니다.&lt;/li&gt;
&lt;li&gt;코드 수정 후 &lt;code&gt;develop&lt;/code&gt; 브랜치에 &lt;code&gt;push&lt;/code&gt; 하면 파이프라인이 시작됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GitHub Actions (CI)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Build&lt;/b&gt;: 소스 코드를 체크아웃하고 &lt;code&gt;Dockerfile&lt;/code&gt;을 기반으로 이미지를 빌드합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Push&lt;/b&gt;: 빌드된 이미지를 &lt;b&gt;Docker Hub&lt;/b&gt; 레지스트리로 전송합니다. (&lt;code&gt;push&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AWS EC2 (CD)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Connect&lt;/b&gt;: SSH(&lt;code&gt;appleboy/ssh-action&lt;/code&gt;)를 통해 서버에 접속합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Pull&lt;/b&gt;: Docker Hub에서 최신 이미지를 &lt;code&gt;pull&lt;/code&gt; 받아옵니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Env Generation&lt;/b&gt;: GitHub Secrets에 저장된 민감한 정보들을 사용하여 서버 내부에서 &lt;code&gt;.env&lt;/code&gt; 파일을 &lt;b&gt;동적으로 생성&lt;/b&gt;합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Run&lt;/b&gt;: &lt;code&gt;docker-compose up -d&lt;/code&gt; 명령어로 변경된 컨테이너만 스마트하게 교체하고 실행합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Health Check&lt;/b&gt;: 서버가 정상적으로 떴는지 &lt;code&gt;curl&lt;/code&gt; 명령어로 확인 후 배포를 완료합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 주요 트러블슈팅 및 해결 과정 (Learning Points)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Issue 1: &quot;포트가 이미 사용 중입니다&quot; (Bind for 0.0.0.0:3306 failed)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;현상&lt;/b&gt;: 배포 스크립트 실행 시 MySQL 포트 충돌 에러(&lt;code&gt;driver failed programming external connectivity&lt;/code&gt;)가 발생하며 배포가 실패했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원인&lt;/b&gt;:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;초기 설정에서 DB 포트를 &lt;code&gt;0.0.0.0:3306&lt;/code&gt;으로 열어두었는데, 이를 보안 강화를 위해 &lt;code&gt;127.0.0.1:3306&lt;/code&gt;으로 변경하는 과정에서 Docker의 네트워크 설정(iptables)이 꼬였습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker-compose down&lt;/code&gt;을 해도 &lt;code&gt;docker-proxy&lt;/code&gt; 프로세스가 좀비처럼 남아 포트를 점유하고 있었습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버에 직접 접속하여 &lt;code&gt;sudo lsof -i :3306&lt;/code&gt;으로 범인 프로세스를 색출하고 &lt;code&gt;kill&lt;/code&gt; 했습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker network prune -f&lt;/code&gt;와 &lt;code&gt;sudo systemctl restart docker&lt;/code&gt; 명령어로 꼬인 네트워크 규칙을 강제로 초기화하여 해결했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배운점&lt;/b&gt;: 설정이 크게 바뀔 때는 &lt;code&gt;prune&lt;/code&gt;이나 서비스 재시작 같은 작업이 필요함을 배웠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Issue 2: 환경변수의 미로 (.env vs yaml vs compose)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;현상&lt;/b&gt;: 로컬에서는 잘 되는데 배포만 하면 DB 접속이 안되는 에러가 발생했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원인&lt;/b&gt;: &lt;code&gt;application.yaml&lt;/code&gt;, &lt;code&gt;docker-compose.yml&lt;/code&gt;, 그리고 실제 주입되는 환경변수의 이름이 서로 미묘하게 달랐습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결&lt;/b&gt;: &lt;b&gt;&quot;Single Source of Truth (.env)&quot;&lt;/b&gt; 전략을 도입했습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 변수명을 &lt;code&gt;.env&lt;/code&gt; 기준으로 통일했습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;은 &lt;code&gt;.env&lt;/code&gt;를 읽어 컨테이너에 주입하고, 앱은 그 값을 그대로 받도록 설정을 일원화하여 혼란을 잠재웠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Issue 3: Redis 보안과 데이터 증발&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;현상&lt;/b&gt;: 코드 리뷰 중 &quot;Redis 포트가 외부에 열려있어 위험하다&quot;는 지적과 &quot;컨테이너 재시작 시 데이터가 날아간다&quot;는 문제를 확인했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;보안&lt;/b&gt;: &lt;code&gt;ports: 127.0.0.1:6379:6379&lt;/code&gt; 설정을 통해 외부 접속을 원천 차단하고, 로컬(앱)에서만 접속 가능하게 변경했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터&lt;/b&gt;: &lt;code&gt;volumes&lt;/code&gt; 설정을 추가하고, &lt;code&gt;redis-server --appendonly yes&lt;/code&gt; 옵션을 통해 데이터 영속성을 확보했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 성찰 및 향후 계획&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;깨달은 점&lt;/b&gt;: 단순히 &quot;배포가 된다&quot;를 넘어 &lt;b&gt;&quot;안전하고 효율적으로&quot;&lt;/b&gt; 배포하는 것이 얼마나 어려운지 깨달았습니다. 특히 Docker Compose가 제공하는 &lt;b&gt;코드로써의 인프라&lt;/b&gt; 덕분에, 팀원들이 복잡한 설치 과정 없이 명령어 한 줄로 동일한 개발 환경을 갖게 된 것이 가장 큰 수확인 것 같습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;아쉬운 점&lt;/b&gt;: 현재는 배포 시 약 10~30초 정도의 다운타임(서버 끊김)이 발생합니다. 프론트엔드 팀원들이 테스트 중에 불편을 겪기도 했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;향후 계획&lt;/b&gt;: 다음 단계로는 &lt;b&gt;Blue/Green 배포&lt;/b&gt; 전략을 도입하여, 사용자가 배포 중에도 끊김 없이 서비스를 이용할 수 있는 무중단 배포 환경을 구축하고 싶습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;후기&lt;/b&gt;: &lt;b&gt;정말 좋은 경험이었습니다!!&lt;/b&gt; 인프라 구축을 바닥부터 하나하나 쌓아가며 경험을 얻은 귀중한 시간이었습니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>shootingstar-1117</author>
      <guid isPermaLink="true">https://shootingstar-1117.tistory.com/4</guid>
      <comments>https://shootingstar-1117.tistory.com/4#entry4comment</comments>
      <pubDate>Fri, 6 Feb 2026 16:47:42 +0900</pubDate>
    </item>
    <item>
      <title>[회고] Cloudtype 배포 성공기: MariaDB와 환경 변수 트러블슈팅</title>
      <link>https://shootingstar-1117.tistory.com/3</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 환경(Local)에서 개발하던 스프링 부트 프로젝트를 실제 클라우드 환경(Cloudtype)에 배포하며 겪은 시행착오와 해결 과정을 정리합니다. 이번 경험을 통해 인프라 설정과 데이터베이스 연결의 핵심 원리를 깊이 이해하게 되었습니다.&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 개요&lt;/h2&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;프로젝트명: Pace&lt;/li&gt;&lt;li&gt;배포 환경: Cloudtype (Spring Boot, MariaDB)&lt;/li&gt;&lt;li&gt;주요 기술: Java 21, Spring Boot 3.x, Spring Data JPA, MySQL&lt;/li&gt;&lt;/ul&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 주요 트러블슈팅 (Troubleshooting)&lt;/h2&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 환경 변수 주입 문제&lt;/h3&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;문제: &lt;code&gt;java.lang.RuntimeException: Driver claims to not accept jdbcUrl, ${DATABASE_URL}&lt;/code&gt;&lt;/li&gt; 
 &lt;li&gt;원인: 로컬의 &lt;code&gt;.env&lt;/code&gt; 파일은 깃에 올리지 않으므로, 서버는 &lt;code&gt;${DATABASE_URL}&lt;/code&gt;이라는 변수의 실제 값을 찾지 못해 발생한 문제.&lt;/li&gt; 
 &lt;li&gt;해결: 클라우드타입 콘솔의 &lt;b&gt;환경 변수(Environment Variables)&lt;/b&gt; 설정 메뉴에서 &lt;code&gt;DATABASE_URL&lt;/code&gt;, &lt;code&gt;DATABASE_USERNAME&lt;/code&gt;, &lt;code&gt;DATABASE_PASSWORD&lt;/code&gt;를 직접 등록하여 해결.&lt;/li&gt; 
 &lt;li&gt;학습: 배포 환경에서는 민감한 정보를 외부 환경 변수를 통해 주입(Injection)받는 것이 정석임을 이해함.&lt;/li&gt; 
&lt;/ul&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 MariaDB 호환성 및 접속 주소&lt;/h3&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;문제: 클라우드타입 서비스 목록에 MySQL이 없어 MariaDB를 선택하며 발생한 혼란.&lt;/li&gt; 
 &lt;li&gt;원인: MySQL과 MariaDB는 뿌리가 같아 호환되지만, 접속 주소 형식을 맞추는 작업이 필요했음.&lt;/li&gt; 
 &lt;li&gt;해결: 주소 형식을 &lt;code&gt;jdbc:mysql://&lt;/code&gt;로 유지하여 기존 MySQL 드라이버가 MariaDB와 대화할 수 있도록 설정.&lt;/li&gt; 
&lt;/ul&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 URL 문법 오타&lt;/h3&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;문제: &lt;code&gt;jdbc:mysql//...&lt;/code&gt; (콜론 누락) 에러 발생.&lt;/li&gt; 
 &lt;li&gt;원인: 주소를 수동 입력하는 과정에서 프로토콜 구분자인 &lt;code&gt;:&lt;/code&gt;를 누락하여 드라이버가 주소를 인식하지 못함.&lt;/li&gt; 
 &lt;li&gt;해결: &lt;code&gt;jdbc:mysql://&lt;/code&gt; 형식으로 정확히 수정.&lt;/li&gt; 
 &lt;li&gt;교훈: 인프라 설정 시 문자 하나가 시스템 전체의 실행 여부를 결정하므로 극도의 세밀함이 필요함.&lt;/li&gt; 
&lt;/ul&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4 하이버네이트 방언(Dialect) 에러&lt;/h3&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;문제: &lt;code&gt;java.sql.SQLSyntaxErrorException: Unknown column 'RESERVED' in 'WHERE'&lt;/code&gt;&lt;/li&gt; 
 &lt;li&gt;원인: MySQL 드라이버가 MariaDB의 시스템 테이블 구조를 잘못 파악하여 발생한 문법 오류.&lt;/li&gt; 
 &lt;li&gt;해결: 환경 변수에 &lt;code&gt;SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT&lt;/code&gt;를 추가하고 값을 &lt;code&gt;org.hibernate.dialect.MariaDBDialect&lt;/code&gt;로 명시하여 해결.&lt;/li&gt; 
 &lt;li&gt;학습: JPA가 SQL을 번역할 때 사용하는 &lt;b&gt;방언(Dialect)&lt;/b&gt;의 개념과 자동 판별이 실패할 경우의 수동 설정법을 익힘.&lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 결과 및 성과&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 네트워크 및 설정 에러를 해결한 끝에 다음과 같이 서버가 정상 구동됨을 확인하였다.&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt; 
 &lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat started on port 8080 (http) with context path '/'&lt;br&gt;Started PaceApplication in 114.597 seconds&lt;/p&gt; 
&lt;/blockquote&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 향후 배포 고도화 계획&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 클라우드타입의 간단하게 UI 기반 배포를 사용했으나, 다음 단계로 &lt;b&gt;CI/CD 파이프라인&lt;/b&gt;을 직접 구축할 계획이다.&lt;/p&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;&lt;li&gt;GitHub Actions: 코드 푸시 시 자동 빌드 및 JAR 생성.&lt;/li&gt;&lt;li&gt;Docker Hub: 생성된 이미지를 가상화하여 보관.&lt;/li&gt;&lt;li&gt;AWS EC2 &amp;amp; Docker Compose: 서버에서 이미지를 내려받아 앱과 DB를 컨테이너로 묶어 실행.&lt;/li&gt;&lt;/ol&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 마치며&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;삽질은 배신하지 않는다&quot;는 말을 체감한 하루였습니다. 단순히 코드를 짜는 것만큼이나 내가 짠 코드가 어떤 환경에서 어떻게 돌아가는지 이해하는 것이 백엔드 개발자의 핵심 역량임을 깨달았습니다..&lt;/p&gt;</description>
      <author>shootingstar-1117</author>
      <guid isPermaLink="true">https://shootingstar-1117.tistory.com/3</guid>
      <comments>https://shootingstar-1117.tistory.com/3#entry3comment</comments>
      <pubDate>Thu, 15 Jan 2026 15:59:34 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] 페이징 기법에 대한 이해</title>
      <link>https://shootingstar-1117.tistory.com/2</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 페이징 기법, 왜 필요할까?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;페이징(Paging)&lt;/b&gt;: &lt;b&gt;많은 양의 데이터&lt;/b&gt;를 &lt;b&gt;한 번&lt;/b&gt;에 보여주는 대신, &lt;b&gt;정해진 갯수만큼&lt;/b&gt;의 &lt;b&gt;페이지(Page) 단위&lt;/b&gt;로 나누어 보여주는 기법&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&quot;그냥 데이터가 많으면 전부 다 보내주고, 클라이언트(브라우저)에서 알아서 나눠서 보여주면 안되나?&quot;&lt;/b&gt; 라고 생각할 수도 있습니다. 하지만 그렇게 하면, &lt;b&gt;대부분의 서비스&lt;/b&gt;에서 다음과 같은 재앙이 발생됩니다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터베이스&lt;/b&gt;가 &lt;b&gt;비명&lt;/b&gt;을 지릅니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 &lt;b&gt;데이터베이스&lt;/b&gt;에 '&lt;b&gt;post&lt;/b&gt;' 라는 테이블에 &lt;b&gt;1억 건의 레코드&lt;/b&gt;가 있다고 해봅시다. &quot;&lt;b&gt;SELECT * FROM post;&lt;/b&gt;&quot; 같은 쿼리는 &lt;b&gt;1억 개&lt;/b&gt;의 데이터를 모두 &lt;b&gt;디스크에서&lt;/b&gt; 읽어와야 합니다. 이 작업 하나만으로도 &lt;b&gt;데이터베이스는 엄청난 부하&lt;/b&gt;를 받게 되며, &lt;b&gt;데이터베이스 서버&lt;/b&gt;의 &lt;b&gt;CPU&lt;/b&gt;와 &lt;b&gt;메모리 자원&lt;/b&gt;을 &lt;b&gt;크게 소진&lt;/b&gt;시킵니다.&lt;/li&gt;
&lt;li&gt;결과적으로 &lt;b&gt;쿼리 하나 때문에&lt;/b&gt;, 다른 &lt;b&gt;모든 API 요청&lt;/b&gt;들까지 &lt;b&gt;느려지거나&lt;/b&gt; &lt;b&gt;응답 불가능 상태&lt;/b&gt;에 빠질 수 있게됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cX6mKr/dJMcadf72AT/AAAAAAAAAAAAAAAAAAAAAHq4Ar_pJ8n0dzhRk-ErISJ_EqXxndyWSTEFzb3AzhX3/img.jpg?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1767193199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=q84K05GAOVbCRc1ebLCHzQpXp7g%3D&quot; alt=&quot;&quot; width=&quot;457&quot; height=&quot;360&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네트워크&lt;/b&gt;가 막혀버립니다. (네트워크 과부화)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 &lt;b&gt;1억 건의 게시글 데이터&lt;/b&gt;를 &lt;b&gt;JSON 형태&lt;/b&gt;로 만들어 클라이언트에게 보내는 데는 엄청난 시간이 걸립니다. 그리고 &lt;b&gt;수십 mb, 수백 mb에 달하는 데이터&lt;/b&gt;를 &lt;b&gt;인터넷을 통해 전송하는 것&lt;/b&gt;은 &lt;b&gt;매우매우!!! 비효율적&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;결과적으로 사용자는 하얀 화면만 보면서 하염없이 기다려야 합니다.. &lt;b&gt;모바일 환경&lt;/b&gt;에서는 &lt;b&gt;사용자의 소중한 데이터를 순식간에 전부 소진시켜&lt;/b&gt; 버릴 수도 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자의 기기(브라우저, 앱)&lt;/b&gt;가 &lt;b&gt;멈춰버립니다.&lt;/b&gt; (클라이언트 성능 저하)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어찌저찌 수백 mb의 데이터를 모두 받은 &lt;b&gt;브라우저&lt;/b&gt;나 &lt;b&gt;앱&lt;/b&gt;은 이제 그 데이터를 &lt;b&gt;화면에 그려내야&lt;/b&gt; 합니다. &lt;b&gt;1억 개의 데이터&lt;/b&gt;에 대한 &lt;b&gt;UI 컴포넌트를 한 번에 렌더링하려고 시도&lt;/b&gt;하면, &lt;b&gt;기기의 메모리를 초과&lt;/b&gt;하여 &lt;b&gt;앱이 느려지거나&lt;/b&gt;, &lt;b&gt;멈추거나&lt;/b&gt;, &lt;b&gt;강제 종료&lt;/b&gt;될 수 있습니다.&lt;/li&gt;
&lt;li&gt;결과적으로 &lt;b&gt;사용자 경험&lt;/b&gt;은 &lt;b&gt;최악&lt;/b&gt;으로 치닫고, &lt;b&gt;다시는 그 서비스를 이용하지 않을 겁니다..&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;242&quot; data-origin-height=&quot;208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkpmMz/dJMcaaRilx3/pUkcI3KigypBKsTaHIsPM0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkpmMz/dJMcaaRilx3/pUkcI3KigypBKsTaHIsPM0/img.jpg&quot; data-alt=&quot;뿔난 사용자&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkpmMz/dJMcaaRilx3/pUkcI3KigypBKsTaHIsPM0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkpmMz%2FdJMcaaRilx3%2FpUkcI3KigypBKsTaHIsPM0%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;257&quot; height=&quot;221&quot; data-origin-width=&quot;242&quot; data-origin-height=&quot;208&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;뿔난 사용자&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Spring Boot 에서 페이징 기법 사용을 위한 유용한 도구들&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위에서 얘기했던 문제들처럼 우리는 &lt;b&gt;페이징 없이 대용량의 데이터를 처리하는 서비스를 만들기가 거의 불가능하다&lt;/b&gt;는 것을 확인했습니다! 그렇다면 복잡한 &lt;b&gt;페이징 로직&lt;/b&gt;(&lt;b&gt;LIMIT, OFFSET, COUNT, WHERE 쿼리&lt;/b&gt; 등)을 개발자가 매번 직접 구현해야 할까요?&lt;/li&gt;
&lt;li&gt;다행히도 &lt;b&gt;스프링의 JPA&lt;/b&gt;는 이 모든 과정을 &lt;b&gt;거의 자동화&lt;/b&gt;해주는 강력한 &quot;&lt;b&gt;페이징 삼총사&lt;/b&gt;&quot;를 제공합니다!
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Pageable 인터페이스&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;&lt;b&gt;어떻게 데이터를 가져올지&lt;/b&gt;&quot;에 대한 요청 규격(인터페이스)입니다.&lt;/li&gt;
&lt;li&gt;페이징을 요청하려면 &quot;&lt;b&gt;페이지 번호&lt;/b&gt;&quot;, &quot;&lt;b&gt;페이지 크기&lt;/b&gt;&quot;, &quot;&lt;b&gt;정렬 방식&lt;/b&gt;&quot; 정보가 필요하다는 약속입니다.&lt;/li&gt;
&lt;li&gt;다음은 가장 기본적이고 널리 쓰이는 표준 방식 예시 코드입니다. (후술할 &lt;b&gt;커스텀 validator 어노테이션&lt;/b&gt; 사용 방법도 있음)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@GetMapping(&quot;/api/posts&quot;)
public ApiResponse&amp;lt;...&amp;gt; getPosts(Pageable pageable){ // Pageable 객체를 직접 받음
  // 여기에서 서비스로 Pageable 객체를 넘겨서 로직 처리
  // pageable.getPageNumber(); -&amp;gt; 인덱스처럼 처음이 0인 0-based 페이지 번호
  // pageable.getPageSize(); -&amp;gt; 페이지의 크기 (기본값 20)
  // pageable.getSort(); -&amp;gt; 정렬 정보
  return ...;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 코드처럼 &lt;b&gt;Pageable 인터페이스를 선언&lt;/b&gt;해두면, &lt;b&gt;스프링&lt;/b&gt;이 &lt;b&gt;url의 쿼리 파라미터&lt;/b&gt;(?page=...&amp;amp;size=...&amp;amp;sort=...)를 보고 알아서 해당 규격에 맞는 객체(pageable)를 만들어줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PageRequest 객체&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Pageable&lt;/b&gt; 이라는 '&lt;b&gt;양식&lt;/b&gt;'에 구체적인 내용을 채워넣은 &lt;b&gt;실제 요청서&lt;/b&gt;(클래스) 입니다.&lt;/li&gt;
&lt;li&gt;예를 들어 &quot;&lt;b&gt;3번 페이지의 데이터 10개를 최신순으로 정렬해서 주세요~&lt;/b&gt;&quot; 라는 구체적인 요청 내용을 담고 있는 객체입니다. 주로 &lt;b&gt;PageRequest.of(...)&lt;/b&gt; 메서드를 통해 생성하며, &lt;b&gt;서비스 계층&lt;/b&gt;에서 &lt;b&gt;데이터베이스&lt;/b&gt;에 &lt;b&gt;요청을 보내기 직전&lt;/b&gt;에 사용됩니다.&lt;/li&gt;
&lt;li&gt;다음은 실제 사용되는 객체 사용 예시입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 3번 페이지의 데이터 10개를 최신순으로 정렬
// PageRequest.of(페이지, 갯수(limit 절과 동일), 정렬 방식), 페이지는 0부터 시작
Pageable pageRequest = PageRequest.of(2, 10, Sort.by(&quot;createdAt&quot;).descending());&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 코드처럼 작성하여 &lt;b&gt;페이징을 할 조건&lt;/b&gt;을 지정하면 됩니다! &lt;b&gt;반환되는 타입&lt;/b&gt;은 &lt;b&gt;PageRequest&lt;/b&gt; 이나, 보통 &lt;b&gt;JpaRepository&lt;/b&gt;는 더 넓은 범위인, &lt;b&gt;Pageable 인터페이스 타입&lt;/b&gt;을 원합니다.(다형성)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Page 인터페이스&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;페이징 쿼리&lt;/b&gt;의 &lt;b&gt;결과를 담는 그릇&lt;/b&gt;(인터페이스)입니다. 단순한 데이터 목록(List)이 절대 아닙니다!&lt;/li&gt;
&lt;li&gt;&lt;b&gt;작성된 요청서&lt;/b&gt;(PageRequest)에 따라 &lt;b&gt;데이터베이스의 조회&lt;/b&gt;를 마친 후, &lt;b&gt;실제 데이터와 함께 온갖 유용한 통계 정보&lt;/b&gt;들을 함께 담아주는 &quot;&lt;b&gt;종합 보고서&lt;/b&gt;&quot; 역할을 합니다. 다음은 그 &lt;b&gt;보고서의 내용물&lt;/b&gt;(주요 메서드)을 소개하겠습니다. 소개하려는 메서드 외에도 더욱 많습니다!
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;getContent()&lt;/b&gt;: &lt;b&gt;이번 페이지&lt;/b&gt;에 해당하는 &lt;b&gt;실제 데이터 목록&lt;/b&gt; (List)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;getTotalElements()&lt;/b&gt;: &lt;b&gt;필터링된 전체 데이터&lt;/b&gt;의 &lt;b&gt;총 갯수&lt;/b&gt; (Integer)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;getTotalPages()&lt;/b&gt;: &lt;b&gt;전체 페이지 수&lt;/b&gt; (Integer)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;isFirst()&lt;/b&gt;, &lt;b&gt;isLast()&lt;/b&gt;: &lt;b&gt;첫 페이지인지, 마지막 페이지인지 여부&lt;/b&gt; (true/false)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;getSize()&lt;/b&gt;, &lt;b&gt;getNumber()&lt;/b&gt;, &lt;b&gt;hasNext()&lt;/b&gt;... 등등이 있습니다..!!&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 페이징 삼총사들을 어떻게 써먹을 것인가? (표준 방식)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위에서 &lt;b&gt;Page&lt;/b&gt;, &lt;b&gt;PageRequest&lt;/b&gt;, &lt;b&gt;Pageable&lt;/b&gt; 라는 &lt;b&gt;JPA&lt;/b&gt;에서 제공하는 &lt;b&gt;페이징 삼총사들&lt;/b&gt;의 개념을 살펴보았습니다. 이제 이 삼총사가 실제 프로젝트의 각 계층(&lt;b&gt;Controller&lt;/b&gt;, &lt;b&gt;Service&lt;/b&gt;, &lt;b&gt;Repository&lt;/b&gt;)에서 어떻게 &lt;b&gt;유기적으로 협력&lt;/b&gt;하여 &lt;b&gt;페이징 API&lt;/b&gt;를 완성하는지, '&lt;b&gt;가게 리뷰 목록 조회&lt;/b&gt;' 기능을 예시로 단계별로 설명해 보겠습니다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 먼저, &lt;b&gt;데이터베이스와 통신&lt;/b&gt;하는 &lt;b&gt;Repository 계층&lt;/b&gt;에 페이징을 위한 '&lt;b&gt;규격&lt;/b&gt;'을 정의합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/ol&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Repository
public interface ReviewRepository extends JpaRepository&amp;lt;Review, Long&amp;gt; {
    // storeId(기본키)로 리뷰를 찾는데, Pageable 정보에 맞춰서 페이징 해달라고 약속
    Page&amp;lt;Review&amp;gt; findByStoreId(Long storeId, Pageable pageable);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다음으로, &lt;b&gt;Service&lt;/b&gt; &lt;b&gt;계층&lt;/b&gt;에서 위의 &lt;b&gt;Repository&lt;/b&gt;를 호출하여 &lt;b&gt;비즈니스 로직&lt;/b&gt;을 수행합니다. 여기서는 스프링의 표준 방식을 최대한 활용하여, &lt;b&gt;Pageable&amp;nbsp;&lt;/b&gt;객체를 그대로 전달하는 간결한 형태로 구현해 보겠습니다. 먼저 &lt;b&gt;인터페이스&lt;/b&gt;를 통해 메서드를 정의합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1765271838748&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface ReviewService {
	// Pageable 객체를 그대로 받는 메서드를 인터페이스에서 정의
    Page&amp;lt;Review&amp;gt; getReviewByStoreId(Long storeId, Pageable pageable);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그 후, 해당 &lt;b&gt;인터페이스&lt;/b&gt;의 &lt;b&gt;구현체&lt;/b&gt;를 구현합니다. &lt;b&gt;Service&lt;/b&gt;의 역할은 &lt;b&gt;Controller&lt;/b&gt;로부터 &lt;b&gt;페이징 요청&lt;/b&gt;을 받아 &lt;b&gt;Repository&lt;/b&gt;에 그대로 '&lt;b&gt;전달&lt;/b&gt;'하는 것입니다. 이 방식의 장점은 &lt;b&gt;Service&lt;/b&gt;가 페이징의 세부 구현(&lt;b&gt;페이지 크기&lt;/b&gt;, &lt;b&gt;정렬 방식&lt;/b&gt; 등)에 얽매이지 않고, 오직 &lt;b&gt;비즈니스 로직에만 집중&lt;/b&gt;할 수 있다는 점입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1765272174969&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
@RequiredArgsConstructor
public class ReviewServiceImpl implements ReviewService {
    private final JPAQueryFactory queryFactory;
    private final ReviewRepository reviewRepository;
    private final StoreRepository storeRepository;
    
    @Transaction(readOnly = true)
    @Override
    public Page&amp;lt;Review&amp;gt; getReviewByStoreId(Long storeId, Pageable pageable) {
        // 가게 존재 여부 확인
        storeRepository.findById(storeId)
                .orElseThrow(() -&amp;gt; new StoreHandler(StoreErrorCode.NOT_FOUND));
        
        // Controller로부터 받은 Pageable 객체를 Repository에 그대로 전달
        return reviewRepository.findByStoreId(storeId, pageable);
    }
}&lt;/code&gt;&lt;/pre&gt;
&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Service&lt;/b&gt;로부터 받은 &lt;b&gt;Page&amp;lt;Review&amp;gt; 객체&lt;/b&gt;를 &lt;b&gt;클라이언트&lt;/b&gt;가 보기 좋게끔 &lt;b&gt;최종 DTO&lt;/b&gt;로 변환합니다. &lt;b&gt;Page 객체&lt;/b&gt;는 &lt;b&gt;단순한 리스트가 아니라&lt;/b&gt;, &lt;b&gt;페이징에 필요한 모든 정보&lt;/b&gt;를 담고 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Converter&lt;/b&gt;에서는 이 객체의 &lt;b&gt;실제 내용물&lt;/b&gt;(&lt;b&gt;getContent()&lt;/b&gt;)과 &lt;b&gt;페이지의 통계 정보&lt;/b&gt;(&lt;b&gt;getTotalPages()&lt;/b&gt;, &lt;b&gt;getTotalElements()&lt;/b&gt;, &lt;b&gt;isLast()&lt;/b&gt; ... 등)를 꺼내서, 설계한&lt;b&gt; 최종 응답 DTO&lt;/b&gt;에 맞게 채워넣는 역할을 합니다!&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1765279706457&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ReviewConverter {
	public static ReviewResDTO.ReviewPreviewListDTO toReviewPreviewListDTO(Page&amp;lt;Review&amp;gt; reviewPage) {
    	List&amp;lt;ReviewResDTO.ReviewPreviewDTO&amp;gt; reviewList = reviewPage.getContent().
        	.stream().map(ReviewConverter::toReviewDTO).toList();
            
        return ReviewResDTO.ReviewPreviewListDTO.builder()
                 .isLast(reviewPage.isLast())
                 .isFirst(reviewPage.isFirst())
                 .totalPage(reviewPage.getTotalPages())
                 .totalElements(reviewPage.getTotalElements())
                 .listSize(reviewList.size())
                 .reviewList(reviewList)
                  .build();
    }
    
    public static toReviewDTO(Review review) {
    	return ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&amp;nbsp;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이제 클라이언트의 &lt;b&gt;http&lt;/b&gt; 요청을 받는 &lt;b&gt;Controller&lt;/b&gt;를 구현합니다. &lt;b&gt;파라미터&lt;/b&gt;에 &lt;b&gt;Pageable&lt;/b&gt;을 선언하면, &lt;b&gt;스프링 MVC&lt;/b&gt;가 &lt;i&gt;&lt;b&gt;?page=1&amp;amp;size=10&amp;amp;sort=createdAt,desc&lt;/b&gt;&lt;/i&gt; 와 같은 &lt;b&gt;쿼리 파라미터&lt;/b&gt;를 자동으로 해석하여 &lt;b&gt;Pageable&lt;/b&gt; 객체를 만들어줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Controller&lt;/b&gt;에서는 편리하게 만들어진 이 객체를 받아 &lt;b&gt;Service&lt;/b&gt;에 전달하고, &lt;b&gt;Service&lt;/b&gt;로부터 받은 &lt;b&gt;Page&lt;/b&gt; 라는 &quot;&lt;b&gt;결과 보고서&lt;/b&gt;&quot;를 &lt;b&gt;Converter&lt;/b&gt;를 통해 &lt;b&gt;최종 응답 형태로 가공&lt;/b&gt;해서 &lt;b&gt;return&lt;/b&gt; 하면 됩니다!&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1765279056995&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/api&quot;)
public class ReviewController {
	private final ReviewQueryService reviewQueryService;
    private final ReviewConverter reviewConverter;
    
    @GetMapping(&quot;/stores/{storeId}/reviews&quot;)
    public ApiResponse&amp;lt;ReviewResDTO.ReviewPreviewListDTO&amp;gt; getStoreReviewList(
    	@PathVariable Long storeId,
        Pageable pageable // Pageable을 파라미터로 선언!!
    ) {
    	Page&amp;lt;Review&amp;gt; reviewPage = reviewQueryService.getStoreReviewList(storeId, pageable);
        
        return ApiResponse.onSuccess(
        	reviewConverter.toReviewPreviewListDTO(reviewPage);
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 살짝 아쉬운 표준 방식..? (커스텀 어노테이션)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앞서 우리는 &lt;b&gt;Pageable 인터페이스&lt;/b&gt;를 이용해 얼마나 간편하게 페이징 API를 구현할 수 있는지 확인했습니다. &lt;b&gt;Repository&lt;/b&gt;에 메서드 한 줄만 추가하면 &lt;b&gt;페이징 쿼리&lt;/b&gt;가 완성이 됐었습니다!&lt;/li&gt;
&lt;li&gt;하지만 이 순정 옵션(?)을 실제 프로젝트에 그대로 적용시키기에는 몇 가지 &lt;b&gt;아쉬운 점&lt;/b&gt;들이 존재합니다. 이번 챕터에서는 그 아쉬운 점들이 무엇인지 함께 살펴보겠습니다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&quot;&lt;b&gt;0-based 인덱스&lt;/b&gt;&quot; - &lt;b&gt;&lt;i&gt;개발자는 편할 수 있어도, 클라이언트는 헷갈릴 수 있다.&lt;/i&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링의 JPA의 &lt;b&gt;Pageable&lt;/b&gt;은 기본적으로 &lt;b&gt;페이지 번호를 0부터 시작&lt;/b&gt;(&lt;b&gt;0-based index&lt;/b&gt;)하는 것으로 인식합니다. 즉, 첫 번째 페이지를 조회하려면 클라이언트는 &lt;b&gt;&lt;i&gt;?page=0&lt;/i&gt;&lt;/b&gt; 으로 요청을 보내야 합니다.&lt;/li&gt;
&lt;li&gt;직관적으로 생각했을 때 당연히 첫 번째 페이지는 &lt;b&gt;page=1&lt;/b&gt; 이라고 생각합니다. 하지만 이 작은 차이인 &lt;b&gt;0부터 시작하는 것&lt;/b&gt;은 &lt;b&gt;페이지가 하나씩 밀리는&lt;/b&gt; 혼란스러운 &lt;b&gt;버그의 원인&lt;/b&gt;이 될 수도 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;nbsp;&lt;b&gt;유효하지 않은 입력값&lt;/b&gt;에 대한 &lt;b&gt;방어 부재&lt;/b&gt; (&lt;b&gt;유효성 검증 관련&lt;/b&gt;)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링의 기본 &lt;b&gt;HandlerMethodArgumentResolver&lt;/b&gt;(&lt;b&gt;인자 검사기 역할&lt;/b&gt;, &lt;b&gt;인터페이스&lt;/b&gt;)는 &lt;b&gt;Pageable 파라미터의 유효성&lt;/b&gt;을 직접적으로 &lt;b&gt;검증해주지 않습니다&lt;/b&gt;. 만약 클라이언트가 &lt;b&gt;유효하지 않은 값&lt;/b&gt;을 보내면 어떻게 될까요? 그러면 클라이언트는 &lt;b&gt;의미를 알 수 없는&lt;/b&gt; &lt;b&gt;500 Internal Server Error 응답&lt;/b&gt;을 받게 됩니다. &lt;b&gt;커스텀 예외 처리&lt;/b&gt;를 통해 &lt;b&gt;어떤 것이 잘못&lt;/b&gt;인지 알려주는 것이 더 좋지 않을까요?!&lt;/li&gt;
&lt;li&gt;다음은 간단한 유효하지 않은 값에 대한 예시입니다.&amp;nbsp;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;음수 값 요청&lt;/b&gt; (&lt;b&gt;?page=-1&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;숫자가 아닌 값 요청&lt;/b&gt; (&lt;b&gt;?page=abc&lt;/b&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서비스 계층&lt;/b&gt;에서의 &lt;b&gt;책임 증가&lt;/b&gt;와 &lt;b&gt;코드 중복&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위의 두 가지 문제를 해결하기 위해, &lt;b&gt;Service 계층&lt;/b&gt;에서 &lt;b&gt;직접 유효성 검증&lt;/b&gt;하는 코드를 넣습니다. 하지만 이 방법은 또 다른 문제를 낳습니다. 다음은 나쁜(?) 해결책 코드 예시입니다.&lt;/li&gt;
&lt;li&gt;먼저, &lt;b&gt;페이지 번호 값&lt;/b&gt;만 받는다고 생각을 하고, &lt;b&gt;Pageable 객체&lt;/b&gt;로 받는 것이 아닌, &lt;b&gt;int형&lt;/b&gt;으로 받도록 하겠습니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1765281286772&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Page&amp;lt;Review&amp;gt; getStoreReviewList(Long storeId, int page) {
	// 클라이언트가 1부터 요청한다고 가정하고, 서비스에서 검증 및 변환
    if(page &amp;lt; 1 || page == null) {
    	throw new MyException(...);
    }
    
    // page - 1에서, 한 페이지 당 5개씩 
	Pageable pageRequest = PageRequest.of(page - 1, 5);
    
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;서비스 계층&lt;/b&gt;의 &lt;b&gt;본질적인 책임&lt;/b&gt;은 &lt;b&gt;비즈니스 로직 처리&lt;/b&gt;입니다. 하지만 위 코드는 &lt;b&gt;웹 파라미터 변환&lt;/b&gt;이라는, &lt;b&gt;웹 계층&lt;/b&gt;에 더 가까운 &lt;b&gt;책임&lt;/b&gt;을 떠안게 되었습니다. 그리고 &lt;b&gt;페이징이 필요한 메서드&lt;/b&gt;마다 저 &lt;b&gt;if문&lt;/b&gt;과 &lt;b&gt;page - 1&lt;/b&gt; 변환 코드가 &lt;b&gt;반복적으로 사용&lt;/b&gt;됩니다.&lt;/li&gt;
&lt;li&gt;나중에 &lt;b&gt;페이지 정책이 바뀌면&lt;/b&gt;(페이지 당 데이터 갯수를 5개에서 10개로 바뀌다던가..), 이 &lt;b&gt;모든 메서드를 찾아서 수정&lt;/b&gt;해야 합니다. 당연히 유지보수도 힘들겠죠?!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;나만의 어노테이션&lt;/b&gt;으로 문제를 해결해보자!
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앞서 우리는 &lt;b&gt;서비스 계층&lt;/b&gt;에서 검증 로직이 추가되면서 &lt;b&gt;코드가 중복&lt;/b&gt;되고, 책임이 모호해지는 &lt;b&gt;아쉬운 점&lt;/b&gt;들을 확인했습니다.&lt;/li&gt;
&lt;li&gt;이제 &lt;b&gt;스프링 MVC&lt;/b&gt;의 &lt;b&gt;강력한 확장 기능&lt;/b&gt;인 &lt;b&gt;HandlerMethodArgumentResolver&lt;/b&gt;를 사용하여, 이 모든 문제를 해결하는 &lt;b&gt;커스텀 어노테이션&lt;/b&gt;을 만들어 보겠습니다.&lt;/li&gt;
&lt;li&gt;어노테이션 이름은 &lt;b&gt;@CheckPage&lt;/b&gt; 로 정해놓고 진행하겠습니다! 해당 어노테이션 자체로는 &lt;b&gt;어떤 동작을 하진 않습니다.&lt;/b&gt; &lt;b&gt;Controller&lt;/b&gt; 메서드의 파라미터에 붙이는 '&lt;b&gt;트리거&lt;/b&gt;'를 붙였다고 생각하면 될 것 같습니다!&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1765328353107&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Target(ElementType.PARAMETER) // 메서드의 파라미터에만 붙일 수 있다는 조건
@Retention(RetentionPolicy.RUNTIME) // 런타임에도 해당 어노테이션의 정보를 유지함을 알림
public @interface CheckPage {
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그 다음 &lt;b&gt;@CheckPage&lt;/b&gt; 트리거가 붙은 주문을 &lt;b&gt;실제로 처리할 메서드&lt;/b&gt;를 만듭니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HandlerMethodArgumentResolver&lt;/b&gt; &lt;b&gt;인터페이스&lt;/b&gt;를 구현한 클래스는 특정 조건의 &lt;b&gt;파라미터에 들어갈 값을 직접 만들어서 반환하는 책임&lt;/b&gt;을 가집니다. 다음은 &lt;b&gt;HandlerMethodArgumentResolver&lt;/b&gt; &lt;b&gt;인터페이스&lt;/b&gt;가 정의하고 있는 메서드와 코드입니다.&lt;br /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;supportsParameter(MethodParameter parameter)&lt;/b&gt;: 특정 어노테이션이 붙었는지 &lt;b&gt;확인하는 역할&lt;/b&gt;을 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; NativeWebRequest webRequest, WebDataBinderFactory binderFactory)&lt;/b&gt;: &lt;b&gt;실제 로직을 처리하는 역할&lt;/b&gt;을 합니다. &lt;b&gt;쿼리 스트링&lt;/b&gt;(&lt;b&gt;Query string&lt;/b&gt;)이나 &lt;b&gt;Path Variable&lt;/b&gt;, 또는 &lt;b&gt;바디 영역&lt;/b&gt;에서 &lt;b&gt;로직을 처리할 값을 꺼내고&lt;/b&gt; &lt;b&gt;원하는 값으로 반환&lt;/b&gt;할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1765330587567&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CheckPageArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        // 처리할 파라미터인지 검사(@CheckPage 어노테이션이 붙어있는지 확인)
        return parameter.hasParameterAnnotation(CheckPage.class);
    }

    @Override
    public Object resolveArgument(
            MethodParameter parameter,
            ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest,
            WebDataBinderFactory binderFactory
    ) throws Exception {
    	// HttpServletRequest 객체를 얻음
        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        // 요청에서 page 값을 얻어옴
        String pageString = request.getParameter(&quot;page&quot;);

        int page;

        try {
            // 값이 없으면 기본값 1로 처리
            page = (pageString == null) ? 1 : Integer.parseInt(pageString);
        } catch (NumberFormatException e) { // 숫자가 아닌 값이 들어왔을 때
            throw new GeneralException(PageErrorCode.BAD_REQUEST);
        }

        // 1미만의 값이 들어왔을 때
        if (page &amp;lt; 1) {
            throw new GeneralException(PageErrorCode.BAD_REQUEST);
        }
		
        // 1부터 시작하기 위해 page - 1의 값을 최종 반환
        return page - 1;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;현재 &lt;b&gt;@CheckPage&lt;/b&gt; 라는 어노테이션이 붙어있을 때,&lt;b&gt; 페이지 정보 값&lt;/b&gt;을 &lt;b&gt;CheckPageArgumentResolver&lt;/b&gt;가 가로채서 해당 &lt;b&gt;쿼리 파라미터를 검증&lt;/b&gt;하고, &lt;b&gt;첫 번째 페이지&lt;/b&gt;가 1부터 시작할 수 있게끔 로직을 담당하고 있습니다.&lt;/li&gt;
&lt;li&gt;하지만 &lt;b&gt;Spring MVC&lt;/b&gt;(&lt;b&gt;Spring 컨테이너 위에서 동작하는 하나의 독립된 프레임워크 모듈&lt;/b&gt;)는 이 클래스의 존재 자체를 &lt;b&gt;자동으로 알지 못합니다&lt;/b&gt;. 지금은 단순히 &lt;b&gt;HandlerMethodArgumentResolver&lt;/b&gt;의 클래스를 구현했다 뿐이지 &lt;b&gt;프레임워크가 알지 못하는 상황&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;그래서! 우리가 만든 &lt;b&gt;CheckPageArgumentResolver&lt;/b&gt; 클래스를 &lt;b&gt;Spring MVC&lt;/b&gt;가 &lt;b&gt;인식&lt;/b&gt;하고 사용하도록 &lt;b&gt;등록&lt;/b&gt;시켜줘야 합니다!&lt;/li&gt;
&lt;li&gt;이는 &lt;b&gt;Spring&lt;/b&gt;이 &lt;b&gt;@RequestParam&lt;/b&gt;, &lt;b&gt;@PathVariable&lt;/b&gt;, &lt;b&gt;@RequestBody&lt;/b&gt;와 같은 &lt;b&gt;표준 어노테이션들&lt;/b&gt;을 &lt;b&gt;해석하는 방식&lt;/b&gt;과 같게끔, &lt;b&gt;@CheckPage&lt;/b&gt; &lt;b&gt;어노테이션이 붙은 파라미터도 자동으로 인식하고 처리할 수 있도록&lt;/b&gt; 하는 과정입니다.&lt;/li&gt;
&lt;li&gt;등록은 &lt;b&gt;WebMvcConfigurer 인터페이스를 구현한 설정 클래스&lt;/b&gt;에서 이루어집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WebMvcConfigurer&lt;/b&gt;: &lt;b&gt;Spring MVC&lt;/b&gt;의 &lt;b&gt;자동 설정&lt;/b&gt;을 완전히 &lt;b&gt;대체하는 것이 아니라&lt;/b&gt;, &lt;b&gt;기존 설정을 그대로 유지&lt;/b&gt;하면서 &lt;b&gt;필요한 부분만 추가&lt;/b&gt;하거나 &lt;b&gt;재정의&lt;/b&gt;할 수 있는 &lt;b&gt;Hook&lt;/b&gt;을 제공합니다. &lt;b&gt;WebMvcConfigurer&lt;/b&gt;의 주요 특징은 다음과 같습니다.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Spring MVC&lt;/b&gt;의 &lt;b&gt;내부 동작을 건드리지 않고&lt;/b&gt;, &lt;b&gt;약속된 메서드&lt;/b&gt;를 &lt;b&gt;오버라이드&lt;/b&gt;하는 것만으로 설정을 변경할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Argument Resolver&lt;/b&gt; 등록 외에도 &lt;b&gt;인터셉터 추가&lt;/b&gt;(&lt;b&gt;addInterceptors&lt;/b&gt;), &lt;b&gt;CORS 설정&lt;/b&gt;(&lt;b&gt;addCorsMappings&lt;/b&gt;) 등 &lt;b&gt;MVC&lt;/b&gt; 전반에 걸친 &lt;b&gt;다양한 설정을 변경할 수 있는&lt;/b&gt; &lt;b&gt;메서드&lt;/b&gt;를 제공합니다.&lt;/li&gt;
&lt;li&gt;보통 &lt;b&gt;@Configuration&lt;/b&gt; &lt;b&gt;어노테이션&lt;/b&gt;이 붙은 &lt;b&gt;설정 클래스&lt;/b&gt;에서 &lt;b&gt;WebConfigurer를 구현하여 사용&lt;/b&gt;합니다. 이를 통해 &lt;b&gt;Spring 컨테이너&lt;/b&gt;가 &lt;b&gt;서버 구동 시&lt;/b&gt; &lt;b&gt;해당 설정 정보를 읽어들이게끔 하여 MVC 설정에 반영&lt;/b&gt;합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;addArgumentResolvers()&lt;/b&gt;: &lt;b&gt;WebMvcConfigurer&lt;/b&gt;가 제공하는 여러 메서드 중 하나로, &lt;b&gt;HandlerMethodArgumentResolver&lt;/b&gt;의 &lt;b&gt;구현체들을 등록하는 데 특화된 메서드&lt;/b&gt;입니다. 동작 방식 및 개념은 다음과 같습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;호출 시점&lt;/b&gt;: &lt;b&gt;Spring 애플리케이션이 시작될 때&lt;/b&gt;, &lt;b&gt;Spring MVC&lt;/b&gt;는 &lt;b&gt;WebMvcConfigurer&lt;/b&gt;를 &lt;b&gt;구현한 모든 설정 클래스를 찾아&lt;/b&gt; &lt;b&gt;addArgumentResolvers&lt;/b&gt; 메서드를 호출합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;resolvers 파라미터&lt;/b&gt;: 메서드가 호출될 때, &lt;b&gt;Spring&lt;/b&gt;은 기본적으로 내장된 모든 &lt;b&gt;Argument Resolver&lt;/b&gt;들이 이미 들어있는 &lt;b&gt;List 객체&lt;/b&gt;를 &lt;b&gt;파라미터&lt;/b&gt;로 전달해 줍니다. 이 리스트에는 &lt;b&gt;@RequestParam&lt;/b&gt;, &lt;b&gt;@PathVariable&lt;/b&gt;, &lt;b&gt;@RequestBody&lt;/b&gt; 등을 처리하는 &lt;b&gt;기본 resolver&lt;/b&gt;들이 포함되어 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;등록 과정&lt;/b&gt;: 이 &lt;b&gt;resolvers 리스트&lt;/b&gt;에 우리가 직접 만든 &lt;b&gt;CheckPageArgumentResolver&lt;/b&gt;의 인스턴스를 &lt;b&gt;add&lt;/b&gt; 해주기만 하면 됩니다!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1766164133824&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration // 해당 클래스가 Spring의 설정 파일임을 알림
public class WebConfig implements WebMvcConfigurer {
	@Override
    public void addArgumentResolvers(
        List&amp;lt;HandlerMethodArgumentResolver&amp;gt; resolvers
    ) {
        // 직접 만든 CheckPageArgumentResolver를 Spring MVC의 Argument Resolver 목록에 등록
        resolvers.add(new CheckPageArgumentResolver());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;이제 실제로 테스트를 해보겠습니다. 테스트 요청 및 응답 확인은 기존에 제가 사용하던 &lt;b&gt;Swaager UI&lt;/b&gt;로 확인을 해보겠습니다!&lt;/li&gt;
&lt;li&gt;저는 테스트 데이터를 &lt;b&gt;MySQL 데이터베이스&lt;/b&gt;에 미리 삽입을 해놓았습니다!&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가게의 id&lt;/b&gt; 및 &lt;b&gt;1부터 시작하는 페이지 번호&lt;/b&gt;를 입력 후 요청을 아래 사진과 같이 날리게 되면..!!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0osB9/dJMcagRCukZ/Y0sBhciim6KJ9DRZMPNkM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0osB9/dJMcagRCukZ/Y0sBhciim6KJ9DRZMPNkM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0osB9/dJMcagRCukZ/Y0sBhciim6KJ9DRZMPNkM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0osB9%2FdJMcagRCukZ%2FY0sBhciim6KJ9DRZMPNkM1%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;579&quot; height=&quot;296&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;554&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다음과 같이 결과가 나오는 것을 확인할 수 있습니다! 상세 응답 데이터는 코드 블럭으로 넣어두겠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;1160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/P4IEl/dJMcaiIC9Od/GwwrqPqte5gGcWtR3fUzM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/P4IEl/dJMcaiIC9Od/GwwrqPqte5gGcWtR3fUzM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/P4IEl/dJMcaiIC9Od/GwwrqPqte5gGcWtR3fUzM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FP4IEl%2FdJMcaiIC9Od%2FGwwrqPqte5gGcWtR3fUzM1%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;375&quot; height=&quot;456&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;1160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1766242370999&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;isSuccess&quot;: true,
  &quot;code&quot;: &quot;REVIEW200_1&quot;,
  &quot;message&quot;: &quot;리뷰가 성공적으로 조회되었습니다.&quot;,
  &quot;result&quot;: {
    &quot;reviewList&quot;: [
      {
        &quot;ownerNickname&quot;: &quot;슈팅스타&quot;,
        &quot;rating&quot;: 4.5,
        &quot;body&quot;: &quot;정말 맛있어요! 첫 번째 리뷰입니다.&quot;,
        &quot;createdAt&quot;: &quot;2025-12-20T14:48:27&quot;
      },
      {
        &quot;ownerNickname&quot;: &quot;슈팅스타&quot;,
        &quot;rating&quot;: 5,
        &quot;body&quot;: &quot;분위기가 너무 좋아요. 강추!&quot;,
        &quot;createdAt&quot;: &quot;2025-12-20T14:48:27&quot;
      },
      {
        &quot;ownerNickname&quot;: &quot;슈팅스타&quot;,
        &quot;rating&quot;: 3.5,
        &quot;body&quot;: &quot;가격이 조금 비싸지만 맛은 있네요.&quot;,
        &quot;createdAt&quot;: &quot;2025-12-20T14:48:27&quot;
      },
      {
        &quot;ownerNickname&quot;: &quot;슈팅스타&quot;,
        &quot;rating&quot;: 4,
        &quot;body&quot;: &quot;직원분들이 친절해서 좋았어요.&quot;,
        &quot;createdAt&quot;: &quot;2025-12-20T14:48:27&quot;
      },
      {
        &quot;ownerNickname&quot;: &quot;슈팅스타&quot;,
        &quot;rating&quot;: 2.5,
        &quot;body&quot;: &quot;기대보다는 별로였어요.&quot;,
        &quot;createdAt&quot;: &quot;2025-12-20T14:48:27&quot;
      }
    ],
    &quot;listSize&quot;: 5,
    &quot;totalPage&quot;: 4,
    &quot;totalElements&quot;: 20,
    &quot;isFirst&quot;: true,
    &quot;isLast&quot;: false
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결과는 대성공! &lt;b&gt;총 20개의 데이터&lt;/b&gt; 중 &lt;b&gt;5개만 가져온 것을 확인&lt;/b&gt;할 수 있으며, 정상적으로 &lt;b&gt;페이지 번호 1번이 첫 번째 페이지&lt;/b&gt;로 인식되고 있는 것을 확인할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 마치며...&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위에서 진행했던 내용들을 요약하자면 다음과 같습니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Q.&lt;/b&gt; &lt;b&gt;페이징 처리를 왜&lt;/b&gt; 하는가?&lt;/li&gt;
&lt;li&gt;&lt;b&gt;A.&lt;/b&gt; 안하면 &lt;b&gt;많은 데이터를 한꺼번에 요청으로 보낼 때&lt;/b&gt;, &lt;b&gt;별의별 큰 문제&lt;/b&gt;들이 생기기 때문입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Q.&lt;/b&gt; &lt;b&gt;Spring Boot&lt;/b&gt;에서 &lt;b&gt;페이징 기법&lt;/b&gt;을 무엇으로 구현하면 되겠는가?&lt;/li&gt;
&lt;li&gt;&lt;b&gt;A.&lt;/b&gt; &lt;b&gt;SQL 구문&lt;/b&gt;으로 직접적으로 &lt;b&gt;페이징 처리&lt;/b&gt;를 할 수 있지만 &lt;b&gt;Spring Boot&lt;/b&gt;의 &lt;b&gt;JPA&lt;/b&gt;는 &lt;b&gt;Pageable&lt;/b&gt;, &lt;b&gt;PageRequest&lt;/b&gt;, &lt;b&gt;Page&lt;/b&gt; 와 같은 도구가 있어, &lt;b&gt;더욱 간단하게 구현이 가능&lt;/b&gt;합니다. 하지만 실제로 &lt;b&gt;SQL 구문&lt;/b&gt;으로 직접 &lt;b&gt;OFFSET 페이징&lt;/b&gt;이나, &lt;b&gt;CURSOR 페이징&lt;/b&gt;을 구현해보는 것도 좋을 것 같습니다! 참고로, &lt;b&gt;페이징 객체들로 구현한 방식&lt;/b&gt;은 내부적으로 &lt;b&gt;OFFSET 페이징 방식&lt;/b&gt;으로 동작한다는 것을 알아두셨으면 좋겠습니다!&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Q.&lt;/b&gt; 왜 굳이 복잡하게 &lt;b&gt;커스텀 어노테이션&lt;/b&gt;을 직접 만들어서 사용하는가?&lt;/li&gt;
&lt;li&gt;&lt;b&gt;A.&lt;/b&gt; &lt;b&gt;페이지 번호의 유효성 검증 및 page -1 로직&lt;/b&gt; 같은 것을 &lt;b&gt;Service 계층&lt;/b&gt;에서 처리하는 것은 &lt;b&gt;코드의 중복이 많아져 비효율적&lt;/b&gt;이고, &lt;b&gt;유지보수성이 저하&lt;/b&gt;됩니다. 그래서 &lt;b&gt;중앙화된 관리&lt;/b&gt;를 위해 &lt;b&gt;어노테이션 방식&lt;/b&gt;으로 진행을 했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;i&gt;&lt;b&gt;부족하고, 긴 글이지만 읽어주셔서 감사합니다! 피드백은 언제나 환영입니다!&lt;/b&gt;&amp;nbsp;☺️&lt;/i&gt;&lt;/p&gt;</description>
      <author>shootingstar-1117</author>
      <guid isPermaLink="true">https://shootingstar-1117.tistory.com/2</guid>
      <comments>https://shootingstar-1117.tistory.com/2#entry2comment</comments>
      <pubDate>Sun, 21 Dec 2025 00:33:03 +0900</pubDate>
    </item>
    <item>
      <title>[Django] 직렬화 / 역직렬화(serialization / deserialization)의 이해</title>
      <link>https://shootingstar-1117.tistory.com/1</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 직렬화와 역직렬화, 왜 필요할까?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;직렬화(Serialization)&lt;/b&gt;: Django 서버는 &lt;b&gt;파이썬 객체(object)&lt;/b&gt;로써 데이터를 이해합니다. 하지만&lt;b&gt;&amp;nbsp;클라이언트&lt;/b&gt;(웹, 모바일 앱 등)는 파이썬 객체를 알지 못합니다. 그래서 클라이언트는 보통 json이나 xml 형태의 데이터를 사용합니다. 여기서 &lt;b&gt;직렬화&lt;/b&gt;는 &lt;b&gt;파이썬 객체&lt;/b&gt;를 &lt;b&gt;json, xml 같이&amp;nbsp; 클라이언트가 이해할 수 있는 데이터&lt;/b&gt;&amp;nbsp;&lt;b&gt;형태&lt;/b&gt;로 변환하는 과정을 뜻합니다. 보통 &lt;b&gt;서버&lt;/b&gt;에서 &lt;b&gt;클라이언트&lt;/b&gt;로 데이터를 보낼 때 해당 과정을 거칩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;역직렬화(Deserialization)&lt;/b&gt;: 직렬화의 &lt;b&gt;반대&lt;/b&gt;입니다. 예를 들어서, &lt;b&gt;클라이언트&lt;/b&gt;가 &lt;b&gt;서버&lt;/b&gt;에게 &quot;내가 보낸 데이터로 새로운 게시물을 만들어줘!&quot; 라고 요청할 때, &lt;b&gt;json 데이터&lt;/b&gt;를 보냅니다. 그럼 &lt;b&gt;Django 서버&lt;/b&gt;는 해당 &lt;b&gt;json 데이터&lt;/b&gt;를 그대로 이해할 수 없으므로, 서버에서 알아들을 수 있는 &lt;b&gt;파이썬 객체&lt;/b&gt;로 다시 번역하는 과정이 필요합니다. 여기서 &lt;b&gt;역직렬화&lt;/b&gt;는&lt;b&gt; json, xml 같은 데이터&lt;/b&gt;를 &lt;b&gt;파이썬 객체&lt;/b&gt;로써&lt;b&gt; Django 서버&lt;/b&gt;가 이해할 수 있는 데이터로 변환하는 과정을 뜻합니다. 보통 &lt;b&gt;클라이언트&lt;/b&gt;가 &lt;b&gt;서버&lt;/b&gt;로부터 데이터를 보낼 때(요청할 때) 해당 과정을 거칩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 직렬화를 통해 파이썬 객체를 json으로 변환하기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;직렬화&lt;/b&gt;는 보통 &lt;b&gt;데이터베이스&lt;/b&gt;에 이미 존재하는 데이터를 &lt;b&gt;클라이언트&lt;/b&gt;에게 보여줄 때 사용된다고 앞서 말씀드렸습니다. &lt;b&gt;http 메서드&lt;/b&gt;로 따지면&lt;b&gt; GET 요청&lt;/b&gt;을 처리하는 경우입니다.&lt;/li&gt;
&lt;li&gt;예를 들어, 다음과 같이 사용자가 했던 질문에 관한 모델 &lt;b&gt;Question&lt;/b&gt;과 해당 모델을 역/직렬화 하는 &lt;b&gt;QuestionSerializer&lt;/b&gt;가 있다고 해봅시다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1752677683561&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; class Question(models.Model):
    question_text = models.CharField(max_length=200) # 질문에 대한 텍스트 필드
    pub_date = models.DateTimeField('date published') # 해당 질문이 생성된 날짜 필드
    owner = models.ForeignKey(User, related_name='questions', on_delete=models.CASCADE) # 질문에 대한 사용자 필드&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 &lt;b&gt;클래스 변수&lt;/b&gt;가 의미하는 바는 &lt;b&gt;데이터베이스의 필드&lt;/b&gt;와 같다고 생각하시면 됩니다.&lt;/li&gt;
&lt;li&gt;owner 필드는 &lt;b&gt;ForeignKey&lt;/b&gt;, 즉 &lt;b&gt;외래키&lt;/b&gt;입니다. Django는 기본적으로 &lt;b&gt;유저를 관리&lt;/b&gt;하기 위해&lt;b&gt;django.contrib.auth.models&lt;/b&gt;&amp;nbsp;내부에 &lt;b&gt;User&lt;/b&gt; 라는 테이블을 가지고 있습니다. &lt;b&gt;settings.py&lt;/b&gt;에서 유저를 관리하는 테이블을 따로 지정할 수 있지만, 저는 기본 제공되는 유저 테이블을 사용해 보았습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1752677706101&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class QuestionSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source=&quot;owner.username&quot;)
    
    class Meta:
        model = Question
        fields = [&quot;id&quot;, &quot;question_text&quot;, &quot;pub_date&quot;, &quot;owner&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;serializers.ReadOnlyField&lt;/b&gt;는 &lt;b&gt;클라이언트 측&lt;/b&gt;에서 &lt;b&gt;읽기&lt;/b&gt;만 가능하게 &lt;b&gt;접근을 제한&lt;/b&gt;하는 필드를 나타냅니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;source=&quot;owner.username&quot;&lt;/b&gt; 인수는 &lt;b&gt;owner 필드를 직렬화 &lt;/b&gt;할건데, 실제 직렬화 데이터를 &lt;b&gt;owner&lt;/b&gt; 안에 있는 &lt;b&gt;username&lt;/b&gt; 속성으로 지정하겠다. 라는 의미입니다.&lt;/li&gt;
&lt;li&gt;클래스 내부에 &lt;b&gt;Meta 클래스&lt;/b&gt;에서 &lt;b&gt;model은 어떤 모델을 역/직렬화 할 것인지를 나타내고&lt;/b&gt;, &lt;b&gt;fields는 데이터베이스의 어떤 필드를 역/직렬화 할 것인지 설정&lt;/b&gt;합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. Question 객체를 직렬화하는 과정의 흐름 살펴보기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;Primary Key(이하 pk) 값이 1인 Question 객체를 직렬화하는 과정을 Django shell 환경에서 살펴보겠습니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1752678368131&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; from polls.models import Question
&amp;gt;&amp;gt;&amp;gt; from polls_api.serializers import QuestionSerializer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;우선, 임의로 만든 앱들에서 Question 모델과 QuestionSerializer를 import 해옵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1752678614336&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; question = Question.objects.get(pk=14)
&amp;gt;&amp;gt;&amp;gt; serializer = QuestionSerializer(instance=question)
&amp;gt;&amp;gt;&amp;gt; serializer.data
{'id': 14, 'question_text': 'postman에서 보는 제목', 'pub_date': '2025-07-29T02:06:36.926054Z', 'owner': 'user1'}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;question 변수에 &lt;b&gt;pk&lt;/b&gt; 값이 14인 Question 객체를 할당합니다.&lt;/li&gt;
&lt;li&gt;QuestionSerializer 클래스의 &lt;b&gt;instance&lt;/b&gt; 인수로 Question 객체를 넘겨 &lt;b&gt;serializer&lt;/b&gt; 변수에 할당합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;instance&lt;/b&gt; 인수에 모델 객체를 설정하면 기존의 데이터베이스에서 그 객체를 가져와 직렬화를 수행합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;serializer의 data 값을 출력&lt;/b&gt;하면, Question의 각 필드 값이 나오게 됩니다. &lt;b&gt;({'id':&amp;nbsp;14,&amp;nbsp;'question_text':&amp;nbsp;'postman에서&amp;nbsp;보는&amp;nbsp;제목',&amp;nbsp;'pub_date':&amp;nbsp;'2025-07-29T02:06:36.926054Z',&amp;nbsp;'owner':&amp;nbsp;'user1'})&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;여기서 중요한 포인트!&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;직렬화를 할 때는 반드시 &quot;&lt;b&gt;instance&lt;/b&gt;&quot; 키워드 인자를 사용하여 변환할 객체를 지정해줘야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;serializer.data&lt;/b&gt;의 결과물은 아직&lt;b&gt; json 문자열&lt;/b&gt;이 아닙니다. &lt;b&gt;파이썬의 딕셔너리('dict')&lt;/b&gt; 형태입니다. 해당 파이썬 딕셔너리를 클라이언트에게 보내기 위해서는 &lt;b&gt;JSONRenderer&lt;/b&gt;를 통해 최종적으로 클라이언트가 이해할 수 있는 &lt;b&gt;json 문자열&lt;/b&gt;로 변환하여 응답해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. Postman에서 응답 확인해보기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GET 요청을 통해 해당 값을 조회해보겠습니다. api 요청 경로는 &lt;span style=&quot;background-color: #212121; color: #ffffff; text-align: left;&quot;&gt;http://127.0.0.1:8000/rest/question/14/&lt;/span&gt; 으로 설정해두었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2095&quot; data-origin-height=&quot;762&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rrCM6/btsPIvRdBwS/QTDHacNiTJNaakggKhcCZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rrCM6/btsPIvRdBwS/QTDHacNiTJNaakggKhcCZ1/img.png&quot; data-alt=&quot;GET 요청을 보내서 응답 받은 JSON 형태의 데이터&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rrCM6/btsPIvRdBwS/QTDHacNiTJNaakggKhcCZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrrCM6%2FbtsPIvRdBwS%2FQTDHacNiTJNaakggKhcCZ1%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;2095&quot; height=&quot;762&quot; data-origin-width=&quot;2095&quot; data-origin-height=&quot;762&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;GET 요청을 보내서 응답 받은 JSON 형태의 데이터&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 과정을 요약하자면 &lt;b&gt;클라이언트 측&lt;/b&gt;에서 특정&lt;b&gt; 데이터를 요청, 조회&lt;/b&gt;할 때&lt;b&gt; Django의 파이썬 객체&lt;/b&gt;를 &lt;b&gt;직렬화&lt;/b&gt;하여 클라이언트가 확인할 수 있는 형태로 &lt;b&gt;데이터를 가공&lt;/b&gt;하여 응답을 보내줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 역직렬화를 통해 json을 파이썬 객체로 변환하기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역직렬화&lt;/b&gt;는 &lt;b&gt;클라이언트로부터 받은 데이터로 새로운 객체를 만들거나(POST)&lt;/b&gt;, &lt;b&gt;기존 객체를 수정할 때(PUT, PATCH)&lt;/b&gt; 사용됩니다. 이 과정은 직렬화보다 조금 더 복잡하고 중요합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클라이언트가&lt;/b&gt; 다음과 같은 json 데이터를 보내서 새로운 질문을 생성한다고 해봅시다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2102&quot; data-origin-height=&quot;986&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/P5Ksp/btsPNHh8qlM/Xyue1djs8XbRM4Tan5CKUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/P5Ksp/btsPNHh8qlM/Xyue1djs8XbRM4Tan5CKUK/img.png&quot; data-alt=&quot;POST 요청을 보내서 body의 내용을 Question 테이블에 저장(csrf token 값과 session id 값은 보안상 임의 값으로 대체)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/P5Ksp/btsPNHh8qlM/Xyue1djs8XbRM4Tan5CKUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FP5Ksp%2FbtsPNHh8qlM%2FXyue1djs8XbRM4Tan5CKUK%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;986&quot; data-origin-width=&quot;2102&quot; data-origin-height=&quot;986&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;POST 요청을 보내서 body의 내용을 Question 테이블에 저장(csrf token 값과 session id 값은 보안상 임의 값으로 대체)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성공적으로 질문이 등록되었습니다. Django shell에서 실제로 데이터베이스에 해당 데이터가 잘 저장됐는지와, 구체적으로 &lt;b&gt;역직렬화&lt;/b&gt; 과정이 어떻게 이루어지는지 살펴보겠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1754664514583&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; question = Question.objects.get(pk=10002)
&amp;gt;&amp;gt;&amp;gt; question
&amp;lt;Question: new!!! 제목: 내일의 날씨는 어떨까?, 날짜: 2025-08-08 12:41:16.083128+00:00&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;/ul&gt;
&lt;pre id=&quot;code_1754664777389&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# views.py (간략화된 예시)
@api_view([&quot;POST&quot;])
def question_create(request):
    if(request.method == &quot;POST&quot;):
        # 1. 클라이언트가 보낸 JSON 데이터를 파이썬 dict로 변환 (DRF가 자동으로 처리)
        request_data = request.data
        # 2. 'data' 인자로 데이터를 넘겨 Serializer를 초기화한다.
        serializer = QuestionSerializer(data=request_data)

        # 3. 유효성 검사! (가장 중요한 단계)
        if serializer.is_valid(raise_exception=True):
        # 4. 유효성 검사를 통과한 데이터로 객체를 저장한다.
            serializer.save() # 이 안에서 create() 또는 update()가 호출됨
            return Response(serializer.data, status=status.HTTP_201_CREATED)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;클라이언트 측&lt;/b&gt;에서 보낸 요청이 &lt;b&gt;request 매개변수&lt;/b&gt;에 할당됩니다. 그리고&lt;b&gt; request.data&lt;/b&gt;에 실제 &lt;b&gt;요청 본문(body)&lt;/b&gt;이 &lt;b&gt;파이썬 딕셔너리 형태&lt;/b&gt;로 담겨있습니다.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;&lt;b&gt;역직렬화&lt;/b&gt;를 하기 위해 &lt;b&gt;QuestionSerializer&lt;/b&gt; 클래스의 &lt;b&gt;data&lt;/b&gt; 인수로 &lt;b&gt;request.data&lt;/b&gt; 값을 넘겨서 &lt;b&gt;serializer&lt;/b&gt;를 &lt;b&gt;초기화(할당)&lt;/b&gt; 합니다.&amp;nbsp; &lt;b&gt;*여기서 request.data의 값은 QuestionSerializer의 필드 형식에 맞아야 합니다.*&amp;nbsp;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;is_valid()&lt;/b&gt;를 통해 유효성 검사를 진행합니다. request.data의 값이 실제 &lt;b&gt;models.py&lt;/b&gt;에 &lt;b&gt;Question 테이블&lt;/b&gt;의 필드의 조건(&lt;b&gt;데이터 타입, 필드 옵션, 제약 조건 등&lt;/b&gt;)을 충족하는지 검사합니다. 모든 검사를 통과하면, True를 반환하고, 검증 과정에서 &lt;b&gt;정제된 데이터&lt;/b&gt;를 &lt;b&gt;serializer.validated_data(파이썬 객체) 라는 속성&lt;/b&gt;에 저장합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유효성 검사를 통과한 데이터&lt;/b&gt;는 &lt;b&gt;serializer.save() 메서드&lt;/b&gt;를 통해, &lt;b&gt;새로 만들거나(create)&lt;/b&gt; &lt;b&gt;수정(update)&lt;/b&gt;합니다. 보통&amp;nbsp; Serializer 인수에 &lt;b&gt;data&lt;/b&gt;만 들어가는 경우는 &lt;b&gt;create&lt;/b&gt;되고, &lt;b&gt;instance&lt;/b&gt;와 &lt;b&gt;data&lt;/b&gt; 인수 값이 넘겨지는 경우가 &lt;b&gt;update&lt;/b&gt; 입니다.&lt;/li&gt;
&lt;li&gt;최종적으로 &lt;b&gt;클라이언트의 응답&lt;/b&gt;에 최종 결과물인 &lt;b&gt;serializer.data&lt;/b&gt;와 정상적으로 등록 처리되었다는 &lt;b&gt;응답 코드인 201번을 반환합니다.&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며..&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;직렬화(instance=...)&lt;/b&gt;는 &lt;b&gt;데이터베이스의 객체&lt;/b&gt;를 &lt;b&gt;외부(클라이언트)&lt;/b&gt;로 보여주기 위한 &quot;&lt;b&gt;출력&lt;/b&gt;&quot; 과정입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;역직렬화(data=..., instance=...)&lt;/b&gt;는 &lt;b&gt;클라이언트로부터&lt;/b&gt; 받은 &lt;b&gt;데이터를 검증&lt;/b&gt;하고 &lt;b&gt;데이터베이스에 저장&lt;/b&gt;하기 위한 &quot;&lt;b&gt;입력&lt;/b&gt;&quot; 과정입니다.&lt;/li&gt;
&lt;li&gt;is_valid() 메서드는 &lt;b&gt;데이터의 안정성을 보장&lt;/b&gt;하는 &lt;b&gt;방화벽&lt;/b&gt; 역할을 합니다.&lt;/li&gt;
&lt;li&gt;Serializer의&lt;b&gt; save()&lt;/b&gt;는 상황에 맞게 &lt;b&gt;create()나 update()를 호출하는 지휘자&lt;/b&gt; 역할을 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;</description>
      <author>shootingstar-1117</author>
      <guid isPermaLink="true">https://shootingstar-1117.tistory.com/1</guid>
      <comments>https://shootingstar-1117.tistory.com/1#entry1comment</comments>
      <pubDate>Sat, 9 Aug 2025 01:15:14 +0900</pubDate>
    </item>
  </channel>
</rss>