<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Heyjinu Dev</title>
    <link>https://norang2810.tistory.com/</link>
    <description>AI로 초안을 만들고, 여러 번 반복해서 진짜 이해한 내용만 남깁니다.  
백엔드 개발자가 되기 위한 공부 기록입니다.
</description>
    <language>ko</language>
    <pubDate>Tue, 2 Jun 2026 15:26:40 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Heyjinu_</managingEditor>
    <image>
      <title>Heyjinu Dev</title>
      <url>https://tistory1.daumcdn.net/tistory/6667387/attach/bfbb3941924c4fbd99549d4de915ff4f</url>
      <link>https://norang2810.tistory.com</link>
    </image>
    <item>
      <title>PHP/Laravel 학습 시작 - 프로젝트 생성부터 MySQL 연결까지</title>
      <link>https://norang2810.tistory.com/123</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 사용하는 기술 스택을 확인해 보니 PHP, Laravel, Vue.js 주로 사용하는것같더라... 기존에는 Java/Spring 기반으로 프로젝트를 진행해 왔지만, 실제 회사 환경에 빠르게 적응하려면 Laravel 기반의 백엔드 구조도 직접 만들어보는 것이 필요하다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Laravel 프로젝트를 처음 생성하고, 기본 SQLite 설정에서 MySQL로 변경한 뒤 DBeaver에서 테이블 생성까지 확인한 과정을 정리한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 개발 환경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습 환경은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;OS: Windows
PHP: 8.3.9
Laravel: 13.6.0
DB: MySQL
DB Tool: DBeaver 25.3.3
Terminal: Git Bash&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Git Bash에서 Laravel 프로젝트를 생성했고, 데이터베이스 확인은 DBeaver를 사용했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Composer 설치 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://getcomposer.org/download/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Composer 다운로드 페이지&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel 프로젝트를 생성하려면 Composer가 필요하다. 먼저 터미널에서 Composer가 정상적으로 설치되어 있는지 확인했다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;composer -V&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Composer 명령어가 잡히지 않아 Windows용 Composer Installer를 설치했다. 설치 후 새 터미널을 열고 다시 확인하니 정상적으로 Composer 명령어가 실행되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Laravel 프로젝트 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트는 바탕화면 경로에서 생성했다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;cd ~/Desktop
composer create-project laravel/laravel shuttle-admin-backend&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 생성이 완료되면 아래와 같은 Laravel 기본 구조가 만들어진다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;app/
bootstrap/
config/
database/
public/
resources/
routes/
storage/
vendor/
.env
artisan
composer.json&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Laravel이 편하다고 느꼈던 부분은 명령어 한 번으로 .env, 라우팅, 마이그레이션, 기본 디렉토리 구조가 한 번에 생성된다는 점이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에서 직접 설정하던 것들과 비교하면, Laravel은 초기에 필요한 골격을 빠르게 만들어주는 느낌이 강했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Laravel 서버 실행&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 폴더로 이동한 뒤 서버를 실행했다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;cd shuttle-admin-backend
php artisan serve&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적으로 실행되면 다음과 같은 메시지가 나온다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;INFO  Server running on [http://127.0.0.1:8000].&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 처음 실수한 부분이 있었다. 브라우저에 127.0.0.1만 입력했더니 접속이 되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel 개발 서버는 8000 포트에서 실행되고 있었기 때문에 정확히 아래 주소로 접속해야 했다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://127.0.0.1:8000&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 첫 번째 오류 - SQLite driver 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 접속했을 때 Laravel 화면이 바로 뜨지 않고 다음과 같은 오류가 발생했다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;could not find driver
Connection: sqlite
Database: database/database.sqlite&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 .env 파일의 기본 DB 설정이 SQLite로 되어 있었기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DB_CONNECTION=sqlite&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 현재 PHP 환경에는 SQLite 드라이버가 활성화되어 있지 않았기 때문에 DB 연결 과정에서 오류가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어차피 이후 MySQL을 사용할 예정이었기 때문에 SQLite 설정을 우회하지 않고 바로 MySQL 연결로 변경하기로 했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. DBeaver에서 MySQL 연결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 관리는 DBeaver를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DBeaver에서 새 연결을 만들고 MySQL을 선택한 뒤 다음과 같이 입력했다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;Server Host: localhost
Port: 3306
Username: root
Password: 본인 MySQL 비밀번호&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 33066 포트로 연결을 시도했지만 해당 포트에 MySQL 서버가 떠 있지 않아 연결이 거부되었다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;Connection refused: getsockopt
Communications link failure&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 일반적인 MySQL 기본 포트인 3306으로 변경해서 진행했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. shuttle_admin 데이터베이스 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel 프로젝트에서 사용할 데이터베이스를 생성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DBeaver SQL Editor에서 다음 쿼리를 실행했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CREATE DATABASE shuttle_admin
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성 후 shuttle_admin 데이터베이스가 정상적으로 보이는 것을 확인했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. Laravel .env MySQL 설정 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel 프로젝트의 .env 파일을 열어 DB 설정을 수정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 설정은 SQLite였다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DB_CONNECTION=sqlite&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 MySQL 설정으로 변경했다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=shuttle_admin
DB_USERNAME=root
DB_PASSWORD=본인비밀번호&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호가 root라면 아래처럼 작성하면 된다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DB_PASSWORD=root&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호가 없다면 비워두면 된다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DB_PASSWORD=&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 두 번째 오류 - pdo_mysql driver 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.env를 MySQL로 변경한 뒤 마이그레이션을 실행했다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;php artisan migrate&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 다음 오류가 발생했다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;could not find driver
Connection: mysql&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 MySQL 서버 문제가 아니라 PHP의 MySQL 드라이버 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DBeaver가 MySQL에 연결되는 것과 Laravel이 MySQL에 연결되는 것은 별개다. DBeaver는 자체 드라이버로 MySQL에 접속하지만, Laravel은 PHP의 PDO MySQL 드라이버를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, PHP에서 pdo_mysql 확장이 활성화되어 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PHP 설정 파일 위치를 확인했다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;php --ini&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 PHP 모듈 목록에서 MySQL 관련 확장을 확인했다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;php -m | grep -i mysql&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pdo_mysql이 보이지 않으면 php.ini 파일에서 아래 주석을 해제해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;;extension=pdo_mysql
;extension=mysqli&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래처럼 세미콜론을 제거한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;extension=pdo_mysql
extension=mysqli&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 후 터미널을 새로 열고 다시 마이그레이션을 실행했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 마이그레이션 성공&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 마이그레이션을 실행했다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;php artisan migrate&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;1462&quot; data-origin-height=&quot;557&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0Jzdc/dJMcagL4Ytq/dSPIKG4UnU2LTOlZKe9120/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0Jzdc/dJMcagL4Ytq/dSPIKG4UnU2LTOlZKe9120/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0Jzdc/dJMcagL4Ytq/dSPIKG4UnU2LTOlZKe9120/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0Jzdc%2FdJMcagL4Ytq%2FdSPIKG4UnU2LTOlZKe9120%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;1462&quot; height=&quot;557&quot; data-origin-width=&quot;1462&quot; data-origin-height=&quot;557&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel이 MySQL에 정상적으로 연결되었고, 기본 테이블들이 생성되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. DBeaver에서 테이블 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DBeaver에서 shuttle_admin 데이터베이스를 선택하고 다음 쿼리를 실행했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;show tables;&lt;/code&gt;&lt;/pre&gt;
&lt;p 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;1076&quot; data-origin-height=&quot;1441&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGlkKd/dJMcaiC5xhw/YVC4L7GQB6IBmXNi6U4fS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGlkKd/dJMcaiC5xhw/YVC4L7GQB6IBmXNi6U4fS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGlkKd/dJMcaiC5xhw/YVC4L7GQB6IBmXNi6U4fS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGlkKd%2FdJMcaiC5xhw%2FYVC4L7GQB6IBmXNi6U4fS0%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;529&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;1441&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel 기본 마이그레이션으로 생성된 테이블들이 정상적으로 확인되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. Laravel 기본 화면 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 다시 Laravel 서버를 실행했다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;php artisan serve&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 아래 주소로 접속했다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://127.0.0.1:8000&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2478&quot; data-origin-height=&quot;1271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UkKHM/dJMcafTVyno/IlikaGNCrB1Q4HD147NWfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UkKHM/dJMcafTVyno/IlikaGNCrB1Q4HD147NWfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UkKHM/dJMcafTVyno/IlikaGNCrB1Q4HD147NWfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUkKHM%2FdJMcafTVyno%2FIlikaGNCrB1Q4HD147NWfk%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;2478&quot; height=&quot;1271&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2478&quot; data-origin-height=&quot;1271&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 DB 오류 없이 Laravel 기본 화면이 정상적으로 출력되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습에서는 Laravel 프로젝트를 생성하고 MySQL과 연결하는 기본 흐름을 진행했다.&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;Composer 설치 확인&lt;/li&gt;
&lt;li&gt;Laravel 프로젝트 생성&lt;/li&gt;
&lt;li&gt;php artisan serve로 서버 실행&lt;/li&gt;
&lt;li&gt;기본 SQLite 설정 오류 확인&lt;/li&gt;
&lt;li&gt;MySQL 데이터베이스 생성&lt;/li&gt;
&lt;li&gt;.env를 MySQL 설정으로 변경&lt;/li&gt;
&lt;li&gt;pdo_mysql 드라이버 활성화&lt;/li&gt;
&lt;li&gt;php artisan migrate 실행&lt;/li&gt;
&lt;li&gt;DBeaver에서 테이블 생성 확인&lt;/li&gt;
&lt;li&gt;Laravel 기본 화면 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 과정에서 중요한 점은 단순히 Laravel 화면을 띄우는 것이 아니라, Laravel이 어떤 방식으로 .env 설정을 읽고 DB에 연결하는지 직접 확인했다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 DBeaver 연결 성공과 Laravel DB 연결 성공은 별개라는 점을 알게 되었다. DBeaver는 DB GUI 도구이고, Laravel은 PHP의 PDO 드라이버를 통해 MySQL에 연결하기 때문에 PHP 확장 설정도 반드시 확인해야 한다.&lt;/p&gt;</description>
      <category>PHP/Larabel</category>
      <category>dbeaver</category>
      <category>Laravel</category>
      <category>MySQL</category>
      <category>php</category>
      <category>입문</category>
      <author>Heyjinu_</author>
      <guid isPermaLink="true">https://norang2810.tistory.com/123</guid>
      <comments>https://norang2810.tistory.com/123#entry123comment</comments>
      <pubDate>Sat, 25 Apr 2026 16:18:08 +0900</pubDate>
    </item>
    <item>
      <title>[LG U+ 유레카 3기] Redis Queue + Scheduler로 선착순 쿠폰(100장) 동시성 해결 실습</title>
      <link>https://norang2810.tistory.com/122</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  Redis Queue + Scheduler로 선착순 쿠폰(100장) 동시성 해결 실습&lt;/h2&gt;
&lt;p style=&quot;background-color: orange; color: white; display: inline-block; padding: 4px 8px; border-radius: 6px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;❶ 상황 설명&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선착순 쿠폰 이벤트는 &amp;ldquo;동시에 많은 사용자가 몰리는&amp;rdquo; 대표적인 동시성 문제다.&lt;br /&gt;처음엔 JPA에서 quantity를 그냥 감소시키면, 여러 요청이 동시에 들어올 때 &lt;b&gt;갱신 유실(Lost Update)&lt;/b&gt; 때문에 재고가 깨지기 쉽다.&lt;br /&gt;이번 실습에서는 DB 락에만 의존하지 않고, &lt;b&gt;Redis Queue로 요청을 줄 세운 뒤&lt;/b&gt; &lt;b&gt;Scheduler가 순차적으로 발급&lt;/b&gt;하는 구조로 선착순 100장을 안정적으로 처리했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;background-color: orange; color: white; display: inline-block; padding: 4px 8px; border-radius: 6px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;❷ 핵심 아이디어 (한 문장 요약)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;신청(apply)은 Redis 큐에 넣기만 하고 빠르게 종료&lt;/b&gt; &amp;rarr; &lt;b&gt;발급(publish)은 스케줄러가 큐에서 꺼내 DB 재고를 감소&lt;/b&gt; &amp;rarr; &lt;b&gt;재고가 0이면 SoldOut 처리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Producer&lt;/b&gt;: apply() &amp;rarr; Redis List에 userId를 쌓는다 (대기열 생성)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Consumer&lt;/b&gt;: Scheduler &amp;rarr; userId를 pop해서 publish() 실행 (발급 로직)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;선착순 제한&lt;/b&gt;: publish()에서 quantity &amp;lt;= 0 이면 SoldOutException 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;background-color: orange; color: white; display: inline-block; padding: 4px 8px; border-radius: 6px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;❸ 전체 흐름(디버깅 관점으로 보기)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 1000명이 동시에 신청 요청&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Thread 1000개가 동시에 couponService.apply(userId)를 호출&lt;/li&gt;
&lt;li&gt;apply()는 DB를 건드리지 않고 Redis Queue에만 userId를 push&lt;/li&gt;
&lt;li&gt;즉, &amp;ldquo;신청 단계&amp;rdquo;는 빠르게 끝나고 서버는 버틴다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) Scheduler가 큐를 소비하며 발급&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fixedDelay=100ms 마다 스케줄러가 실행&lt;/li&gt;
&lt;li&gt;큐에서 userId를 하나씩 pop&lt;/li&gt;
&lt;li&gt;publish(userId)로 DB 재고(quantity)를 1 감소&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) 수량이 0이면 마감&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;quantity &amp;lt;= 0 이면 SoldOutException 발생&lt;/li&gt;
&lt;li&gt;Scheduler가 isSoldOut=true로 변경 후 더 이상 발급 로직을 태우지 않음&lt;/li&gt;
&lt;li&gt;결과적으로 최종 quantity는 0에서 멈춘다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;background-color: orange; color: white; display: inline-block; padding: 4px 8px; border-radius: 6px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;❹ 코드 (실습 전체)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ Entity: Coupon&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package com.mycom.myapp.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@NoArgsConstructor
public class Coupon {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    private String name;
    private int quantity; // 쿠폰 수량

    public Coupon(String name,int quantity) {
        this.name = name;
        this.quantity = quantity;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ Exception: SoldOutException&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;package com.mycom.myapp.exception;

// 쿠폰 소진을 의미하는 사용자 정의 예외
public class SoldOutException extends RuntimeException{

    private static final long serialVersionUID = 1L;

    public SoldOutException(String message) {
        super(message);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ Redis Repository: CouponRedisRepository (Queue)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package com.mycom.myapp.repository;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;
import lombok.RequiredArgsConstructor;

// Redis Queueing 작업 관련 처리
@Repository
@RequiredArgsConstructor
public class CouponRedisRepository {

    private final RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate;

    // queue에 추가 (Producer)
    public void addToQueue(Long userId) {
        redisTemplate.opsForList().rightPush(&quot;coupon_queue&quot;, String.valueOf(userId));
    }

    // queue에서 꺼내기 (Consumer)
    public Long popFromQueue() {
        String userId = redisTemplate.opsForList().leftPop(&quot;coupon_queue&quot;);
        return userId != null ? Long.valueOf(userId) : null;
    }

    // queue 크기
    public Long getSize() {
        return redisTemplate.opsForList().size(&quot;coupon_queue&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ JPA Repository: CouponRepository&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;package com.mycom.myapp.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import com.mycom.myapp.entity.Coupon;

public interface CouponRepository extends JpaRepository&amp;lt;Coupon, Long&amp;gt; {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ Service Interface&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package com.mycom.myapp.service;

// 신청(apply): 사용자를 줄 세움(Redis)
// 발급(publish): 쿠폰 수 감소(DB)
public interface CouponService {
    void apply(Long userId);
    void publish(Long userId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ Service 구현: CouponServiceImpl&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package com.mycom.myapp.service;

import org.springframework.stereotype.Service;

import com.mycom.myapp.entity.Coupon;
import com.mycom.myapp.exception.SoldOutException;
import com.mycom.myapp.repository.CouponRedisRepository;
import com.mycom.myapp.repository.CouponRepository;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class CouponServiceImpl implements CouponService {

    private final CouponRepository couponRepository;
    private final CouponRedisRepository couponRedisRepository;

    // Producer: 신청 &amp;rarr; Redis Queue에 넣고 끝
    @Override
    public void apply(Long userId) {
        couponRedisRepository.addToQueue(userId);
    }

    // Consumer: 발급 &amp;rarr; DB에서 수량 감소
    @Transactional
    @Override
    public void publish(Long userId) {
        Coupon coupon = couponRepository.findById(1L).orElseThrow();

        // 선착순 100개 제한
        if (coupon.getQuantity() &amp;lt;= 0) {
            throw new SoldOutException(coupon.getName() + &quot; 쿠폰이 모두 소진되었습니다.&quot;);
        }

        coupon.setQuantity(coupon.getQuantity() - 1);

        System.out.println(
            coupon.getName() + &quot; 발급 완료 : 사용자 : &quot; + userId +
            &quot; 잔여 수량 : &quot; + coupon.getQuantity()
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ Scheduler: CouponScheduler&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package com.mycom.myapp.scheduler;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import com.mycom.myapp.exception.SoldOutException;
import com.mycom.myapp.repository.CouponRedisRepository;
import com.mycom.myapp.service.CouponService;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class CouponScheduler {

    private final CouponRedisRepository couponRedisRepository;
    private final CouponService couponService;

    private boolean isSoldOut = false;

    @Scheduled(fixedDelay = 100) // 0.1초마다 호출
    public void couponEventScheduler() {
        while (true) {
            Long userId = couponRedisRepository.popFromQueue();

            if (userId == null) {
                // 처리할 요청이 없으면 종료 &amp;rarr; 다음 스케줄에서 재시도
                break;
            }

            if (isSoldOut) {
                // 마감 이후 요청에 대한 정책(로그/저장/알림 등)은 확장 포인트
                continue;
            }

            try {
                couponService.publish(userId);
            } catch (SoldOutException e) {
                isSoldOut = true;
                System.out.println(&quot;선착순 쿠폰이 마감되었습니다!&quot;);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ Main: 스케줄러 활성화&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;package com.mycom.myapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class SpringBootJpaRedisApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootJpaRedisApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;background-color: orange; color: white; display: inline-block; padding: 4px 8px; border-radius: 6px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;❺ 테스트 코드 (동시 요청 1000명 + 선착순 100개 검증)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트는 핵심이 2가지다.&lt;br /&gt;&lt;b&gt;1) apply()는 DB를 안 건드린다&lt;/b&gt; &amp;rarr; 따라서 발급 결과는 Scheduler가 소비한 뒤에 검증해야 한다.&lt;br /&gt;&lt;b&gt;2) 테스트마다 coupon_queue를 초기화해야 한다&lt;/b&gt; &amp;rarr; Redis는 상태를 남기므로 테스트가 오염된다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;package com.mycom.myapp;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;

import com.mycom.myapp.entity.Coupon;
import com.mycom.myapp.repository.CouponRepository;
import com.mycom.myapp.service.CouponService;

@SpringBootTest
public class CouponConcurrencyTest {

    @Autowired
    private CouponRepository couponRepository;

    @Autowired
    private CouponService couponService;

    @Autowired
    private RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate;

    @BeforeEach
    void setUp() {
        couponRepository.save(new Coupon(&quot;선착순 쿠폰&quot;, 100));
        redisTemplate.delete(&quot;coupon_queue&quot;);
    }

    @Test
    void queueingTest() throws Exception {
        int threadCount = 1000; // thread 1개 = user 1명
        ExecutorService executorService = Executors.newFixedThreadPool(32);
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i &amp;lt; threadCount; i++) {
            long userId = i; // 0~999
            executorService.submit(() -&amp;gt; {
                try {
                    couponService.apply(userId);
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await(); // 신청(큐 적재) 완료 대기

        // Scheduler가 큐를 소비할 시간 부여
        Thread.sleep(10000);

        int finalQuantity = couponRepository.findById(1L).orElseThrow().getQuantity();
        System.out.println(&quot;finalQuantity : &quot; + finalQuantity);

        assertEquals(0, finalQuantity);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ngVku/dJMcahwkplS/cBwIjie7lqLLgEViLDBbaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ngVku/dJMcahwkplS/cBwIjie7lqLLgEViLDBbaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ngVku/dJMcahwkplS/cBwIjie7lqLLgEViLDBbaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FngVku%2FdJMcahwkplS%2FcBwIjie7lqLLgEViLDBbaK%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;1091&quot; height=&quot;624&quot; data-origin-width=&quot;1091&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1395&quot; data-origin-height=&quot;577&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bp6OGW/dJMcahQDv7Z/PDsZ6pVskFCJxWuYKhER2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bp6OGW/dJMcahQDv7Z/PDsZ6pVskFCJxWuYKhER2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bp6OGW/dJMcahQDv7Z/PDsZ6pVskFCJxWuYKhER2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbp6OGW%2FdJMcahQDv7Z%2FPDsZ6pVskFCJxWuYKhER2K%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;1395&quot; height=&quot;577&quot; data-origin-width=&quot;1395&quot; data-origin-height=&quot;577&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;217&quot; data-origin-height=&quot;70&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A8c0n/dJMb99SByEr/EFzuN2fuKF2EWZTGN4HxPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A8c0n/dJMb99SByEr/EFzuN2fuKF2EWZTGN4HxPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A8c0n/dJMb99SByEr/EFzuN2fuKF2EWZTGN4HxPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA8c0n%2FdJMb99SByEr%2FEFzuN2fuKF2EWZTGN4HxPk%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;555&quot; height=&quot;179&quot; data-origin-width=&quot;217&quot; data-origin-height=&quot;70&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: orange; color: white; display: inline-block; padding: 4px 8px; border-radius: 6px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;❻ 내가 한 실수(중요) - Test 클래스에서 main 실행 시도&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;public SpringBootJpaRedisApplicationTests(String[] args) {
    SpringApplication.run(SpringBootJpaRedisApplication.class, args);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 &lt;b&gt;JUnit 테스트 클래스에서 main을 직접 실행하려는 코드&lt;/b&gt;라서 잘못된 방향이다.&lt;br /&gt;테스트 클래스는 Spring이 알아서 컨텍스트를 띄우도록 &lt;b&gt;@SpringBootTest&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;pre class=&quot;java&quot;&gt;&lt;code&gt;package com.mycom.myapp;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringBootJpaRedisApplicationTests {

    @Test
    void contextLoads() {
        // 컨텍스트 로딩만 확인
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;background-color: orange; color: white; display: inline-block; padding: 4px 8px; border-radius: 6px; font-weight: bold;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffa500; color: #ffffff; text-align: start;&quot;&gt;❼&lt;/span&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;Redis Queue&lt;/b&gt;: Redis List를 큐처럼 사용(RPUSH + LPOP)해서 대기열을 만든다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Producer/Consumer&lt;/b&gt;: 신청은 빠르게 큐에 적재, 발급은 소비자가 순차 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동시성 해결 전략&lt;/b&gt;: &amp;ldquo;DB에 동시에 접근&amp;rdquo; 자체를 줄여서 경쟁을 최소화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 포인트&lt;/b&gt;: apply는 DB 변화 없음 &amp;rarr; Scheduler 소비 후 quantity를 검증해야 함&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java/SpringBoot</category>
      <author>Heyjinu_</author>
      <guid isPermaLink="true">https://norang2810.tistory.com/122</guid>
      <comments>https://norang2810.tistory.com/122#entry122comment</comments>
      <pubDate>Tue, 6 Jan 2026 15:36:16 +0900</pubDate>
    </item>
    <item>
      <title>[LG U+ 유레카 3기]JPA 쿠폰 동시성 실습 &amp;ndash; 커넥션 풀 설정에 따라 결과가 달라지는 이유</title>
      <link>https://norang2810.tistory.com/121</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  JPA 쿠폰 동시성 실습 &amp;ndash; 커넥션 풀 설정에 따라 결과가 달라지는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습에서는 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;동일한 코드&lt;/span&gt;임에도 불구하고 &lt;b&gt;트랜잭션 유무&lt;/b&gt;, &lt;b&gt;HikariCP 커넥션 풀 크기&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;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선착순 쿠폰 100장&lt;/li&gt;
&lt;li&gt;1000개의 쓰레드가 동시에 발급 요청&lt;/li&gt;
&lt;li&gt;이론적으로 성공은 최대 100번만 발생해야 함&lt;/li&gt;
&lt;/ul&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;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot + Spring Data JPA&lt;/li&gt;
&lt;li&gt;Hibernate&lt;/li&gt;
&lt;li&gt;MySQL&lt;/li&gt;
&lt;li&gt;HikariCP (기본 커넥션 풀)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로, 동시성 충돌을 명확히 보기 위해 서비스 로직 내부에 &lt;code&gt;Thread.sleep(200)&lt;/code&gt;을 넣었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❸ 핵심 서비스 로직&lt;/h3&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;
@Override
@Transactional
public void issue(Long couponId) {

    Coupon coupon = couponRepository.findById(couponId)
        .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;존재하지 않는 쿠폰입니다.&quot;));

    coupon.setQuantity(coupon.getQuantity() - 1);

    try {
        Thread.sleep(200);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    couponRepository.saveAndFlush(coupon);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;821&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLZBKF/dJMcadglBGh/tve3wCebI6JdN1kPhPPfmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLZBKF/dJMcadglBGh/tve3wCebI6JdN1kPhPPfmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLZBKF/dJMcadglBGh/tve3wCebI6JdN1kPhPPfmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLZBKF%2FdJMcadglBGh%2Ftve3wCebI6JdN1kPhPPfmk%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;1514&quot; height=&quot;821&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;821&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❹ HikariCP 커넥션 풀 설정&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;
spring.datasource.hikari.maximum-pool-size=40
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 &lt;b&gt;동시에 DB에 접근 가능한 커넥션 수를 40개로 제한&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;542&quot; data-origin-height=&quot;52&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/opgjp/dJMcab3WAoZ/XSpL9tLwZMphkridFnt890/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/opgjp/dJMcab3WAoZ/XSpL9tLwZMphkridFnt890/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/opgjp/dJMcab3WAoZ/XSpL9tLwZMphkridFnt890/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fopgjp%2FdJMcab3WAoZ%2FXSpL9tLwZMphkridFnt890%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;542&quot; height=&quot;52&quot; data-origin-width=&quot;542&quot; data-origin-height=&quot;52&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❺ 실행 결과 로그&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;
Hibernate: update coupon set name=?, quantity=? where id=?
Hibernate: update coupon set name=?, quantity=? where id=?
Hibernate: update coupon set name=?, quantity=? where id=?
Hibernate: select c1_0.id, c1_0.name, c1_0.quantity from coupon c1_0 where c1_0.id=?

finalQuantity : 68
successCount : 1000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GBanc/dJMcacu1aLH/VNJNqwM9l60yGs76Cc0Gn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GBanc/dJMcacu1aLH/VNJNqwM9l60yGs76Cc0Gn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GBanc/dJMcacu1aLH/VNJNqwM9l60yGs76Cc0Gn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGBanc%2FdJMcacu1aLH%2FVNJNqwM9l60yGs76Cc0Gn1%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;1352&quot; height=&quot;305&quot; data-origin-width=&quot;1352&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❻ 결과 해석&lt;/h3&gt;
&lt;table border=&quot;1&quot; cellpadding=&quot;8&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;초기 수량&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;성공 요청 수&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최종 수량&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 결과는 매우 중요한 사실을 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt; 모든 요청이 성공했다고 판단했지만, 실제로는 재고 차감이 일부만 반영되었다. &lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❼ 왜 이런 현상이 발생했을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 &lt;b&gt;트랜잭션과 커넥션 풀의 조합&lt;/b&gt;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쓰레드는 1000개&lt;/li&gt;
&lt;li&gt;DB 커넥션은 최대 40개&lt;/li&gt;
&lt;li&gt;나머지 쓰레드는 커넥션 대기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, DB 접근이 자연스럽게 &lt;b&gt;부분 직렬화&lt;/b&gt;되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이것은 &lt;b&gt;락이 아니다.&lt;/b&gt;&lt;br /&gt;단지 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt; 물리적 병목&lt;/span&gt;이 생긴 것뿐이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❽ select 수와 update 수의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 보면 select 쿼리는 매우 많이 발생했지만, update 쿼리는 그보다 훨씬 적다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 여러 쓰레드가 &lt;b&gt;같은 수량 값을 읽고&lt;/b&gt;, 마지막에 flush되는 값만 반영되었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 &lt;b&gt;Lost Update(갱신 손실)&lt;/b&gt; 상황이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❾ @Transactional이 해결책일까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt; @Transactional은 원자성을 보장할 뿐, 동시성 자체를 제어하지는 않는다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 결과는 우연히 커넥션 풀 제한으로 충돌이 줄어든 것처럼 보였을 뿐이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;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;/li&gt;
&lt;li&gt;트랜잭션, 커넥션 풀, DB 환경이 모두 결과에 영향을 준다&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;낙관적 락 (@Version)&lt;/li&gt;
&lt;li&gt;비관적 락 (PESSIMISTIC_WRITE)&lt;/li&gt;
&lt;li&gt;Redis 분산 락&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⓫ 면접에서 이렇게 말할 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;동시성 테스트 결과는 코드뿐 아니라 커넥션 풀 설정에도 크게 영향을 받습니다. @Transactional은 트랜잭션 경계를 보장할 뿐, 선착순 쿠폰이나 재고 차감 문제는 락 전략을 반드시 적용해야 합니다.&amp;rdquo;&lt;/p&gt;</description>
      <author>Heyjinu_</author>
      <guid isPermaLink="true">https://norang2810.tistory.com/121</guid>
      <comments>https://norang2810.tistory.com/121#entry121comment</comments>
      <pubDate>Mon, 5 Jan 2026 16:30:18 +0900</pubDate>
    </item>
    <item>
      <title>[LG U+ 유레카 3기]JUnit &amp;amp; Spring 테스트  정리</title>
      <link>https://norang2810.tistory.com/120</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;JUnit &amp;amp; Spring 테스트 기본기 정리&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5sDWl/dJMcacBAQJf/smM5oepakYfFzVgk7F6csk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5sDWl/dJMcacBAQJf/smM5oepakYfFzVgk7F6csk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5sDWl/dJMcacBAQJf/smM5oepakYfFzVgk7F6csk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5sDWl%2FdJMcacBAQJf%2FsmM5oepakYfFzVgk7F6csk%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;505&quot; height=&quot;212&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 상황 설명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LG U+ 유레카 3기 과정에서 JUnit과 Spring 테스트(&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;@SpringBootTest&lt;/span&gt;, &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;@WebMvcTest&lt;/span&gt;, &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;MockMvc&lt;/span&gt;)를 이용해 테스트 주도 개발(TDD) 기초를 실습하는 시간이 있었지만, 수업을 못 들은 상태에서 소스 코드만 전달받은 상황을 가정하고 정리한 글이다.&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;JUnit 5의 기본 어노테이션과 라이프사이클 이해&lt;/li&gt;
&lt;li&gt;Assertion(단언) 메서드들의 역할과 사용법 정리&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;@SpringBootTest&lt;/span&gt; vs &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;@WebMvcTest&lt;/span&gt; 차이 이해&lt;/li&gt;
&lt;li&gt;MockMvc를 이용한 Controller 테스트 흐름 이해&lt;/li&gt;
&lt;li&gt;STS4(Eclipse)에서 JUnit 테스트 실행 방법 정리&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. JUnit &amp;amp; TDD 기본 개념&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1. JUnit이란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JUnit은 자바에서 가장 널리 사용하는 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;단위 테스트 프레임워크&lt;/span&gt; 이다. 테스트 메소드에 &lt;code&gt;@Test&lt;/code&gt;를 붙이고 실행하면, 실제 코드가 자동으로 실행되고 결과를 검증해 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드는 &amp;ldquo;실행 버튼만 누르면 스스로 돌아가는 검증 시나리오&amp;rdquo; 라고 생각하면 된다. 버튼 한 번으로 매번 브라우저 켜서 클릭하던 반복 테스트를 모두 자동화할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-2. TDD(Test-Driven Development)와 JUnit&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TDD는 다음의 짧은 사이클을 계속 반복하는 개발 방식이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Red&lt;/span&gt; : 먼저 실패하는 테스트 작성&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Green&lt;/span&gt; : 테스트를 통과시키기 위한 최소한의 코드 작성&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Refactor&lt;/span&gt; : 테스트를 통과하는 상태에서 코드 정리, 구조 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 &amp;ldquo;테스트&amp;rdquo;를 작성하는 도구가 바로 JUnit이다. 즉, &lt;b&gt;요구사항을 테스트 코드로 먼저 표현하고 &amp;rarr; 그 테스트를 만족시키는 실제 코드를 나중에 작성&lt;/b&gt; 하는 흐름이 TDD다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. JUnit 5 기본 어노테이션 정리&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-1. 테스트 메소드와 실행 순서&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TestBasic {

    @Test
    @Order(2)
    void test1() {
        System.out.println(&quot;test1()&quot;);
    }

    @Test
    @Order(1)
    void test2() {
        System.out.println(&quot;test2()&quot;);
    }

    @Test
    @DisplayName(&quot;회원 등록 테스트&quot;)
    @Order(4)
    void test3() {
        System.out.println(&quot;test3()&quot;);
    }

    @Test
    @DisplayName(&quot;회원 수정 테스트&quot;)
    @Order(3)
    void test4() {
        String s = null;
        s.length(); // NullPointerException 발생
        System.out.println(&quot;test4()&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Test&lt;/code&gt; : 이 메소드는 JUnit 테스트 메소드라는 의미. 리턴 타입은 &lt;code&gt;void&lt;/code&gt;, 파라미터 없음.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@TestMethodOrder(MethodOrderer.OrderAnnotation.class)&lt;/code&gt; : &lt;code&gt;@Order&lt;/code&gt; 애노테이션을 사용해 테스트 실행 순서를 지정.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Order(n)&lt;/code&gt; : 숫자가 작을수록 먼저 실행. 기본 JUnit은 순서를 보장하지 않기 때문에, 시나리오 순서가 중요할 때만 사용.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@DisplayName(&quot;설명&quot;)&lt;/code&gt; : JUnit 뷰나 리포트에서 보이는 이름을 사람이 읽기 좋게 변경.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 순서를 정리하면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1순위: &lt;code&gt;test2()&lt;/code&gt; (Order 1)&lt;/li&gt;
&lt;li&gt;2순위: &lt;code&gt;test1()&lt;/code&gt; (Order 2)&lt;/li&gt;
&lt;li&gt;3순위: &lt;code&gt;test4()&lt;/code&gt; (Order 3)&lt;/li&gt;
&lt;li&gt;4순위: &lt;code&gt;test3()&lt;/code&gt; (Order 4)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-2. 테스트 라이프사이클 &amp;ndash; Before/After&lt;/h4&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@BeforeAll
static void beforeAll() {
    System.out.println(&quot;beforeAll()&quot;);
}

@AfterAll
static void afterAll() {
    System.out.println(&quot;afterAll()&quot;);
}

@BeforeEach
void beforeEach() {
    System.out.println(&quot;beforeEach()&quot;);
}

@AfterEach
void afterEach() {
    System.out.println(&quot;afterEach()&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@BeforeAll&lt;/code&gt; : 테스트 클래스 전체에서 &lt;b&gt;딱 한 번&lt;/b&gt;, 모든 테스트 전에 실행. ( static 메소드 )&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@AfterAll&lt;/code&gt; : 모든 테스트가 끝난 뒤 &lt;b&gt;딱 한 번&lt;/b&gt; 실행. 리소스 정리 등.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@BeforeEach&lt;/code&gt; : 각 테스트 메소드 실행 직전에 매번 실행.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@AfterEach&lt;/code&gt; : 각 테스트 메소드 실행 직후에 매번 실행 (테스트가 실패해도 실행된다는 점이 중요).&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 DB를 사용하는 테스트라면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@BeforeAll&lt;/code&gt; : 테스트용 스키마 생성, 테스트 DB 연결&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@BeforeEach&lt;/code&gt; : 테스트 데이터 초기화&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@AfterEach&lt;/code&gt; : 트랜잭션 롤백, 임시 데이터 삭제&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@AfterAll&lt;/code&gt; : 테스트용 스키마 제거, 커넥션 종료&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. JUnit Assertions(단언) 정리&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-1. 예외 발생 테스트 &amp;ndash; &lt;code&gt;assertThrows&lt;/code&gt;&lt;/h4&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;int getStringLength(String str) { 
    return str.length(); 
}

@Test
void testException() {
    String str = null;

    assertThrows(NullPointerException.class,
                 () -&amp;gt; getStringLength(str),
                 &quot;NullPointerException throws&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;assertThrows&lt;/span&gt; 는 &amp;ldquo;이 코드가 실행될 때 특정 예외가 반드시 발생해야 한다&amp;rdquo; 는 것을 테스트한다. 입력이 잘못됐을 때 커스텀 예외를 던지는 서비스 로직을 검증할 때 매우 유용하다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-2. 묶음 테스트 &amp;ndash; &lt;code&gt;assertAll&lt;/code&gt;&lt;/h4&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;int result = 0;
int m1() { return 4; }
boolean m2() { return true; }
String m3() { return &quot;hello&quot;; }

@Test
void testGroup() {
    assertAll(&quot;묶음 테스트&quot;,
        () -&amp;gt; assertEquals(4, m1()),
        () -&amp;gt; assertTrue(m2()),
        () -&amp;gt; assertNotNull(m3()),
        () -&amp;gt; assertEquals(0, result)
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 관련된 검증을 한 번에 묶어서 실행할 수 있다. 중간에 하나가 실패해도 나머지 검증을 모두 수행한 뒤 어떤 것들이 실패했는지 함께 보여준다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-3. 배열/컬렉션 비교&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int[] expectedArray = {1, 2, 3};
int[] actualArray   = {1, 2, 4};

@Test
void testArrayEquals() {
    assertArrayEquals(expectedArray, actualArray, &quot;두 정수 배열이 같다.&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; expectedList = List.of(&quot;abc&quot;, &quot;def&quot;);
List&amp;lt;String&amp;gt; actualList   = List.of(&quot;abc&quot;, &quot;xyz&quot;);

@Test
void testIterableEquals() {
    assertIterableEquals(expectedList, actualList, &quot;두 문자열 컬렉션이 같다.&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열/컬렉션의 길이와 각 항목을 모두 비교해 주기 때문에, 알고리즘 문제나 JPA 조회 결과를 검증할 때 자주 사용한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-4. 동일성 vs 동등성 &amp;ndash; &lt;code&gt;assertSame&lt;/code&gt; / &lt;code&gt;assertEquals&lt;/code&gt;&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
void testSame() {
    String str1 = &quot;hello&quot;;
    String str2 = new String(&quot;hello&quot;);
    assertSame(str1, str2, &quot;두 객체는 같다&quot;); // == 비교 &amp;rarr; 실패
}

@Test
void testEquals() {
    String str1 = &quot;hello&quot;;
    String str2 = new String(&quot;hello&quot;);
    assertEquals(str1, str2, &quot;두 객체는 같다&quot;); // equals() 비교 &amp;rarr; 성공
}
&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;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;assertSame&lt;/span&gt; : &lt;code&gt;a == b&lt;/code&gt; (동일한 객체인지) 비교&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;assertEquals&lt;/span&gt; : &lt;code&gt;a.equals(b)&lt;/code&gt; (값이 같은지) 비교&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-5. 수행 시간 테스트 &amp;ndash; &lt;code&gt;assertTimeout&lt;/code&gt;&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;void businessLogic() {
    try {
        Thread.sleep(3000); // 3초 대기
    } catch (Exception e) {
        e.printStackTrace();
    }
}

@Test
void testTimeout() {
    assertTimeout(Duration.ofSeconds(1),
                  () -&amp;gt; businessLogic(),
                  &quot;1초 미만 수행 테스트&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지정한 시간 안에 코드가 끝나지 않으면 테스트를 실패시키는 방식이다. 간단한 성능 기준(예: 1초 이내 처리)을 검증할 때 사용할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. SpringBootTest &amp;ndash; 애플리케이션 전체 통합 테스트&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5-1. 코드 구조&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Slf4j
public class DITest {

    @Autowired
    UserController userController;

    @Autowired
    UserService userService;

    @Autowired
    UserRepository userRepository;

    @Autowired
    HttpSession session;

    @Autowired
    HttpServletRequest request;

    @Test
    @Order(1)
    void testDI() {
        log.info(&quot;testDI() 시작&quot;);
        assertNotNull(userController);
        assertNotNull(userService);
        assertNotNull(userRepository);
        log.info(&quot;testDI() 종료&quot;);
    }

    @Test
    @Order(2)
    void testDIAll() {
        log.debug(&quot;testDIAll() 시작&quot;);
        assertAll(&quot;DI 묶음 테스트&quot;,
            () -&amp;gt; assertNotNull(userController),
            () -&amp;gt; assertNotNull(userService),
            () -&amp;gt; assertNotNull(userRepository)
        );
        log.debug(&quot;testDIAll() 종료&quot;);
    }

    @Test
    @Order(3)
    void testDISessionRequest() {
        log.info(&quot;testDISessionRequest() 시작&quot;);
        assertAll(&quot;DI 묶음 테스트&quot;,
            () -&amp;gt; assertNotNull(session),
            () -&amp;gt; assertNotNull(request)
        );
        log.info(&quot;testDISessionRequest() 종료&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5-2. @SpringBootTest의 의미&lt;/h4&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;내장 톰캣, Controller, Service, Repository, DataSource 등 &lt;b&gt;모든 Bean&lt;/b&gt;이 실제 서비스처럼 동작.&lt;/li&gt;
&lt;li&gt;실행 속도는 느리지만, &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;통합 테스트&lt;/span&gt; 용도로 매우 유용하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5-3. 이 테스트가 검증하는 것&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;UserController&lt;/code&gt;, &lt;code&gt;UserService&lt;/code&gt;, &lt;code&gt;UserRepository&lt;/code&gt;가 정상적으로 Bean 등록되어 있는지 (DI 성공 여부).&lt;/li&gt;
&lt;li&gt;웹 환경이 제대로 올라와서 &lt;code&gt;HttpServletRequest&lt;/code&gt;, &lt;code&gt;HttpSession&lt;/code&gt; 같은 객체도 주입 가능한지.&lt;/li&gt;
&lt;li&gt;전체 애플리케이션 설정에 치명적인 문제가 없는지.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 이 위에 실제 API 호출 테스트(예: &lt;code&gt;TestRestTemplate&lt;/code&gt; 사용)까지 결합하여 &amp;ldquo;회원가입 &amp;rarr; 로그인 &amp;rarr; 권한 확인&amp;rdquo;와 같은 흐름을 하나의 통합 시나리오로 테스트하기도 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. WebMvcTest + MockMvc &amp;ndash; Controller 레이어 슬라이스 테스트&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6-1. TestController + WebMvcTest&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@RestController
@Slf4j
public class TestController {

    @GetMapping(&quot;/hello&quot;)
    public void m1() {
        log.info(&quot;/hello&quot;);
    }

    @GetMapping(&quot;/param1&quot;)
    public void m2(@RequestParam(&quot;id&quot;) Integer id,
                   @RequestParam(&quot;name&quot;) String name) {
        log.info(&quot;/param1&quot;);
        log.info(&quot;id : &quot; + id + &quot; name : &quot; + name);
    }

    @PostMapping(&quot;/param2&quot;)
    public void m3(TestDto testDto) {
        log.info(&quot;/param2&quot;);
        log.info(testDto.toString());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@WebMvcTest(TestController.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TestControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    @Order(1)
    void testHello() throws Exception {
        this.mockMvc
            .perform(get(&quot;/hello&quot;))
            .andExpect(status().isOk());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6-2. @WebMvcTest의 특징&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Web/MVC 레이어만 로딩하는 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;슬라이스 테스트&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;지정한 Controller와 관련된 MVC 구성 요소만 올리기 때문에, 속도가 빠르다.&lt;/li&gt;
&lt;li&gt;Service/Repository Bean은 자동으로 올리지 않으므로, 필요하면 &lt;code&gt;@MockBean&lt;/code&gt;으로 가짜를 주입한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6-3. MockMvc로 HTTP 요청 흉내 내기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MockMvc&lt;/code&gt;는 &amp;ldquo;서버를 실제로 띄우지 않고도 Controller에 HTTP 요청을 보내는 도구&amp;rdquo;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;perform(get(&quot;/hello&quot;))&lt;/code&gt; : GET /hello 요청을 가짜로 발생.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;andExpect(status().isOk())&lt;/code&gt; : HTTP 응답 코드가 200 OK인지 검증.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저나 Postman으로 일일이 테스트하던 작업을 JUnit 코드로 자동화하는 것이 바로 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;WebMvcTest + MockMvc&lt;/span&gt; 패턴이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6-4. @SpringBootTest vs @WebMvcTest 비교&lt;/h4&gt;
&lt;table border=&quot;1&quot; cellspacing=&quot;0&quot; cellpadding=&quot;8&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;@SpringBootTest&lt;/th&gt;
&lt;th&gt;@WebMvcTest&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;로드 범위&lt;/td&gt;
&lt;td&gt;애플리케이션 전체 (Controller, Service, Repository, DB 등)&lt;/td&gt;
&lt;td&gt;Web/MVC 레이어만 (Controller 중심)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;속도&lt;/td&gt;
&lt;td&gt;느림&lt;/td&gt;
&lt;td&gt;빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;용도&lt;/td&gt;
&lt;td&gt;통합 테스트, 실제 환경에 가깝게 전체 검증&lt;/td&gt;
&lt;td&gt;Controller URL, 바인딩, 응답 코드 등 웹 레이어 집중 테스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;의존성&lt;/td&gt;
&lt;td&gt;실제 Bean 사용&lt;/td&gt;
&lt;td&gt;Service/Repository는 보통 &lt;code&gt;@MockBean&lt;/code&gt;으로 가짜 주입&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. STS4(Eclipse)에서 JUnit 실행 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;7-1. 단일 테스트 클래스 실행&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;src/test/java&lt;/code&gt;에서 원하는 테스트 클래스 파일을 연다.&lt;/li&gt;
&lt;li&gt;에디터에서 파일 우클릭 &amp;rarr; &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Run As &amp;gt; JUnit Test&lt;/span&gt; 선택.&lt;/li&gt;
&lt;li&gt;하단의 JUnit 뷰에서 초록/빨강 바와 각 테스트 결과를 확인한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;7-2. 메소드 하나만 실행&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;실행하고 싶은 &lt;code&gt;@Test&lt;/code&gt; 메소드 안에 커서를 둔다.&lt;/li&gt;
&lt;li&gt;우클릭 &amp;rarr; Run As &amp;rarr; JUnit Test.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;7-3. 프로젝트 전체 테스트 실행&lt;/h4&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;Run As &amp;rarr; JUnit Test 또는 Maven Test(빌드 과정 포함) 선택.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 실행한 뒤에는 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Ctrl + F11&lt;/span&gt; (마지막 실행 구성 재실행) 로 계속 반복 실행할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 개념 정리 &amp;amp; 교훈&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8-1. 핵심 개념 요약&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;JUnit 5&lt;/span&gt; : 자바 표준 단위 테스트 프레임워크. &lt;code&gt;@Test&lt;/code&gt;, &lt;code&gt;@BeforeEach&lt;/code&gt;, &lt;code&gt;assertEquals&lt;/code&gt; 등을 제공.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;단위 테스트&lt;/span&gt; : 메소드/클래스 단위의 작은 기능을 빠르게 반복 검증.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;통합 테스트&lt;/span&gt; : 여러 레이어(웹, 서비스, DB)를 함께 묶어 실제에 가깝게 검증.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;@SpringBootTest&lt;/span&gt; : 애플리케이션 전체를 로딩하는 무거운 통합 테스트.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;@WebMvcTest + MockMvc&lt;/span&gt; : Controller만 빠르게 테스트하는 웹 슬라이스 테스트.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;assertThrows / assertAll / assertTimeout&lt;/span&gt; : 예외, 묶음, 시간 검증 등 다양한 Assertion 기능.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8-2. 이번 실습에서 얻은 교훈&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;브라우저 열고 클릭해서 눈으로 확인하던 테스트&amp;rdquo;를 모두 JUnit 코드로 옮겨오면, 언제든지 반복 실행할 수 있는 &lt;b&gt;자동 회귀 테스트&lt;/b&gt;가 된다.&lt;/li&gt;
&lt;li&gt;테스트가 많을수록 리팩터링이 두렵지 않고, 시스템 규모가 커져도 기존 기능이 깨졌는지 빠르게 확인할 수 있다.&lt;/li&gt;
&lt;li&gt;실제 현업에서는 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;단위 테스트&lt;/span&gt;를 가장 많이, &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;WebMvcTest&lt;/span&gt; 같은 슬라이스 테스트를 적당히, &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;SpringBootTest 통합 테스트&lt;/span&gt;는 소수만 두는 &amp;ldquo;테스트 피라미드&amp;rdquo; 구조를 많이 사용한다.&lt;/li&gt;
&lt;li&gt;유레카 과정에서 미리 JUnit과 테스트 문화를 익혀두면, 이후 프로젝트나 취업 후에도 코드 품질과 생산성을 동시에 챙길 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글은 JUnit과 Spring 테스트 기본기를 한 번에 정리한 실습 복습용 노트이다. 실제 프로젝트 코드에 &lt;code&gt;@SpringBootTest&lt;/code&gt;, &lt;code&gt;@WebMvcTest&lt;/code&gt;, &lt;code&gt;MockMvc&lt;/code&gt;를 직접 붙여보고, 테스트를 돌려 보면서 감각을 익히는 것이 가장 좋은 공부 방법이다.&lt;/p&gt;</description>
      <author>Heyjinu_</author>
      <guid isPermaLink="true">https://norang2810.tistory.com/120</guid>
      <comments>https://norang2810.tistory.com/120#entry120comment</comments>
      <pubDate>Wed, 3 Dec 2025 16:25:49 +0900</pubDate>
    </item>
    <item>
      <title>[LG U+ 유레카 3기]Spring Boot REST API + Swagger + Postman 연동 실습</title>
      <link>https://norang2810.tistory.com/119</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Spring Boot로 만든 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;학생 관리 REST API&lt;/span&gt;에 Swagger(OpenAPI 3.0)와 Postman을 붙여서, &lt;b&gt;&amp;ldquo;백엔드 API &amp;rarr; 문서 &amp;rarr; 테스트 도구&amp;rdquo;&lt;/b&gt;까지 한 번에 연결하는 실습을 했다.&lt;br /&gt;단순히 코드만 작성하는 수준이 아니라, &lt;b&gt;HTTP 응답 설계(ResponseEntity), API 문서화(@Tag, @Operation), OpenAPI JSON, Postman 연동&lt;/b&gt;까지 현업에서 실제로 사용하는 흐름과 거의 동일한 구조였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❶ REST API 기반 Student CRUD 구조 복습&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 레이어드 아키텍처 구조&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습의 기본 골격은 이미 만들어 둔 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Student CRUD&lt;/span&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;Controller&lt;/b&gt; : HTTP 요청(URI, 메서드) &amp;rarr; Java 메서드 매핑, DTO 입출력 담당&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Service&lt;/b&gt; : 비즈니스 로직, Entity &amp;harr; DTO 변환, 예외 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Repository&lt;/b&gt; : Spring Data JPA, DB CRUD 담당&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Entity&lt;/b&gt; : DB 테이블과 매핑되는 도메인 객체&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DTO&lt;/b&gt; : 클라이언트와 주고받는 데이터 모델&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;
// Entity
@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String name;
    private String email;
    private String phone;
}

// DTO
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class StudentDto {
    private int id;
    private String name;
    private String email;
    private String phone;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service 계층에서는 &lt;b&gt;Entity &amp;harr; DTO&lt;/b&gt; 변환을 직접 해 주어 JPA 내부 구현이 외부(API 응답)에 새어 나가지 않도록 막는다. 이는 실제 서비스에서도 많이 쓰는 패턴이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❷ ResponseEntity로 HTTP 응답 제어하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 컨트롤러는 단순히 &lt;code&gt;StudentResultDto&lt;/code&gt;만 리턴해서, 스프링이 항상 &lt;code&gt;200 OK&lt;/code&gt;로 응답하도록 두었다. 오늘은 여기서 한 단계 더 나가서 &lt;b&gt;HTTP 응답 코드와 바디를 직접 제어하는 방법&lt;/b&gt;을 실습했다. 그 핵심이 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;ResponseEntity&amp;lt;T&amp;gt;&lt;/span&gt; 이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 기본 사용 예시&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;
@RestController
@RequestMapping(&quot;/api&quot;)
@RequiredArgsConstructor
public class StudentControllerCrudResponseEntity {

    private final StudentServiceCrud studentServiceCrud;

    // 목록
    @GetMapping(&quot;/students&quot;)
    public ResponseEntity&amp;lt;StudentResultDto&amp;gt; listStudent() {

        StudentResultDto resultDto = studentServiceCrud.listStudent();

        // 성공
        return new ResponseEntity&amp;lt;&amp;gt;(resultDto, HttpStatus.OK);

        // 혹은 축약형
        // return ResponseEntity.ok(resultDto);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 다른 HTTP 상태 코드 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 성공(200) 뿐 아니라, 상황에 따라 다른 상태 코드를 줄 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;
// 200 OK - 바디 포함
return ResponseEntity
        .status(HttpStatus.OK)
        .body(studentResultDto);

// 404 Not Found - 바디 없음
return ResponseEntity
        .status(HttpStatus.NOT_FOUND)
        .build();

// 축약형 404
return ResponseEntity.notFound().build();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 &lt;code&gt;StudentResultDto&lt;/code&gt; 안에 &lt;code&gt;result=&quot;success/fail&quot;&lt;/code&gt; 같은 문자열만 두는 것보다, HTTP가 원래 가진 표현력(200, 404, 500 등)을 활용할 수 있어서 &lt;b&gt;REST 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;❸ Swagger(springdoc-openapi)로 REST API 문서화&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Gradle 의존성 추가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger 2가 아니라, &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;springdoc-openapi&lt;/span&gt; 기반으로 OpenAPI 3.0을 사용했다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;
dependencies {
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 재시작 후 아래 주소로 접속하면 Swagger UI가 자동으로 뜬다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Swagger UI : &lt;code&gt;http://localhost:8080/swagger-ui/index.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;OpenAPI JSON : &lt;code&gt;http://localhost:8080/v3/api-docs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;OpenAPI YAML : &lt;code&gt;http://localhost:8080/v3/api-docs.yaml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) @Tag &amp;ndash; 컨트롤러(그룹) 단위 설명&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger UI에서 API를 그룹으로 묶기 위해 &lt;code&gt;@Tag&lt;/code&gt;를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;
import io.swagger.v3.oas.annotations.tags.Tag;

@Tag(
    name = &quot;기본 Student CRUD REST API&quot;,
    description = &quot;Student의 등록, 수정, 삭제, 목록 조회, 상세 조회 기능을 REST API 로 제공합니다.&quot;
)
@RestController
@RequestMapping(&quot;/api&quot;)
@RequiredArgsConstructor
public class StudentControllerCrud {
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 태그 정보가 Swagger UI 왼쪽의 섹션 이름으로 표시된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) @Operation &amp;ndash; 메서드 단위 설명&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 API 엔드포인트(메서드)에는 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;@Operation&lt;/span&gt;을 붙여 요약(summary)과 상세 설명(description)을 작성했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;
import io.swagger.v3.oas.annotations.Operation;

@Operation(
    summary = &quot;학생 목록 조회&quot;,
    description = &quot;전체 학생 목록을 조회합니다.&quot;
)
@GetMapping(&quot;/students&quot;)
public StudentResultDto listStudent() {
    return studentServiceCrud.listStudent();
}

@Operation(
    summary = &quot;학생 상세 조회&quot;,
    description = &quot;ID 값을 이용해 개별 학생 1명의 상세 정보를 조회합니다.&quot;,
    deprecated = true   // Swagger UI 에 해당 API가 deprecated 표시
)
@GetMapping(&quot;/students/{id}&quot;)
public StudentResultDto detailStudent(@PathVariable(&quot;id&quot;) Integer id) {
    return studentServiceCrud.detailStudent(id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;deprecated = true&lt;/code&gt; 로 표시하면 Swagger UI에서 이 API가 &amp;ldquo;더 이상 권장되지 않는 API&amp;rdquo;라는 표시가 뜬다. 실무에서는 구버전 API를 남겨 두되, 새 API로 갈아타게 안내할 때 사용한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❹ Swagger 문서를 인터페이스로 분리하는 패턴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 배운 것 중에 특히 인상 깊었던 부분은, &lt;b&gt;Swagger 어노테이션을 인터페이스에 모으고&lt;/b&gt;, 컨트롤러 클래스는 그 인터페이스를 구현만 하는 구조였다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Swagger 전용 인터페이스&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;
@Tag(
    name = &quot;JSON Student CRUD REST API&quot;,
    description = &quot;JSON 요청을 통해 Student의 등록, 수정, 삭제, 조회를 수행하는 REST API&quot;
)
public interface StudentControllerCrudJsonRequestSwagger {

    @Operation(
        summary = &quot;학생 목록&quot;,
        description = &quot;전체 학생 목록을 조회합니다.&quot;
    )
    @GetMapping(&quot;/students&quot;)
    StudentResultDto listStudent();

    @Operation(
        summary = &quot;학생 상세&quot;,
        description = &quot;개별 학생 1명을 조회합니다.&quot;
    )
    @GetMapping(&quot;/students/{id}&quot;)
    StudentResultDto detailStudent(@PathVariable(&quot;id&quot;) Integer id);

    @Operation(
        summary = &quot;학생 등록&quot;,
        description = &quot;JSON 요청을 통해 신규 학생 1명을 등록합니다.&quot;
    )
    @PostMapping(&quot;/students&quot;)
    StudentResultDto insertStudent(StudentDto studentDto);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 실제 컨트롤러 구현&lt;/h4&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;
@RestController
@RequiredArgsConstructor
public class StudentControllerCrudJsonRequest
        implements StudentControllerCrudJsonRequestSwagger {

    private final StudentServiceCrud studentServiceCrud;

    @Override
    public StudentResultDto listStudent() {
        return studentServiceCrud.listStudent();
    }

    @Override
    public StudentResultDto detailStudent(Integer id) {
        return studentServiceCrud.detailStudent(id);
    }

    @Override
    public StudentResultDto insertStudent(StudentDto studentDto) {
        return studentServiceCrud.insertStudent(studentDto);
    }
}
&lt;/code&gt;&lt;/pre&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;b&gt;Swagger 어노테이션으로 지저분해지지 않는다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;인터페이스가 API 스펙 역할을 하기 때문에, 구현체가 반드시 이 스펙을 따르도록 강제할 수 있다.&lt;/li&gt;
&lt;li&gt;버전이 여러 개(v1, v2...)일 때 인터페이스를 나눠서 관리하기 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서도 &amp;ldquo;API 스펙 인터페이스 + 구현 컨트롤러&amp;rdquo; 패턴을 쓰는 팀이 꽤 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❺ /v3/api-docs JSON 확인 + JSON 포매터 + Postman Import&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) /v3/api-docs JSON 확인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swagger UI는 사실 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;/v3/api-docs&lt;/span&gt; 에서 제공되는 OpenAPI JSON 문서를 렌더링한 것일 뿐이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저에서 &lt;code&gt;http://localhost:8080/v3/api-docs&lt;/code&gt; 접속&lt;/li&gt;
&lt;li&gt;엄청 긴 JSON 텍스트가 나온다 &amp;rarr; 이것이 &lt;b&gt;API 스펙의 원본&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;내용 복사 후, JSON 포매터 사이트에 붙여서 구조를 보기 좋게 정리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON 포매터를 사용하면 &lt;code&gt;paths&lt;/code&gt;, &lt;code&gt;components.schemas&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt; 구조를 한눈에 볼 수 있어서 &amp;ldquo;내 API가 OpenAPI 기준으로 어떻게 정의되어 있는지&amp;rdquo;를 정확히 확인할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) Postman에 OpenAPI 스펙 Import&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로, 이 OpenAPI JSON을 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Postman&lt;/span&gt;에 가져와서 API 테스트 컬렉션으로 만드는 작업을 실습했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Postman 실행 후 상단의 &lt;b&gt;Import&lt;/b&gt; 버튼 클릭&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Raw text&lt;/b&gt; 탭 선택&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/v3/api-docs&lt;/code&gt; 에서 복사한 JSON 전체를 붙여넣기&lt;/li&gt;
&lt;li&gt;Postman이 포맷을 자동으로 &lt;b&gt;OpenAPI 3.0&lt;/b&gt; 으로 인식&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Import as: API Collection&lt;/b&gt; 으로 표시되는 것 확인 후 &lt;b&gt;Import&lt;/b&gt; 클릭&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 Swagger에서 정의한 모든 엔드포인트가 Postman Collection으로 자동 생성된다. 각 요청에는 이미 HTTP 메서드, URL, PathVariable 구조, RequestBody 형식이 들어있어서, 별도로 등록할 필요 없이 바로 &lt;b&gt;Send 버튼만 눌러 테스트&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 이 과정을 통해 &amp;ldquo;백엔드 개발자가 만든 Swagger 스펙 &amp;rarr; 프론트/QA 팀이 Postman으로 가져와 테스트&amp;rdquo; 라는 협업 흐름이 자연스럽게 만들어진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1727&quot; data-origin-height=&quot;923&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bz6Jnz/dJMcad1vnqO/diL5KjhOfWdhl6N7XEzPAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bz6Jnz/dJMcad1vnqO/diL5KjhOfWdhl6N7XEzPAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bz6Jnz/dJMcad1vnqO/diL5KjhOfWdhl6N7XEzPAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbz6Jnz%2FdJMcad1vnqO%2FdiL5KjhOfWdhl6N7XEzPAK%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;1727&quot; height=&quot;923&quot; data-origin-width=&quot;1727&quot; data-origin-height=&quot;923&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;레이어드 아키텍처&lt;/span&gt; Controller - Service - Repository - Entity/DTO를 분리해서 유지보수성과 테스트성을 높이는 구조.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;ResponseEntity&amp;lt;T&amp;gt;&lt;/span&gt; HTTP 응답 코드, 헤더, 바디를 직접 제어할 수 있는 스프링의 응답 래퍼. &lt;code&gt;ok()&lt;/code&gt;, &lt;code&gt;status()&lt;/code&gt;, &lt;code&gt;notFound()&lt;/code&gt; 등으로 표현.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;springdoc-openapi&lt;/span&gt; Spring MVC 기반 REST API를 OpenAPI 3.0 문서로 자동 변환해 주는 라이브러리. Swagger UI도 함께 제공.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;@Tag / @Operation&lt;/span&gt; Swagger에서 컨트롤러 그룹과 개별 API 메서드를 설명하기 위한 어노테이션.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;/v3/api-docs&lt;/span&gt; Swagger UI가 참조하는 OpenAPI JSON 원본 문서. 이 파일이 실제 &amp;ldquo;API 계약서&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Postman Import (OpenAPI)&lt;/span&gt; /v3/api-docs JSON을 Postman에 가져오면 Swagger 스펙을 기반으로 한 API Collection이 자동 생성되어 테스트가 매우 편해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❼ 오늘 실습에서 얻은 교훈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 &amp;ldquo;엔드포인트를 만들었다&amp;rdquo;에서 끝나는 것이 아니라, &lt;b&gt;HTTP 상태 코드, 문서화, 테스트 도구까지 연결하는 것&lt;/b&gt;이 요즘 백엔드 개발자의 기본 역량이라는 걸 몸으로 느낀 날이었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드를 짜는 것뿐 아니라, &lt;b&gt;API를 설명하고 공유할 수 있는 상태&lt;/b&gt;까지 만들어야 한다.&lt;/li&gt;
&lt;li&gt;Swagger(OpenAPI)와 Postman은 이를 도와주는 &lt;b&gt;표준 도구 세트&lt;/b&gt;다.&lt;/li&gt;
&lt;li&gt;앞으로 개인 프로젝트에서도 REST API를 만들 때, 오늘 실습한 흐름을 그대로 적용하면 포트폴리오의 완성도가 크게 올라갈 것이다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java/SpringBoot</category>
      <author>Heyjinu_</author>
      <guid isPermaLink="true">https://norang2810.tistory.com/119</guid>
      <comments>https://norang2810.tistory.com/119#entry119comment</comments>
      <pubDate>Tue, 2 Dec 2025 13:40:56 +0900</pubDate>
    </item>
    <item>
      <title>[LG U+ 유레카 3기] Spring Data JPA find() 메서드 실습</title>
      <link>https://norang2810.tistory.com/118</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Data JPA find() 메서드 실습 - Student 검색 패턴 정리&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❶ 상황 설명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 오전 실습에서는 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Spring Data JPA&lt;/span&gt;의 &lt;b&gt;메서드 이름 규칙 기반 조회(find)&lt;/b&gt;를 집중적으로 연습했다.&lt;br /&gt;특히 &lt;code&gt;Student&lt;/code&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;code&gt;findByName()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;복합 조건 조회: &lt;code&gt;findByEmailAndPhone()&lt;/code&gt;, &lt;code&gt;findByEmailOrPhone()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;문자열 패턴 조회: &lt;code&gt;StartingWith&lt;/code&gt;, &lt;code&gt;EndingWith&lt;/code&gt;, &lt;code&gt;Containing&lt;/code&gt;, &lt;code&gt;Like&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;정렬: &lt;code&gt;findAllByOrderByNameDesc()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;범위 검색: &lt;code&gt;findByIdBetween()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &amp;ldquo;&lt;b&gt;쿼리를 직접 안 써도, 메서드 이름만으로 조건과 정렬을 표현할 수 있다&lt;/b&gt;&amp;rdquo;를 몸으로 체득하는 것.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❷ Student 엔티티 &amp;ndash; 조회 대상 도메인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습에서 조회의 대상이 되는 엔티티는 &lt;code&gt;Student&lt;/code&gt; 테이블과 매핑되는 클래스이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;
package com.mycom.myapp.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Column;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Entity
@Table(name = &quot;student&quot;)
@Getter
@Setter
@ToString
public class Student {

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

    @Column(length = 200, nullable = false)
    private String email;

    @Column(length = 13, nullable = false)
    private String phone;

    @Column(length = 255, nullable = false)
    private String name;
}
&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;code&gt;@Entity&lt;/code&gt;, &lt;code&gt;@Table&lt;/code&gt; : JPA가 관리하는 엔티티 + 테이블 이름 지정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Id&lt;/code&gt; + &lt;code&gt;@GeneratedValue(IDENTITY)&lt;/code&gt; : PK + AUTO_INCREMENT 전략&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Column&lt;/code&gt; : 길이와 null 여부 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이 &lt;code&gt;Student&lt;/code&gt; 엔티티를 기준으로, Spring Data JPA의 &lt;code&gt;findBy...&lt;/code&gt; 메서드들을 다양하게 만들어본 것이 이번 실습의 내용이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❸ Repository &amp;ndash; 메서드 이름만으로 쿼리 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Spring Data JPA 메서드 이름 규칙&lt;/span&gt;이다.&lt;br /&gt;&lt;code&gt;StudentRepository&lt;/code&gt;에서 메서드 이름만 보고도 어떤 SQL이 나갈지 유추할 수 있게 되는 것이 목표였다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;
package com.mycom.myapp.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import com.mycom.myapp.entity.Student;

public interface StudentRepository extends JpaRepository&amp;lt;Student, Integer&amp;gt; {

    // 1) 단일 조건
    List&amp;lt;Student&amp;gt; findByName(String name);

    // 2) 복합 조건 (AND / OR)
    List&amp;lt;Student&amp;gt; findByEmailAndPhone(String email, String phone);
    List&amp;lt;Student&amp;gt; findByEmailOrPhone(String email, String phone);

    // 3) 문자열 패턴
    List&amp;lt;Student&amp;gt; findByNameStartingWith(String name);
    List&amp;lt;Student&amp;gt; findByEmailEndingWith(String email);
    List&amp;lt;Student&amp;gt; findByPhoneContaining(String phone);
    List&amp;lt;Student&amp;gt; findByNameLike(String name);

    // 4) 정렬
    List&amp;lt;Student&amp;gt; findAllByOrderByNameDesc();

    // 5) 범위 검색
    List&amp;lt;Student&amp;gt; findByIdBetween(int from, int to);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  메서드 이름 &amp;rarr; SQL 매핑 요약&lt;/h4&gt;
&lt;table border=&quot;1&quot; cellspacing=&quot;0&quot; cellpadding=&quot;6&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;메서드 이름&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;대략적인 JPQL / SQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;findByName&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이름이 정확히 일치&lt;/td&gt;
&lt;td&gt;&lt;code&gt;where name = ?&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;findByEmailAndPhone&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이메일과 전화번호 둘 다 일치&lt;/td&gt;
&lt;td&gt;&lt;code&gt;where email = ? and phone = ?&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;findByEmailOrPhone&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이메일 또는 전화번호 중 하나라도 일치&lt;/td&gt;
&lt;td&gt;&lt;code&gt;where email = ? or phone = ?&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;findByNameStartingWith&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이름이 특정 문자열로 시작&lt;/td&gt;
&lt;td&gt;&lt;code&gt;where name like '값%'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;findByEmailEndingWith&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이메일이 특정 문자열로 끝남&lt;/td&gt;
&lt;td&gt;&lt;code&gt;where email like '%값'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;findByPhoneContaining&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;전화번호에 특정 문자열이 포함&lt;/td&gt;
&lt;td&gt;&lt;code&gt;where phone like '%값%'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;findByNameLike&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;직접 패턴을 넣어서 like 검색&lt;/td&gt;
&lt;td&gt;&lt;code&gt;where name like ?&lt;/code&gt; (와일드카드는 개발자가 붙임)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;findAllByOrderByNameDesc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;전체를 name 내림차순으로 정렬&lt;/td&gt;
&lt;td&gt;&lt;code&gt;order by name desc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;findByIdBetween&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;id가 구간 안에 포함되는 데이터&lt;/td&gt;
&lt;td&gt;&lt;code&gt;where id between ? and ?&lt;/code&gt; (양끝 포함)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❹ Service &amp;amp; Controller &amp;ndash; 레이어드 아키텍처 적용&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;① Service 인터페이스&lt;/h4&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;
package com.mycom.myapp.service;

import java.util.List;
import com.mycom.myapp.entity.Student;

public interface StudentServiceFind {

    List&amp;lt;Student&amp;gt; findByName(String name);
    List&amp;lt;Student&amp;gt; findByEmailAndPhone(String email, String phone);
    List&amp;lt;Student&amp;gt; findByEmailOrPhone(String email, String phone);

    List&amp;lt;Student&amp;gt; findByNameStartingWith(String name);
    List&amp;lt;Student&amp;gt; findByEmailEndingWith(String email);
    List&amp;lt;Student&amp;gt; findByPhoneContaining(String phone);
    List&amp;lt;Student&amp;gt; findByNameLike(String name);

    List&amp;lt;Student&amp;gt; findAllByOrderByNameDesc();
    List&amp;lt;Student&amp;gt; findByIdBetween(int from, int to);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;② Service 구현체&lt;/h4&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;
package com.mycom.myapp.service;

import java.util.List;

import org.springframework.stereotype.Service;

import com.mycom.myapp.entity.Student;
import com.mycom.myapp.repository.StudentRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class StudentServiceFindImpl implements StudentServiceFind {

    private final StudentRepository studentRepository;

    @Override
    public List&amp;lt;Student&amp;gt; findByName(String name) {
        return studentRepository.findByName(name);
    }

    @Override
    public List&amp;lt;Student&amp;gt; findByEmailAndPhone(String email, String phone) {
        return studentRepository.findByEmailAndPhone(email, phone);
    }

    @Override
    public List&amp;lt;Student&amp;gt; findByEmailOrPhone(String email, String phone) {
        return studentRepository.findByEmailOrPhone(email, phone);
    }

    @Override
    public List&amp;lt;Student&amp;gt; findByEmailEndingWith(String email) {
        return studentRepository.findByEmailEndingWith(email);
    }

    @Override
    public List&amp;lt;Student&amp;gt; findByPhoneContaining(String phone) {
        return studentRepository.findByPhoneContaining(phone);
    }

    @Override
    public List&amp;lt;Student&amp;gt; findByNameStartingWith(String name) {
        return studentRepository.findByNameStartingWith(name);
    }

    @Override
    public List&amp;lt;Student&amp;gt; findByNameLike(String name) {
        // Like 패턴은 직접 만들어서 넘겨준다.
        return studentRepository.findByNameLike(&quot;%&quot; + name + &quot;%&quot;);
    }

    @Override
    public List&amp;lt;Student&amp;gt; findAllByOrderByNameDesc() {
        return studentRepository.findAllByOrderByNameDesc();
    }

    @Override
    public List&amp;lt;Student&amp;gt; findByIdBetween(int from, int to) {
        return studentRepository.findByIdBetween(from, to);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;③ Controller &amp;ndash; REST API 엔드포인트&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;
package com.mycom.myapp.controller;

import java.util.List;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.mycom.myapp.entity.Student;
import com.mycom.myapp.service.StudentServiceFind;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping(&quot;/students&quot;)
@RequiredArgsConstructor
public class StudentControllerFind {

    private final StudentServiceFind studentServiceFind;

    @GetMapping(&quot;/find/name&quot;)
    public List&amp;lt;Student&amp;gt; findByName(@RequestParam(&quot;name&quot;) String name) {
        return studentServiceFind.findByName(name);
    }

    @GetMapping(&quot;/find/emailandphone&quot;)
    public List&amp;lt;Student&amp;gt; findByEmailAndPhone(@RequestParam(&quot;email&quot;) String email,
                                                @RequestParam(&quot;phone&quot;) String phone) {
        return studentServiceFind.findByEmailAndPhone(email, phone);
    }

    @GetMapping(&quot;/find/emailorphone&quot;)
    public List&amp;lt;Student&amp;gt; findByEmailOrPhone(@RequestParam(&quot;email&quot;) String email,
                                               @RequestParam(&quot;phone&quot;) String phone) {
        return studentServiceFind.findByEmailOrPhone(email, phone);
    }

    @GetMapping(&quot;/find/namestartingwith&quot;)
    public List&amp;lt;Student&amp;gt; findByNameStartingWith(@RequestParam(&quot;name&quot;) String name) {
        return studentServiceFind.findByNameStartingWith(name);
    }

    @GetMapping(&quot;/find/emailendingwith&quot;)
    public List&amp;lt;Student&amp;gt; findByEmailEndingWith(@RequestParam(&quot;email&quot;) String email) {
        return studentServiceFind.findByEmailEndingWith(email);
    }

    @GetMapping(&quot;/find/phonecontaining&quot;)
    public List&amp;lt;Student&amp;gt; findByPhoneContaining(@RequestParam(&quot;phone&quot;) String phone) {
        return studentServiceFind.findByPhoneContaining(phone);
    }

    @GetMapping(&quot;/find/namelike&quot;)
    public List&amp;lt;Student&amp;gt; findByNameLike(@RequestParam(&quot;name&quot;) String name) {
        return studentServiceFind.findByNameLike(name);
    }

    @GetMapping(&quot;/find/orderbynamedesc&quot;)
    public List&amp;lt;Student&amp;gt; findAllByOrderByNameDesc() {
        return studentServiceFind.findAllByOrderByNameDesc();
    }

    @GetMapping(&quot;/find/idbetween&quot;)
    public List&amp;lt;Student&amp;gt; findByIdBetween(@RequestParam(&quot;from&quot;) int from,
                                            @RequestParam(&quot;to&quot;) int to) {
        return studentServiceFind.findByIdBetween(from, to);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해서 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Controller &amp;rarr; Service &amp;rarr; Repository &amp;rarr; Entity &amp;rarr; DB&lt;/span&gt; 흐름을 깔끔하게 나눈 레이어드 아키텍처로 작성했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❺ Postman으로 테스트한 주요 예시&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 이름 정확 일치 검색&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;
GET http://localhost:8080/students/find/name?name=홍길동1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1445&quot; data-origin-height=&quot;922&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tSvqI/dJMcac9k3A2/gH7EeJaRQFZtZdDqkLDyOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tSvqI/dJMcac9k3A2/gH7EeJaRQFZtZdDqkLDyOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tSvqI/dJMcac9k3A2/gH7EeJaRQFZtZdDqkLDyOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtSvqI%2FdJMcac9k3A2%2FgH7EeJaRQFZtZdDqkLDyOK%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;1445&quot; height=&quot;922&quot; data-origin-width=&quot;1445&quot; data-origin-height=&quot;922&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 이메일 + 전화번호 AND 검색&lt;/h4&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;
GET http://localhost:8080/students/find/emailandphone?email=1@gildong.com&amp;amp;phone=010-0000-0001
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 이메일 OR 전화번호 검색&lt;/h4&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;
GET http://localhost:8080/students/find/emailorphone?email=1@gildong.com&amp;amp;phone=010-0000-0003
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) 이름 시작 문자열 검색 (StartingWith)&lt;/h4&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;
GET http://localhost:8080/students/find/namestartingwith?name=홍길동
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;657&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIV5p4/dJMcabQazbM/eKd46UBvloLvkSBdwvMOmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIV5p4/dJMcabQazbM/eKd46UBvloLvkSBdwvMOmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIV5p4/dJMcabQazbM/eKd46UBvloLvkSBdwvMOmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIV5p4%2FdJMcabQazbM%2FeKd46UBvloLvkSBdwvMOmK%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;808&quot; height=&quot;657&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;657&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5) 이메일 끝나는 문자열 검색 (EndingWith)&lt;/h4&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;
GET http://localhost:8080/students/find/emailendingwith?email=@gmail.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6) 전화번호 포함 문자열 검색 (Containing)&lt;/h4&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;
GET http://localhost:8080/students/find/phonecontaining?phone=0000
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;7) Like 검색 (직접 패턴 구성)&lt;/h4&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;
GET http://localhost:8080/students/find/namelike?name=홍길동
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8) 이름 내림차순 정렬&lt;/h4&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;
GET http://localhost:8080/students/find/orderbynamedesc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1445&quot; data-origin-height=&quot;922&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l59yb/dJMcabvRXgp/K9y1wWQjj8Xl1ZXSlfmXs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l59yb/dJMcabvRXgp/K9y1wWQjj8Xl1ZXSlfmXs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l59yb/dJMcabvRXgp/K9y1wWQjj8Xl1ZXSlfmXs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl59yb%2FdJMcabvRXgp%2FK9y1wWQjj8Xl1ZXSlfmXs0%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;1445&quot; height=&quot;922&quot; data-origin-width=&quot;1445&quot; data-origin-height=&quot;922&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;9) ID 범위 검색 (Between)&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;
GET http://localhost:8080/students/find/idbetween?from=31&amp;amp;to=40
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;888&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcmvK0/dJMcaaX12sK/uxEoSBr1odcMChad8K8zTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcmvK0/dJMcaaX12sK/uxEoSBr1odcMChad8K8zTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcmvK0/dJMcaaX12sK/uxEoSBr1odcMChad8K8zTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcmvK0%2FdJMcaaX12sK%2FuxEoSBr1odcMChad8K8zTk%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;1510&quot; height=&quot;888&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;888&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 요청에 대해 Hibernate가 출력하는 SQL 로그를 보면서, &lt;b&gt;&amp;ldquo;메서드 이름 &amp;rarr; 실제 쿼리&amp;rdquo;&lt;/b&gt;가 머릿속에서 연결되도록 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❻ 개념 정리 &amp;ndash; 꼭 기억해둘 포인트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Spring Data JPA 메서드 규칙&lt;/span&gt;을 이해하면, 간단한 조회는 JPQL/SQL을 직접 쓰지 않고도 구현 가능하다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;findByXxx&lt;/code&gt; : 기본 equal 검색&lt;/li&gt;
&lt;li&gt;&lt;code&gt;And&lt;/code&gt;, &lt;code&gt;Or&lt;/code&gt; : 복합 조건 결합&lt;/li&gt;
&lt;li&gt;&lt;code&gt;StartingWith / EndingWith / Containing&lt;/code&gt; : 내부적으로 &lt;code&gt;like&lt;/code&gt;를 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Like&lt;/code&gt; : 패턴(&lt;code&gt;&quot;%값%&quot;&lt;/code&gt;)은 개발자가 직접 만들어서 넣는 방식&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OrderBy필드명Desc/Asc&lt;/code&gt; : 정렬 조건을 메서드 이름에 포함&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Between&lt;/code&gt; : 범위 검색이며, &lt;b&gt;양 끝값도 포함&lt;/b&gt;된다.&lt;/li&gt;
&lt;li&gt;레이어드 아키텍처: &lt;code&gt;Controller &amp;rarr; Service &amp;rarr; Repository &amp;rarr; Entity &amp;rarr; DB&lt;/code&gt; 구조를 유지하면, 나중에 기능이 복잡해져도 유지보수가 훨씬 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;&amp;ldquo;메서드 이름을 잘 짓는 것 = 쿼리를 잘 짜는 것&amp;rdquo;&lt;/span&gt;과 비슷하다는 감각을 얻었다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;findByNameLike(&quot;%&quot; + name + &quot;%&quot;)&lt;/code&gt; 처럼, 결국 뒤에서 어떤 SQL이 나갈지를 상상할 수 있어야 진짜로 JPA를 이해한 것이다.&lt;/li&gt;
&lt;li&gt;Controller, Service, Repository를 분리해 둔 덕분에, 중간에 검색 조건을 추가하거나 바꿀 때 구조 전체를 바꿀 필요가 없다.&lt;/li&gt;
&lt;li&gt;이번 실습은 이후에 배울 &lt;b&gt;페이징(Pageable), 정렬(Sort), 동적 쿼리(JPQL/QueryDSL)&lt;/b&gt;을 이해하는 기초가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오전 실습 덕분에, 앞으로 프로젝트에서 학생/회원/게시글 등의 검색 기능을 만들 때 Spring Data JPA의 메서드 이름 기반 조회를 자신 있게 활용할 수 있는 기반이 만들어졌다.&lt;/p&gt;</description>
      <category>Java/JPA</category>
      <author>Heyjinu_</author>
      <guid isPermaLink="true">https://norang2810.tistory.com/118</guid>
      <comments>https://norang2810.tistory.com/118#entry118comment</comments>
      <pubDate>Wed, 26 Nov 2025 17:54:03 +0900</pubDate>
    </item>
    <item>
      <title>[LG U+ 유레카 3기] Spring Data JPA CRUD + Lombok + 패턴 실습 정리</title>
      <link>https://norang2810.tistory.com/117</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Data JPA CRUD + Lombok + 패턴 + 레이어드 아키텍처 한 번에 정리&lt;/h2&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 오늘 실습 상황 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습에서는 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Spring Boot + Spring Data JPA + Lombok&lt;/span&gt; 조합으로 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Student 테이블 CRUD + 페이징&lt;/span&gt;을 구현하고,&lt;br /&gt;추가로 &lt;b&gt;레이어드 아키텍처 개념&lt;/b&gt;, &lt;b&gt;Lombok의 역할&lt;/b&gt;, &lt;b&gt;Spring Data JPA 구조&lt;/b&gt;, &lt;b&gt;Builder 패턴&lt;/b&gt;, &lt;b&gt;Singleton 패턴&lt;/b&gt;까지 연결해서 정리했다.&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;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;entity&lt;/span&gt; : Student 엔티티&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;repository&lt;/span&gt; : StudentRepository (JpaRepository 상속)&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;service&lt;/span&gt; : StudentServiceCrud / StudentServiceCrudImpl&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;controller&lt;/span&gt; : StudentControllerCrud (REST API)&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;pattern&lt;/span&gt; : builder / methodchain / singleton 개념 예제&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;lombok&lt;/span&gt; : Lombok 어노테이션 맛보기&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;265&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3yvmr/dJMcabo5N58/GKV24HEbfouDnQu11hw05k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3yvmr/dJMcabo5N58/GKV24HEbfouDnQu11hw05k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3yvmr/dJMcabo5N58/GKV24HEbfouDnQu11hw05k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3yvmr%2FdJMcabo5N58%2FGKV24HEbfouDnQu11hw05k%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;265&quot; height=&quot;458&quot; data-origin-width=&quot;265&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 레이어드 아키텍처 vs Spring MVC (개념 정리)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1. 레이어드 아키텍처란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이어드 아키텍처(Layered Architecture)는 &lt;b&gt;애플리케이션을 역할별 층(layer)으로 나눠서 설계하는 방식&lt;/b&gt;이다.&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;[Controller]  &amp;larr; 프레젠테이션 계층 (웹 요청/응답)
   &amp;darr;
[Service]     &amp;larr; 비즈니스 로직 계층
   &amp;darr;
[Repository]  &amp;larr; 데이터 접근 계층 (DB 조회/저장)
   &amp;darr;
[DB]          &amp;larr; 실제 데이터베이스 (MySQL 등)
&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;각 계층은 &amp;ldquo;아래 계층의 기능만 사용&amp;rdquo;하도록 설계 &amp;rarr; 결합도 감소, 변경 용이&lt;/li&gt;
&lt;li&gt;테스트, 유지보수, 역할 분리가 쉬워진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-2. Spring MVC는 무엇인가?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC는 &lt;b&gt;웹 요청(HTTP 요청/응답)을 처리하는 프레임워크&lt;/b&gt;다.&lt;br /&gt;레이어드 아키텍처 중 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;프레젠테이션 계층(Controller + View)&lt;/span&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;code&gt;@Controller&lt;/code&gt;, &lt;code&gt;@RestController&lt;/code&gt; 로 요청 처리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@GetMapping&lt;/code&gt;, &lt;code&gt;@PostMapping&lt;/code&gt; 등으로 URL + HTTP Method 매핑&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-3. 관계 정리&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;레이어드 아키텍처&lt;/b&gt; = 애플리케이션 전체 구조 설계 방식&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring MVC&lt;/b&gt; = 그 구조 중 &amp;ldquo;웹 계층(Controller)&amp;rdquo;을 구현하는 웹 프레임워크&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 줄로 요약하면,&lt;br /&gt;&lt;b&gt;레이어드 아키텍처는 건물 설계도, Spring MVC는 1층 로비를 구현하는 기술&lt;/b&gt; 정도로 이해하면 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. DB &amp;amp; Student 엔티티 매핑&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-1. Student 테이블 DDL (요약)&lt;/h4&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;create table student (
  id int not null auto_increment,
  email varchar(200) default null,
  phone varchar(13) default '010-0000-0000',
  name varchar(255) default null,
  primary key (id)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습에서는 미리 1~100까지 &lt;code&gt;홍길동1~홍길동100&lt;/code&gt; 데이터를 insert 해 두고, 곧바로 조회/페이징 테스트가 가능하도록 구성했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-2. Student 엔티티 코드&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package com.mycom.myapp.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Column;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Entity
@Table(name = &quot;student&quot;)
@Getter
@Setter
@ToString
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT
    private Integer id;

    @Column(length = 200)
    private String email;

    @Column(length = 13, columnDefinition = &quot;varchar(13) default '010-0000-0000'&quot;)
    private String phone;

    @Column(length = 255)
    private String name;
}
&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;code&gt;@Entity&lt;/code&gt; : JPA가 관리하는 엔티티 클래스&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Table(name = &quot;student&quot;)&lt;/code&gt; : 실제 매핑될 테이블 이름&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Id&lt;/code&gt; + &lt;code&gt;@GeneratedValue(IDENTITY)&lt;/code&gt; : PK + AUTO_INCREMENT 전략 사용&lt;/li&gt;
&lt;li&gt;Lombok(&lt;code&gt;@Getter&lt;/code&gt;, &lt;code&gt;@Setter&lt;/code&gt;, &lt;code&gt;@ToString&lt;/code&gt;)로 보일러플레이트 코드 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Lombok이 실제로 하는 일과 정체&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-1. Lombok 이란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Lombok&lt;/span&gt;은 &lt;b&gt;컴파일 시점에 어노테이션을 보고 자바 코드를 자동 생성해주는 라이브러리&lt;/b&gt;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바 표준이 아니라 &lt;b&gt;외부 라이브러리&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;IDE 플러그인 + Gradle/Maven 의존성 필요&lt;/li&gt;
&lt;li&gt;소스에는 코드가 안 보여도, &lt;code&gt;.class&lt;/code&gt; 파일에는 실제 메서드가 생성되어 들어간다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-2. 실습에서 사용한 Lombok 어노테이션&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Getter&lt;/code&gt; / &lt;code&gt;@Setter&lt;/code&gt; : getter / setter 자동 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@ToString&lt;/code&gt; : &lt;code&gt;toString()&lt;/code&gt; 자동 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@EqualsAndHashCode&lt;/code&gt; : equals, hashCode 자동 생성 (이번엔 직접 쓰진 않았지만 대표적인 기능)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@AllArgsConstructor&lt;/code&gt;, &lt;code&gt;@NoArgsConstructor&lt;/code&gt; : 모든 필드/기본 생성자 자동 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Builder&lt;/code&gt; : 빌더 패턴 코드 자동 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RequiredArgsConstructor&lt;/code&gt; : &lt;b&gt;final 필드만 받는 생성자&lt;/b&gt; 자동 생성 &amp;rarr; 생성자 주입(DI)에 많이 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-3. 왜 Spring과 궁합이 좋나?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Service에서 이렇게 작성하면:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class StudentServiceCrudImpl implements StudentServiceCrud {

    private final StudentRepository studentRepository;

    // 나머지 메서드들...
}
&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;code&gt;@RequiredArgsConstructor&lt;/code&gt;가 &lt;code&gt;StudentRepository&lt;/code&gt;를 파라미터로 받는 생성자를 자동 생성&lt;/li&gt;
&lt;li&gt;Spring 이 이 생성자를 사용해서 &lt;b&gt;생성자 주입(DI)&lt;/b&gt;을 수행&lt;/li&gt;
&lt;li&gt;우리는 굳이 &lt;code&gt;public StudentServiceCrudImpl(StudentRepository studentRepository) {...}&lt;/code&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;846&quot; data-origin-height=&quot;497&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKE4Ou/dJMcaiaGFhC/GgAGT5gOYdSPKtgNCj8DB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKE4Ou/dJMcaiaGFhC/GgAGT5gOYdSPKtgNCj8DB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKE4Ou/dJMcaiaGFhC/GgAGT5gOYdSPKtgNCj8DB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKE4Ou%2FdJMcaiaGFhC%2FGgAGT5gOYdSPKtgNCj8DB1%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;749&quot; height=&quot;440&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;497&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #dddddd;&quot;&gt;STS4 에 자동으로 설정되는것이아닌 직접 Lombok.jar을 열어&lt;/span&gt;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #dddddd;&quot;&gt; sts의 exe 파일을 지정해줬다.&lt;/span&gt;&lt;/h3&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;h3 data-ke-size=&quot;size23&quot;&gt;5. Spring Data JPA &amp;amp; JpaRepository&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5-1. JPA / Hibernate / Spring Data JPA 관계&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;JPA&lt;/span&gt; : 자바 ORM에 대한 &lt;b&gt;표준 스펙(인터페이스 규격)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Hibernate&lt;/span&gt; : JPA 구현체 중 하나 (우리가 실제로 사용하는 라이브러리)&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Spring Data JPA&lt;/span&gt; : JPA/Hibernate 위에 얹혀 &lt;b&gt;Repository 인터페이스만 정의하면 CRUD/페이징 등을 자동 구현&lt;/b&gt;해 주는 스프링 프레임워크&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5-2. StudentRepository 코드&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;package com.mycom.myapp.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import com.mycom.myapp.entity.Student;

// Spring Data Jpa 의 시작은 제공되는 interface 를 상속받는 것.
// 이를 통해서 Student 에 대한 기본적인 CRUD 는 자동화 처리
// 이 interface 를 구현하는 클래스를 생성 X &amp;lt;= Spring Data Jpa 가 자동으로 생성
public interface StudentRepository extends JpaRepository&amp;lt;Student, Integer&amp;gt; {

}
&lt;/code&gt;&lt;/pre&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;code&gt;JpaRepository&amp;lt;Student, Integer&amp;gt;&lt;/code&gt; 상속 &amp;rarr; 이 순간부터 Student에 대한 CRUD, 페이징, 정렬 기능이 자동 제공&lt;/li&gt;
&lt;li&gt;우리가 구현체 클래스를 만들지 않아도, Spring Data JPA가 프록시 객체를 만들어서 빈으로 등록&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5-3. 기본 제공 메서드&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;findAll()&lt;/code&gt; : 전체 목록 조회&lt;/li&gt;
&lt;li&gt;&lt;code&gt;findById(id)&lt;/code&gt; : PK로 한 건 조회 &amp;rarr; &lt;code&gt;Optional&amp;lt;Student&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;save(entity)&lt;/code&gt; : &lt;b&gt;id 없으면 insert, id 있으면 update&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deleteById(id)&lt;/code&gt; : PK로 삭제&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count()&lt;/code&gt; : 전체 건수&lt;/li&gt;
&lt;li&gt;&lt;code&gt;findAll(Pageable pageable)&lt;/code&gt; : 페이징 &amp;amp; 정렬된 목록&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. Service 계층 &amp;ndash; StudentServiceCrud &amp;amp; Impl&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6-1. Service 인터페이스&lt;/h4&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package com.mycom.myapp.service;

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

import com.mycom.myapp.entity.Student;

// 학습 예제 코드의 간단함을 위해 Student &amp;lt;-&amp;gt; StudentDto 생략
public interface StudentServiceCrud {

    // 목록, 상세
    List&amp;lt;Student&amp;gt; listStudent();
    Optional&amp;lt;Student&amp;gt; detailStudent(int id);

    // 등록, 수정, 삭제
    Student insertStudent(Student student);
    Optional&amp;lt;Student&amp;gt; updateStudent(Student student);
    void deleteStudent(int id);

    // 전체 건수, 페이징
    long countStudent();
    List&amp;lt;Student&amp;gt; listStudent(int pageNumber, int pageSize);
}
&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;Controller는 이 인터페이스만 바라보고 호출&lt;/li&gt;
&lt;li&gt;구현체를 다른 걸로 교체해도 Controller 코드는 그대로 유지 가능 &amp;rarr; 느슨한 결합&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6-2. Service 구현 &amp;ndash; StudentServiceCrudImpl&lt;/h4&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;package com.mycom.myapp.service;

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

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import com.mycom.myapp.entity.Student;
import com.mycom.myapp.repository.StudentRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class StudentServiceCrudImpl implements StudentServiceCrud {

    // 생성자 주입
    private final StudentRepository studentRepository;

    @Override
    public List&amp;lt;Student&amp;gt; listStudent() {
        return studentRepository.findAll(); // 전체 목록
    }

    @Override
    public Optional&amp;lt;Student&amp;gt; detailStudent(int id) {
        return studentRepository.findById(id);
    }

    // save()
    //  전달되는 엔티티 객체에 id 가 있으면 select - update
    //  전달되는 엔티티 객체에 id 가 없으면 insert
    @Override
    public Student insertStudent(Student student) {
        return studentRepository.save(student);
    }

    @Override
    public Optional&amp;lt;Student&amp;gt; updateStudent(Student student) {
        // 무조건 save 호출
        return Optional.of(studentRepository.save(student));

        // 체크하고 save 호출 (예시)
        // Optional&amp;lt;Student&amp;gt; existingStudent = studentRepository.findById(student.getId());
        // if (existingStudent.isPresent()) {
        //     return Optional.of(studentRepository.save(student));
        // }
        // return Optional.empty();
    }

    @Override
    public void deleteStudent(int id) {
        studentRepository.deleteById(id);
    }

    @Override
    public long countStudent() {
        return studentRepository.count();
    }

    // 마지막 페이지에 대한 요청을 제외하고, 페이지 요청을 하면 항상 count() 를 통해서 Page 객체를 구성한다.
    // 단순 목록 외 나머지 항목들 계산을 위해 count() 수행
    @Override
    public List&amp;lt;Student&amp;gt; listStudent(int pageNumber, int pageSize) {
        Pageable pageable = PageRequest.of(pageNumber, pageSize);
        Page&amp;lt;Student&amp;gt; page = studentRepository.findAll(pageable);
        return page.toList();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6-3. save() 내부 동작 이해&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;id 없음&lt;/b&gt; &amp;rarr; 신규 엔티티로 판단 &amp;rarr; &lt;code&gt;insert into student ...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;id 있음&lt;/b&gt; &amp;rarr; 기존 엔티티로 간주 &amp;rarr; 내부적으로 &lt;code&gt;select&lt;/code&gt; 후 &lt;code&gt;update&lt;/code&gt; 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강사님 주석 내용처럼,&lt;br /&gt;&lt;b&gt;&amp;ldquo;id 가 없는 update 처리. findById() 와 if() 코드를 따르지 않고, 항상 save 하도록 하면 insert 가 수행된다.&amp;rdquo;&lt;/b&gt;&lt;br /&gt;&amp;rarr; 이게 바로 save()의 동작 원리.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. Controller 계층 &amp;ndash; StudentControllerCrud&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package com.mycom.myapp.controller;

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

import org.springframework.web.bind.annotation.*;

import com.mycom.myapp.entity.Student;
import com.mycom.myapp.service.StudentServiceCrud;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping(&quot;/students&quot;)
@RequiredArgsConstructor
public class StudentControllerCrud {

    private final StudentServiceCrud studentServiceCrud;

    // 1. 전체 목록
    @GetMapping(&quot;/list&quot;)
    public List&amp;lt;Student&amp;gt; listStudent() {
        return studentServiceCrud.listStudent();
    }

    // 2. 상세 조회
    @GetMapping(&quot;/detail/{id}&quot;)
    public Optional&amp;lt;Student&amp;gt; detailStudent(@PathVariable(&quot;id&quot;) int id) {
        return studentServiceCrud.detailStudent(id);
    }

    // 3. 등록 (JSON Body)
    @PostMapping(&quot;/insert&quot;)
    public Student insertStudent(@RequestBody Student student) {
        return studentServiceCrud.insertStudent(student);
    }

    // 4. 수정 (JSON Body)
    @PutMapping(&quot;/update&quot;)
    public Optional&amp;lt;Student&amp;gt; updateStudent(@RequestBody Student student) {
        return studentServiceCrud.updateStudent(student);
    }

    // 5. 삭제
    @DeleteMapping(&quot;/delete/{id}&quot;)
    public void deleteStudent(@PathVariable(&quot;id&quot;) int id) {
        studentServiceCrud.deleteStudent(id);
    }

    // 6. 전체 건수
    @GetMapping(&quot;/count&quot;)
    public long countStudent() {
        return studentServiceCrud.countStudent();
    }

    // 7. 페이징 목록
    @GetMapping(&quot;/page&quot;)
    public List&amp;lt;Student&amp;gt; listStudent(
            @RequestParam(&quot;pageNumber&quot;) Integer pageNumber,
            @RequestParam(&quot;pageSize&quot;) Integer pageSize) {
        return studentServiceCrud.listStudent(pageNumber, pageSize);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;각 API 요약 (Postman 기준)&lt;/h4&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;GET /students/list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상세&lt;/b&gt; : &lt;code&gt;GET /students/detail/{id}&lt;/code&gt; (예: &lt;code&gt;/students/detail/1&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;등록&lt;/b&gt; : &lt;code&gt;POST /students/insert&lt;/code&gt; + Body(JSON)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수정&lt;/b&gt; : &lt;code&gt;PUT /students/update&lt;/code&gt; + Body(JSON)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;삭제&lt;/b&gt; : &lt;code&gt;DELETE /students/delete/{id}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전체 건수&lt;/b&gt; : &lt;code&gt;GET /students/count&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;페이징&lt;/b&gt; : &lt;code&gt;GET /students/page?pageNumber=0&amp;amp;pageSize=10&lt;/code&gt;&lt;code&gt;&lt;/code&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;1162&quot; data-origin-height=&quot;855&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbmlFt/dJMcaacEnZt/hlyFWgBCKZ41QiTKRE9zo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbmlFt/dJMcaacEnZt/hlyFWgBCKZ41QiTKRE9zo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbmlFt/dJMcaacEnZt/hlyFWgBCKZ41QiTKRE9zo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbmlFt%2FdJMcaacEnZt%2FhlyFWgBCKZ41QiTKRE9zo1%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;1162&quot; height=&quot;855&quot; data-origin-width=&quot;1162&quot; data-origin-height=&quot;855&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. Postman 테스트 &amp;amp; -parameters 문제 트러블슈팅&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8-1. -parameters 에러 상황&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삭제 API 호출 시 다음과 같은 에러가 떴다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;IllegalArgumentException: Name for argument of type [int] not specified,
and parameter name information not available via reflection.
Ensure that the compiler uses the '-parameters' flag.
&lt;/code&gt;&lt;/pre&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;Java 컴파일 시, 메서드 파라미터 이름(&lt;code&gt;id&lt;/code&gt;) 정보가 class에 포함되지 않음&lt;/li&gt;
&lt;li&gt;Spring이 &lt;code&gt;@PathVariable int id&lt;/code&gt; 에서 &lt;code&gt;id&lt;/code&gt;란 이름을 reflection으로 읽지 못해 에러&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8-2. 해결 방법 (Gradle + STS4)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;build.gradle&lt;/code&gt;에 다음 추가:&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;tasks.withType(JavaCompile) {
    options.compilerArgs += [&quot;-parameters&quot;]
}
&lt;/code&gt;&lt;/pre&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;Gradle Refresh&lt;/li&gt;
&lt;li&gt;Project Clean&lt;/li&gt;
&lt;li&gt;Spring Boot 재실행&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8-3. @PathVariable 명시적으로 이름 지정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음처럼 이름을 명시해 주면 더 안전하다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@DeleteMapping(&quot;/delete/{id}&quot;)
public void deleteStudent(@PathVariable(&quot;id&quot;) int id) {
    studentServiceCrud.deleteStudent(id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8-4. Postman DELETE 요청 주의&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Method: &lt;code&gt;DELETE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;URL: &lt;code&gt;http://localhost:8080/students/delete/100&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Body: &lt;b&gt;none&lt;/b&gt; (DELETE는 보통 Body 필요 없음)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 패턴 패키지 &amp;ndash; Builder / Method Chaining / Singleton&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;9-1. Builder 패턴 예제 &amp;ndash; Board&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package com.mycom.myapp.pattern.builder;

// builder pattern (Inner Class 버전)
public class Board {
    private final String title;
    private final String content;
    private final String category;

    Board(Builder builder){
        this.title = builder.title;
        this.content = builder.content;
        this.category = builder.category;
    }

    public static class Builder{
        private String title;
        private String content;
        private String category;

        public Builder title(String title) {
            this.title = title;
            return this;
        }

        public Builder content(String content) {
            this.content = content;
            return this;
        }

        public Builder category(String category) {
            this.category = category;
            return this;
        }

        public Board build() {
            return new Board(this);
        }
    }

    @Override
    public String toString() {
        return &quot;Board [title=&quot; + title + &quot;, content=&quot; + content + &quot;, category=&quot; + category + &quot;]&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Test {

    public static void main(String[] args) {
        Board board = new Board.Builder()
                            .title(&quot;게시글 제목&quot;)
                            .content(&quot;게시글 내용&quot;)
                            .category(&quot;분류 A&quot;)
                            .build();

        System.out.println(board);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;9-2. Builder 패턴 vs 메서드 체이닝&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;메서드 체이닝(Method Chaining)&lt;/b&gt;: &lt;code&gt;obj.a().b().c()&lt;/code&gt; 처럼 메서드 호출을 이어서 쓰는 &amp;ldquo;스타일&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Builder 패턴&lt;/b&gt;: 이 체이닝 스타일을 활용해서 &lt;b&gt;복잡한 객체 생성 문제를 푸는 디자인 패턴&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 Board 예제에서:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;new Board.Builder()&lt;/code&gt; &amp;rarr; 빌더 객체 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.title()&lt;/code&gt;, &lt;code&gt;.content()&lt;/code&gt;, &lt;code&gt;.category()&lt;/code&gt; &amp;rarr; 빌더에 값 세팅 (체이닝)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.build()&lt;/code&gt; &amp;rarr; 최종 &lt;code&gt;Board&lt;/code&gt; 객체 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &amp;ldquo;&lt;b&gt;필드가 많은 객체를, 생성자 대신 가독성 좋게 만들기&lt;/b&gt;&amp;rdquo;라는 점이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;9-3. Singleton 패턴 &amp;ndash; Logger 예제&lt;/h4&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public class Logger {
    private static final Logger instance = new Logger();

    private Logger() {}

    public static Logger getInstance() {
        return instance;
    }

    public void log(String msg) {
        System.out.println(msg);
    }
}
&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;code&gt;private static final Logger instance = new Logger();&lt;/code&gt;&lt;br /&gt;&amp;rarr; 애플리케이션 전체에서 &lt;b&gt;단 하나만 존재하는 객체&lt;/b&gt;를 미리 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;private Logger() {}&lt;/code&gt; &amp;rarr; 외부에서 &lt;code&gt;new Logger()&lt;/code&gt; 금지 (생성자 숨김)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getInstance()&lt;/code&gt; &amp;rarr; 이 메서드를 통해서만 Logger에 접근&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 질문했던 것처럼&lt;br /&gt;&lt;b&gt;&lt;code&gt;private static final Logger instance = new Logger();&lt;/code&gt; 이 줄이 &amp;ldquo;싱글톤 인스턴스 선언 + 생성&amp;rdquo;하는 부분이 맞다.&lt;/b&gt;&lt;br /&gt;다만, 진짜 싱글톤 패턴은 보통 &amp;ldquo;private 생성자 + static 단일 필드 + static getter&amp;rdquo; 이 세트를 함께 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서는 &lt;code&gt;@Service&lt;/code&gt;, &lt;code&gt;@Repository&lt;/code&gt;, &lt;code&gt;@Controller&lt;/code&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;10. 오늘 Q&amp;amp;A 개념 정리 요약&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;10-1. 레이어드 아키텍처 vs Spring MVC&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레이어드 아키텍처 = 애플리케이션 전체를 계층(Controller / Service / Repository / DB)으로 나눈 설계 방식&lt;/li&gt;
&lt;li&gt;Spring MVC = 그 중 &amp;ldquo;웹 계층(Controller)&amp;rdquo;을 구현하는 웹 프레임워크&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;10-2. Lombok 이란?&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴파일 시점에 어노테이션을 보고 코드(getter/setter/생성자/builder 등)를 생성해주는 라이브러리&lt;/li&gt;
&lt;li&gt;보일러플레이트 코드 제거 &amp;rarr; 코드 양 줄이고 가독성 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;10-3. Spring Data JPA &amp;amp; JpaRepository&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;JpaRepository&amp;lt;Student, Integer&amp;gt;&lt;/code&gt; 상속 = &amp;ldquo;이 엔티티에 대한 CRUD/페이징/정렬을 Spring Data JPA가 자동 구현해줘&amp;rdquo; 라는 의미&lt;/li&gt;
&lt;li&gt;우리는 Repository 인터페이스만 선언하고, 구현체는 만들지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;10-4. Builder 패턴 핵심&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드 체이닝을 이용하는 &amp;ldquo;스타일&amp;rdquo;이면서, 동시에 &amp;ldquo;객체 생성 문제&amp;rdquo;를 해결하는 디자인 패턴&lt;/li&gt;
&lt;li&gt;필드가 많거나, 선택적인 값이 많은 경우에 유용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;10-5. Singleton 패턴 핵심&lt;/h4&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;code&gt;private static final 인스턴스 + private 생성자 + public static getter&lt;/code&gt; 세트로 이해&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11. 교훈 / 핵심 요약&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;레이어드 아키텍처&lt;/span&gt;를 이해하면, 프로젝트 구조가 단숨에 눈에 들어온다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Lombok&lt;/span&gt;은 코드 양을 줄여줄 뿐 아니라, 생성자 주입(&lt;code&gt;@RequiredArgsConstructor&lt;/code&gt;)과 같이 스프링과 궁합이 매우 좋다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Spring Data JPA&lt;/span&gt;는 JPA 위에 얹힌 추상화 계층으로, Repository 인터페이스만 정의하면 CRUD/페이징을 거의 공짜로 얻는다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Builder 패턴&lt;/span&gt;은 메서드 체이닝을 활용해 복잡한 객체 생성을 가독성 좋게 만드는 패턴이다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Singleton 패턴&lt;/span&gt;은 &amp;ldquo;단일 인스턴스&amp;rdquo;를 보장하는 패턴이고, 스프링 빈 기본 스코프와 개념적으로 연결된다.&lt;/li&gt;
&lt;li&gt;Postman으로 API를 직접 호출하면서, Controller &amp;rarr; Service &amp;rarr; Repository &amp;rarr; DB 흐름을 눈으로 확인해 보며 JPA 동작 방식(save, find, delete, paging)을 체감했다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java/JPA</category>
      <author>Heyjinu_</author>
      <guid isPermaLink="true">https://norang2810.tistory.com/117</guid>
      <comments>https://norang2810.tistory.com/117#entry117comment</comments>
      <pubDate>Tue, 25 Nov 2025 17:59:06 +0900</pubDate>
    </item>
    <item>
      <title>[LG U+ 유레카 3기]  Spring MVC + CORS 실습</title>
      <link>https://norang2810.tistory.com/116</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 실습은&lt;/b&gt; Spring MVC의 핵심 개념인 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;POST 처리&lt;/span&gt;, &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;redirect 흐름&lt;/span&gt;, &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;RestController의 JSON 응답 방식&lt;/span&gt;, 그리고 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;CORS 전역 설정&lt;/span&gt;까지 묶어서 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;143&quot; data-start=&quot;105&quot; data-ke-size=&quot;size26&quot;&gt;✔ @SpringBootApplication&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-end=&quot;181&quot; data-start=&quot;145&quot; data-ke-size=&quot;size23&quot;&gt;1️⃣ &lt;b&gt;@SpringBootConfiguration&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;241&quot; data-start=&quot;182&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;212&quot; data-start=&quot;182&quot;&gt;내부적으로 &lt;b&gt;@Configuration&lt;/b&gt;을 상속&lt;/li&gt;
&lt;li data-end=&quot;241&quot; data-start=&quot;213&quot;&gt;스프링 컨테이너에 &lt;b&gt;설정 클래스&lt;/b&gt;임을 알려줌&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;246&quot; data-start=&quot;243&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;284&quot; data-start=&quot;248&quot; data-ke-size=&quot;size23&quot;&gt;2️⃣ &lt;b&gt;@EnableAutoConfiguration&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;400&quot; data-start=&quot;285&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;317&quot; data-start=&quot;285&quot;&gt;스프링이 자동으로 필요한 Bean 들을 설정해주는 기능&lt;/li&gt;
&lt;li data-end=&quot;377&quot; data-start=&quot;318&quot;&gt;예:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;377&quot; data-start=&quot;325&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;339&quot; data-start=&quot;325&quot;&gt;Tomcat 자동 설정&lt;/li&gt;
&lt;li data-end=&quot;360&quot; data-start=&quot;342&quot;&gt;DataSource 자동 설정&lt;/li&gt;
&lt;li data-end=&quot;377&quot; data-start=&quot;363&quot;&gt;MVC 설정 자동 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;400&quot; data-start=&quot;378&quot;&gt;스프링 부트 &amp;ldquo;자동 설정&amp;rdquo; 핵심 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;405&quot; data-start=&quot;402&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;433&quot; data-start=&quot;407&quot; data-ke-size=&quot;size23&quot;&gt;3️⃣ &lt;b&gt;@ComponentScan&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;528&quot; data-start=&quot;434&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;528&quot; data-start=&quot;434&quot;&gt;현재 패키지를 기준으로 &lt;b&gt;하위 패키지 전체에서 Bean(@Component, @Service, @Repository,&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❶ 로그인 POST + Redirect 동작 이해&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 실습에서 작성한 &lt;code&gt;@PostMapping(&quot;/login&quot;)&lt;/code&gt; 코드이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@PostMapping(&quot;/login&quot;)
public String login(@RequestParam(&quot;username&quot;) String username,
                    @RequestParam(&quot;password&quot;) String password) {

    System.out.println(username);
    System.out.println(password);

    // return &quot;main.html&quot;; // &amp;larr; 오류 남 (POST를 static HTML로 forward 시도)
    return &quot;redirect:main.html&quot;; // &amp;larr; 정상 동작
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✔ 왜 return &quot;main.html&quot; 은 에러가 발생했는가?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;main.html은 static 폴더에 있고&lt;/b&gt;, static 리소스 제공자는 &lt;b&gt;GET/HEAD만 처리한다.&lt;/b&gt;&lt;br /&gt;POST 요청으로 static HTML을 열려고 시도하면서 다음 오류가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Request method 'POST' is not supported&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;static 폴더의 HTML은 POST 방식으로 forward 할 수 없다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✔ redirect를 사용해야 하는 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;return &quot;redirect:main.html&quot;&lt;/code&gt; 은 내부 forward가 아니라 &lt;b&gt;302 Redirect 응답&lt;/b&gt;이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;브라우저에게 &lt;i&gt;&amp;ldquo;/main.html 로 다시 GET 요청해라&amp;rdquo;&lt;/i&gt; 라고 지시&lt;/li&gt;
&lt;li&gt;브라우저는 &lt;b&gt;GET /main.html&lt;/b&gt; 요청 수행&lt;/li&gt;
&lt;li&gt;static 핸들러가 정상적으로 main.html을 제공&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 웹의 고전적 패턴인 &lt;b&gt;PRG (Post-Redirect-Get)&lt;/b&gt; 과 동일하며, 새로고침 시 중복 POST도 방지한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❷ @RestController 실습 &amp;ndash; JSON/문자 응답 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 실습에서는 Spring MVC에서 뷰를 반환하지 않고 데이터를 직접 반환하는 &lt;code&gt;@RestController&lt;/code&gt; 를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
public class JsonController {

    @GetMapping(&quot;/string&quot;)
    public String m1() {
        System.out.println(&quot;/string&quot;);
        return &quot;안녕하세요!&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✔ @RestController 의 의미&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@RestController = @Controller + @ResponseBody&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 리턴값이 &lt;b&gt;뷰 이름이 아니라&lt;/b&gt; HTTP 응답 바디로 나간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;return &quot;안녕하세요!&quot;&lt;/code&gt; 는 그대로 브라우저에서 문자열이 출력된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트(Vue/React)와 통신할 때 사용하는 모든 API는 이런 방식으로 만들어지며, JSON 객체도 자동 변환되어 응답된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❸ CORS 전역 설정 실습&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘의 마지막 개념은 &lt;b&gt;CORS (Cross-Origin Resource Sharing)&lt;/b&gt; 이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트가 localhost:3000, 백엔드가 localhost:8080이면 &lt;b&gt;두 도메인은 서로 다른 Origin&lt;/b&gt;이다.&lt;br /&gt;브라우저는 보안상 이 요청을 기본적으로 차단하므로 &lt;b&gt;서버가 허용해주도록 설정해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습에서 사용한 전역 CORS 설정 예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping(&quot;/**&quot;)
                .allowedOrigins(&quot;http://localhost:3000&quot;)
                .allowedMethods(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;)
                .allowedHeaders(&quot;*&quot;)
                .allowCredentials(true);
    }
}
&lt;/code&gt;&lt;/pre&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;우리 서버의 모든 URL(&quot;/**&quot;) 에 대해&lt;/li&gt;
&lt;li&gt;React 개발 서버(3000)에서 오는 요청을 허용&lt;/li&gt;
&lt;li&gt;GET/POST 등 모든 주요 메서드 허용&lt;/li&gt;
&lt;li&gt;쿠키/세션/인증 정보도 허용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약하면, &lt;b&gt;프론트 &amp;rarr; 백엔드 API 호출이 가능하도록 브라우저에서 막는 보안 정책을 서버가 허용해주는 것&lt;/b&gt;이다.&lt;/p&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;1200&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YTqm7/dJMcabQaalQ/5ai1xXc0MhcHIu9ifbJCX1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YTqm7/dJMcabQaalQ/5ai1xXc0MhcHIu9ifbJCX1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YTqm7/dJMcabQaalQ/5ai1xXc0MhcHIu9ifbJCX1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYTqm7%2FdJMcabQaalQ%2F5ai1xXc0MhcHIu9ifbJCX1%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;1200&quot; height=&quot;675&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❹ 오늘 실습 전체 흐름 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 배운 내용을 간단하게 전체 흐름으로 묶으면 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인 화면에서 &lt;b&gt;POST /login&lt;/b&gt; 요청 전송&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@PostMapping&lt;/code&gt;에서 파라미터 받음&lt;/li&gt;
&lt;li&gt;static HTML을 바로 열 수 없기 때문에 &lt;b&gt;redirect 처리&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;API가 필요한 경우 &lt;b&gt;@RestController&lt;/b&gt; 로 JSON/문자 반환&lt;/li&gt;
&lt;li&gt;프론트와 실제로 연동하려면 &lt;b&gt;CORS&lt;/b&gt; 설정 필수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;&amp;ldquo;Spring MVC 기본 구조 &amp;rarr; 화면 이동 흐름 &amp;rarr; API 방식 &amp;rarr; CORS 허용&amp;rdquo;&lt;/b&gt; 의 순서로 오늘의 모든 실습은 서로 연결되어 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❺ 개념 정리 (CS + Spring 핵심)&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;Forward&lt;/b&gt;: 서버 내부에서 다른 페이지로 연결 (URL 안 바뀜)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Redirect&lt;/b&gt;: 브라우저에게 새 GET 요청 보내도록 지시 (URL 바뀜)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@RestController&lt;/b&gt;: 데이터(JSON/문자)를 그대로 응답하는 컨트롤러&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JSON&lt;/b&gt;: 프론트&amp;ndash;백엔드 통신 표준 포맷&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CORS&lt;/b&gt;: 브라우저가 다른 Origin 요청을 차단하는 보안 정책&lt;/li&gt;
&lt;li&gt;&lt;b&gt;static 리소스&lt;/b&gt;: GET 방식만 제공하는 정적 HTML/CSS/JS 파일&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PRG 패턴&lt;/b&gt;: POST &amp;rarr; Redirect &amp;rarr; GET 흐름&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 24일 실습은 Spring 기본기 중에서 &lt;b&gt;가장 핵심적인 흐름&lt;/b&gt;을 전부 손으로 직접 해본 중요한 수업이었다.&lt;br /&gt;특히 redirect 문제와 RestController/CORS 개념은 나중에 React/Spring 연동할 때 그대로 쓰이기 때문에 꼭 정확히 이해해두면 좋다.&lt;/p&gt;</description>
      <category>Java/Spring</category>
      <author>Heyjinu_</author>
      <guid isPermaLink="true">https://norang2810.tistory.com/116</guid>
      <comments>https://norang2810.tistory.com/116#entry116comment</comments>
      <pubDate>Tue, 25 Nov 2025 14:10:13 +0900</pubDate>
    </item>
    <item>
      <title>[LG U+ 유레카 3기] JPA N+1 &amp;middot; Fetch Join &amp;middot; JPQL Join</title>
      <link>https://norang2810.tistory.com/115</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 JPA를 사용할 때 반드시 알아야 하는 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;N+1 문제&lt;/span&gt;와 이를 해결하는 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;fetch join&lt;/span&gt;, 그리고 &lt;code&gt;JPQL Join&lt;/code&gt; 문법을 실습 중심으로 정리한 날이다.&lt;br /&gt;실제 Hibernate 로그가 어떻게 출력되는지 직접 보면서 이해하는 &amp;ldquo;핵심 실습&amp;rdquo;이었기 때문에 내용을 정확히 정리해두면 향후 프로젝트/JPA 최적화에 큰 도움이 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❶ N+1 문제란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 엔티티 목록을 조회하는 SELECT(1) 후, 각 엔티티가 가지고 있는 &lt;b&gt;연관 엔티티를 N번 추가로 조회&lt;/b&gt;하며 발생하는 비효율이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;String jpql = &quot;select o from Orders o&quot;;
List&amp;lt;Orders&amp;gt; list = em.createQuery(jpql, Orders.class)
                       .getResultList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate 로그는 아래처럼 찍힌다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 1) Orders 목록 조회
select * from orders;

# 2) Orders 각각에 대해 Product/Customer 즉시 로딩(EAGER)
select * from product where id=?
select * from customer where id=?
select * from product where id=?
select * from customer where id=?
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &lt;b&gt;1번 + N번 = 총 N+1개의 SQL&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA의 연관 관계 기본 로딩 전략 때문.&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;LAZY&lt;/b&gt; &amp;rarr; &amp;ldquo;필요한 순간 조회&amp;rdquo; &amp;rarr; 목록 조회 시 거의 100% N+1&lt;/li&gt;
&lt;li&gt;&lt;b&gt;EAGER&lt;/b&gt; &amp;rarr; &amp;ldquo;즉시 조회&amp;rdquo;이지만 &lt;b&gt;JOIN을 강제하는 게 아니라&lt;/b&gt; 엔티티 로딩 순간 추가 SELECT를 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 EAGER도 목록 조회에서는 N+1이 발생한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❸ 해결 방법: Fetch Join&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 해결책은 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;join fetch&lt;/span&gt; 한 줄이다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;String jpql = 
&quot;select o from Orders o 
 join fetch o.customer 
 join fetch o.product&quot;;

List&amp;lt;Orders&amp;gt; list = em.createQuery(jpql, Orders.class).getResultList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate 출력:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;select o.*, c.*, p.*
from orders o
join customer c on o.customer_id = c.id
join product p on o.product_id = p.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;rarr; 단 1번의 SELECT로 Orders + Product + Customer를 모두 로딩&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&amp;rarr; N+1 완벽 제거&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;1400&quot; data-origin-height=&quot;593&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xsFJI/dJMcafSxmJM/EWJOMPaFKay5KEHT2OOkK0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xsFJI/dJMcafSxmJM/EWJOMPaFKay5KEHT2OOkK0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xsFJI/dJMcafSxmJM/EWJOMPaFKay5KEHT2OOkK0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxsFJI%2FdJMcafSxmJM%2FEWJOMPaFKay5KEHT2OOkK0%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;1400&quot; height=&quot;593&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;593&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❹ JPQL Join 문법 2가지 방식&lt;/h3&gt;
&lt;h4 style=&quot;margin-top: 20px;&quot; data-ke-size=&quot;size20&quot;&gt;1) 현대적인 join 문법 (추천)&lt;/h4&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;select o, p
from Orders o
join o.product p
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계 필드를 기준으로 JOIN하는 &lt;b&gt;정석 JPQL&lt;/b&gt; 문법이다.&lt;/p&gt;
&lt;h4 style=&quot;margin-top: 20px;&quot; data-ke-size=&quot;size20&quot;&gt;2) 전통 스타일 (SQL 스타일)&lt;/h4&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;select o, p 
from Orders o, Product p 
where o.product = p
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 SQL의 고전 조인 방식이며 내부적으로는 아래와 동일한 의미이다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;from orders o
cross join product p
where o.product_id = p.id
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 INNER JOIN처럼 동작한다.&lt;br /&gt;하지만 실무/강의에서는 &lt;b&gt;관계 필드 기반 JOIN&lt;/b&gt;을 훨씬 더 많이 사용한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❺ 엔티티 두 개를 동시에 Select하는 경우&lt;/h3&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;select o, p 
from Orders o 
join o.product p
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 타입은 무조건 &lt;code&gt;List&amp;lt;Object[]&amp;gt;&lt;/code&gt; 형태.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;for(Object[] row : result){
    Orders o = (Orders) row[0];
    Product p = (Product) row[1];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❻ UnknownEntityException 원인 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA/Hibernate 수동 환경에서는 엔티티를 직접 등록해야 한다.&lt;br /&gt;&lt;code&gt;MyPersistenceUnitInfo&lt;/code&gt; 안에 아래가 반드시 있어야 한다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;@Override
public List&amp;lt;String&amp;gt; getManagedClassNames() {
    return List.of(
        &quot;entity.Orders&quot;,
        &quot;entity.Customer&quot;,
        &quot;entity.Product&quot;
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록을 빠뜨리면 Hibernate는 엔티티를 모른다 &amp;rarr; &lt;b&gt;UnknownEntityException: Could not resolve root entity &amp;lsquo;Orders&amp;rsquo;&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 사용할 때 반드시 기억해야 하는 3줄 결론:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1) 목록 조회 시는 무조건 &lt;b&gt;LAZY + fetch join&lt;/b&gt; 조합&lt;/li&gt;
&lt;li&gt;2) EAGER는 예상보다 더 쉽게 N+1을 유발&lt;/li&gt;
&lt;li&gt;3) JPQL join은 &lt;b&gt;join o.product&lt;/b&gt; 형태가 가장 안전하고 정석&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 내용은 JPA 성능 최적화의 핵심이기 때문에 나중에 스프링 부트 프로젝트에서도 그대로 적용된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate 로그를 직접 확인하며 N+1 &amp;rarr; fetch join &amp;rarr; join 문법을 모두 체득한 하루였다.&lt;br /&gt;이제부터 JPQL 목록 조회가 보이면 자동으로 &amp;ldquo;여기 fetch join 필요하겠는데?&amp;rdquo; 하고 감이 올 것이다.&lt;/p&gt;</description>
      <category>Java/JPA</category>
      <author>Heyjinu_</author>
      <guid isPermaLink="true">https://norang2810.tistory.com/115</guid>
      <comments>https://norang2810.tistory.com/115#entry115comment</comments>
      <pubDate>Thu, 20 Nov 2025 16:33:19 +0900</pubDate>
    </item>
    <item>
      <title>[LG U+ 유레카 3기] JPA 연관관계 &amp;amp; Fetch 전략</title>
      <link>https://norang2810.tistory.com/114</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;[LG U+ 유레카 3기] &quot;JPA 연관관계 &amp;amp; Fetch 전략(OneToMany / ManyToMany) 실습 정리&quot;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JPA 연관관계 &amp;amp; Fetch 전략 (어제+오늘 실습 총정리)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❶ 실습 환경 &amp;amp; 공통 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 이틀 동안의 실습은 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;순수 JPA + Hibernate&lt;/span&gt; 조합으로 진행했다.&lt;br /&gt;Spring 없이 직접 &lt;code&gt;EntityManagerFactory&lt;/code&gt;와 &lt;code&gt;EntityManager&lt;/code&gt;를 만들면서 내부 동작을 눈으로 확인하는 것이 목표였다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;
public class TestTemplate {

    public static void main(String[] args) {

        Map&amp;lt;String, String&amp;gt; props = new HashMap&amp;lt;&amp;gt;();
        props.put(&quot;hibernate.hbm2ddl.auto&quot;, &quot;update&quot;);
        props.put(&quot;hibernate.show_sql&quot;, &quot;true&quot;);

        EntityManagerFactory emf = new HibernatePersistenceProvider()
                .createContainerEntityManagerFactory(new MyPersistenceUnitInfo(), props);

        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();

        // 여기서 각종 실습 코드 실행

        em.getTransaction().commit();
        em.close();
    }
}
&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;MyPersistenceUnitInfo&lt;/code&gt; : persistence.xml 대신 자바 코드로 설정을 넘겨주는 역할&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hibernate.show_sql=true&lt;/code&gt; : JPA 코드 한 줄이 어떤 SQL로 바뀌는지 &lt;b&gt;눈으로 확인&lt;/b&gt;&amp;lt;/음&amp;gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hibernate.hbm2ddl.auto=update&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;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;425&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wISPu/dJMcajm2BSo/wllA1NKPD4WKbUvFOnpe0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wISPu/dJMcajm2BSo/wllA1NKPD4WKbUvFOnpe0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wISPu/dJMcajm2BSo/wllA1NKPD4WKbUvFOnpe0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwISPu%2FdJMcajm2BSo%2FwllA1NKPD4WKbUvFOnpe0K%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;648&quot; height=&quot;425&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;425&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❷ OneToMany(Post &amp;harr; Comment) 연관관계 &amp;amp; Fetch 전략&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 엔티티 구조&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게시글(Post) 하나에 댓글(Comment) 여러 개가 달리는 아주 익숙한 구조를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;
// Post.java
@Entity
public class Post {

    @Id @GeneratedValue
    private Integer id;

    private String title;
    private String content;

    @OneToMany(mappedBy = &quot;post&quot;, fetch = FetchType.LAZY)
    private List&amp;lt;Comment&amp;gt; comments = new ArrayList&amp;lt;&amp;gt;();

    // getters, setters, toString ...
}

// Comment.java
@Entity
public class Comment {

    @Id @GeneratedValue
    private Integer id;

    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    private Post post;

    // getters, setters, toString ...
}
&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;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Post : Comment = 1 : N&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@OneToMany(mappedBy = &quot;post&quot;)&lt;/code&gt; : &lt;b&gt;연관관계의 주인은 Comment&lt;/b&gt; (FK를 가진 쪽)&lt;/li&gt;
&lt;li&gt;양쪽 모두 FetchType 기본값은 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;LAZY&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) LAZY에서의 조회 흐름&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;
Post p = em.find(Post.class, 1);      // Post 한 건만 select
List&amp;lt;Comment&amp;gt; comments = p.getComments(); // 이 시점에는 select 없음

// 실제로 데이터를 사용할 때 비로소 select 발생
comments.forEach(System.out::println);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;find()&lt;/code&gt; 호출 시 댓글까지 전부 가져올 줄 알았지만, 콘솔을 보니 &lt;b&gt;Post만 select&lt;/b&gt;되고 댓글은 조회되지 않았다. 리스트를 실제로 출력하는 순간에야 Hibernate가 댓글을 가져오기 위한 SQL을 날리는 것을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;LAZY&lt;/span&gt;는 &amp;ldquo;&lt;b&gt;필요할 때까지 DB 접근을 미룬다&lt;/b&gt;&amp;rdquo; 라는 개념을 눈으로 확인한 실습이었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) EAGER로 바꿨을 때의 변화&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;
@OneToMany(mappedBy = &quot;post&quot;, fetch = FetchType.EAGER)
private List&amp;lt;Comment&amp;gt; comments;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;
Post p = em.find(Post.class, 1); // 이 한 줄에서 Post + Comment + 조인 테이블까지 한 번에 join
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FetchType을 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;EAGER&lt;/span&gt;로 바꾼 후에는 게시글 하나를 조회하는 순간, JPA가 Post, Comment, 조인 테이블을 한 번에 join 해서 가져왔다. &amp;ldquo;편해 보이지만, 댓글이 수천 개라면 매번 join 지옥&amp;rdquo;이 되기 때문에 실무에서는 거의 사용하지 않는다는 것을 체감했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) 컬렉션 변경 시 delete &amp;rarr; insert 폭탄의 이유&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;
Post p = em.find(Post.class, 1); // 기존 댓글 2개라고 가정

Comment c3 = new Comment();
c3.setContent(&quot;코멘트 3&quot;);
c3.setPost(p);

p.getComments().add(c3);
em.persist(c3);  // c3 영속화

// 트랜잭션 커밋 시점
em.getTransaction().commit();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋 시점에 로그를 확인해보면 다음과 같은 순서로 SQL이 실행된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;select&lt;/code&gt; : 기존 Post, Comment, 조인 테이블 조회&lt;/li&gt;
&lt;li&gt;&lt;code&gt;insert into Comment ...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delete from Post_Comment where post_id = ?&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;insert into Post_Comment (post_id, comments_id) values (?,?)&lt;/code&gt; 여러 번&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &amp;ldquo;댓글 하나 추가했는데 왜 전체 delete 후 insert를 다시 하지?&amp;rdquo; 라는 의문이 들었다.&lt;br /&gt;원인은 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Hibernate의 변경 감지(Dirty Checking)&lt;/span&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;스냅샷(snapshot)&lt;/b&gt;을 저장해 둔다.&lt;/li&gt;
&lt;li&gt;단순 필드(제목, 내용 등)는 이전 값과 지금 값을 비교해서 변경 여부를 쉽게 판단할 수 있다.&lt;/li&gt;
&lt;li&gt;하지만 &lt;b&gt;컬렉션(List&amp;lt;Comment&amp;gt;)&lt;/b&gt;은 &amp;ldquo;어떤 것이 추가/삭제/순서 변경되었는지&amp;rdquo; 비교 비용이 크다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Hibernate는 컬렉션에 대해서는 똑똑하게 diff를 계산하는 대신, &lt;br /&gt;&lt;b&gt;&amp;ldquo;그냥 다 지우고 지금 리스트 상태대로 다시 넣자&amp;rdquo;&lt;/b&gt; 라는 단순한 전략을 택한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 동작을 실제 SQL 로그로 확인하면서, 면접에서 자주 나오는 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;컬렉션 연관관계의 delete &amp;rarr; insert 패턴&lt;/span&gt;을 몸으로 이해하게 된 실습이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❸ ManyToMany(Team &amp;harr; User) 저장 실습 &amp;ndash; 연관관계의 주인 &amp;amp; Cascade&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 엔티티 구조&lt;/h4&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;
// Team.java
@Entity
public class Team {

    @Id @GeneratedValue
    private Integer id;

    private String name;

    @ManyToMany(/* 필요 시 cascade, fetch 등 옵션 */)
    private List&amp;lt;User&amp;gt; users = new ArrayList&amp;lt;&amp;gt;();
}

// User.java
@Entity
public class User {

    @Id @GeneratedValue
    private Integer id;

    private String name;

    @ManyToMany(mappedBy = &quot;users&quot;)
    private List&amp;lt;Team&amp;gt; teams = new ArrayList&amp;lt;&amp;gt;();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Team과 User 사이에는 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;다대다(N:N)&lt;/span&gt; 관계가 있고,&lt;br /&gt;DB에는 &lt;code&gt;teams&lt;/code&gt;, &lt;code&gt;users&lt;/code&gt;, &lt;code&gt;teams_users&lt;/code&gt; 3개의 테이블이 생성된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) Team &amp;harr; User를 어떻게 저장하느냐에 따른 결과 비교&lt;/h4&gt;
&lt;h5&gt;(1) Team과 User를 각각 따로 저장&lt;/h5&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;
User u1 = new User(); u1.setName(&quot;회원 1&quot;);
User u2 = new User(); u2.setName(&quot;회원 2&quot;);

Team t1 = new Team(); t1.setName(&quot;팀 1&quot;);
Team t2 = new Team(); t2.setName(&quot;팀 2&quot;);

em.persist(u1);
em.persist(u2);
em.persist(t1);
em.persist(t2);
&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;code&gt;users&lt;/code&gt; 테이블 : 2건 insert&lt;/li&gt;
&lt;li&gt;&lt;code&gt;teams&lt;/code&gt; 테이블 : 2건 insert&lt;/li&gt;
&lt;li&gt;&lt;code&gt;teams_users&lt;/code&gt; : 아무 것도 insert 안 됨 (연관관계를 아직 안 걸었기 때문)&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;(2) Team에 User 리스트를 연결 후, Team만 persist 했을 때&lt;/h5&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;
t1.setUsers(List.of(u1, u2));
t2.setUsers(List.of(u2));

em.persist(t1);
em.persist(t2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User가 아직 영속 상태가 아니기 때문에, &lt;b&gt;저장되지 않은(transient) 객체를 참조한다는 예외&lt;/b&gt;가 발생한다.&lt;/p&gt;
&lt;h5&gt;(3) Team에 User를 연결하고, Team과 User 모두 persist 했을 때&lt;/h5&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;
t1.setUsers(List.of(u1, u2));
t2.setUsers(List.of(u2));

em.persist(t1);
em.persist(t2);
em.persist(u1);
em.persist(u2);
&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;code&gt;teams&lt;/code&gt; insert 2건&lt;/li&gt;
&lt;li&gt;&lt;code&gt;users&lt;/code&gt; insert 2건&lt;/li&gt;
&lt;li&gt;&lt;code&gt;teams_users&lt;/code&gt; insert 3건 (t1-u1, t1-u2, t2-u2)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 배운 것: &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;다대다 관계에서는 조인 테이블 저장을 누가 담당하는지가 중요하다&lt;/span&gt;.&lt;/p&gt;
&lt;h5&gt;(4) User에 Team을 설정하고, User만 persist 했을 때&lt;/h5&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;
u1.setTeams(List.of(t1, t2));
u2.setTeams(List.of(t2));

em.persist(u1);
em.persist(u2);
&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;code&gt;users&lt;/code&gt; : insert 2건&lt;/li&gt;
&lt;li&gt;&lt;code&gt;teams&lt;/code&gt; : insert 없음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;teams_users&lt;/code&gt; : 역시 insert 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &amp;ldquo;User에서 Team을 걸어줬는데 왜 조인 테이블이 비어 있지?&amp;rdquo; 라는 의문이 들었지만, 원인은 &lt;b&gt;연관관계의 주인(Owning Side)&lt;/b&gt; 개념에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 실습에서는 Team 쪽이 주인이라고 가정했기 때문에,&lt;br /&gt;&lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;주인이 아닌 User에서 아무리 setTeams()를 해도 DB에는 반영되지 않는다&lt;/span&gt;.&lt;/p&gt;
&lt;h5&gt;(5) CascadeType.PERSIST를 사용한 Team 중심 저장&lt;/h5&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Team에 &lt;code&gt;cascade = CascadeType.PERSIST&lt;/code&gt;를 설정하고 다음 코드를 실행했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;
t1.setUsers(List.of(u1, u2));
t2.setUsers(List.of(u2));

em.persist(t1);
em.persist(t2); // cascade 덕분에 User도 함께 persist
&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;code&gt;teams&lt;/code&gt; : insert 2건&lt;/li&gt;
&lt;li&gt;&lt;code&gt;users&lt;/code&gt; : insert 2건 (cascade)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;teams_users&lt;/code&gt; : insert 3건&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;Cascade는 엔티티 저장 전파&lt;/span&gt;를 담당하고,&lt;br /&gt;조인 테이블에 무엇이 들어가는지는 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;연관관계의 주인&lt;/span&gt;이 결정한다는 것을 실습으로 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❹ ManyToMany 조회 &amp;amp; Fetch 전략 + toString() 순환 참조&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Team 한 건만 find 했을 때 (LAZY)&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;
Team t1 = em.find(Team.class, 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 로그를 보면 &lt;code&gt;teams&lt;/code&gt; 테이블만 select 되고, &lt;code&gt;teams_users&lt;/code&gt;와 &lt;code&gt;users&lt;/code&gt;는 조회되지 않는다. ManyToMany의 기본 fetch가 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;LAZY&lt;/span&gt;라는 것을 다시 한 번 확인한 부분이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) Team을 출력하면서 toString() 호출 &amp;ndash; StackOverflowError&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;
Team t1 = em.find(Team.class, 1);
System.out.println(t1); // toString() 체인 호출
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Team의 &lt;code&gt;toString()&lt;/code&gt;에서 &lt;code&gt;users&lt;/code&gt;를 출력하고,&lt;br /&gt;User의 &lt;code&gt;toString()&lt;/code&gt;에서 다시 &lt;code&gt;teams&lt;/code&gt;를 출력하도록 구현되어 있으면, 아래와 같은 순환이 만들어진다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Team.toString() &amp;rarr; users 출력&lt;/li&gt;
&lt;li&gt;User.toString() &amp;rarr; teams 출력&lt;/li&gt;
&lt;li&gt;다시 Team.toString() &amp;rarr; users 출력&lt;/li&gt;
&lt;li&gt;&amp;hellip; 무한 반복 &amp;rarr; &lt;b&gt;StackOverflowError&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 실행 결과, 연관된 Team과 User를 번갈아가며 select 하다가 스택이 꽉 차서 에러가 발생했다.&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;toString()에서 연관 필드는 빼거나&lt;/li&gt;
&lt;li&gt;Lombok 사용 시 &lt;code&gt;@ToString.Exclude&lt;/code&gt; 적용&lt;/li&gt;
&lt;li&gt;JSON 변환 시에는 &lt;code&gt;@JsonIgnore&lt;/code&gt; 등으로 순환 참조 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) User에서 teams를 사용할 때의 쿼리 흐름 (LAZY)&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;
User u1 = em.find(User.class, 1);      // users 한 건만 select
u1.getTeams().forEach(System.out::println); // 이 시점에 teams, users 추가 select
&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;user 1건 조회&lt;/li&gt;
&lt;li&gt;해당 user가 속한 team 목록을 가져오기 위해 &lt;code&gt;teams_users&lt;/code&gt; + &lt;code&gt;teams&lt;/code&gt; join&lt;/li&gt;
&lt;li&gt;각 team의 toString() 안에서 users를 출력하면 다시 &lt;code&gt;teams_users + users&lt;/code&gt; 조회&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, LAZY를 잘못 쓰면 이런 식으로 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;N+1 문제가 자연스럽게 발생&lt;/span&gt;한다는 것을 몸으로 느낀 실습이었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) Team의 FetchType을 EAGER로 바꿨을 때&lt;/h4&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;
@ManyToMany(fetch = FetchType.EAGER)
private List&amp;lt;User&amp;gt; users;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;
Team t1 = em.find(Team.class, 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 Team 한 건을 조회하는 순간, &lt;code&gt;teams&lt;/code&gt;, &lt;code&gt;teams_users&lt;/code&gt;, &lt;code&gt;users&lt;/code&gt;를 모두 left join 해서 가져왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;조회 한 번에 다 가져와서 편해 보이지만, join 폭탄이 될 수 있다&amp;rdquo;는 점 때문에&lt;br /&gt;실무에서는 기본적으로 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;LAZY + 필요할 때 fetch join 사용&lt;/span&gt;이 정석이라는 것도 다시 정리했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;❺ hbm2ddl.auto 옵션 실습 &amp;ndash; create vs update&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Product 예제에서 JPQL을 사용하기 전에, 미리 MySQL에서 데이터를 insert 해 두고 실행했는데 콘솔에 아무 것도 찍히지 않는 상황을 경험했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 보니 다음과 같은 SQL이 실행되고 있었다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;
drop table if exists Product;
create table Product (...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 설정값이 &lt;code&gt;hibernate.hbm2ddl.auto = create&lt;/code&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;code&gt;create&lt;/code&gt; : 기존 테이블을 &lt;b&gt;무조건 drop 후 다시 생성&lt;/b&gt; &amp;rarr; 사전에 넣어둔 데이터가 모두 사라짐&lt;/li&gt;
&lt;li&gt;&lt;code&gt;update&lt;/code&gt; : 스키마를 변경할 필요가 있을 때만 alter, 데이터는 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 실습용 DB에서 미리 데이터를 넣어두고 JPA로 조회하고 싶다면, 반드시 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;update&lt;/span&gt;로 설정해 두어야 한다는 점을 알게 되었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;연관관계 &amp;amp; Fetch 전략&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OneToMany, ManyToMany 모두 기본 fetch는 &lt;span style=&quot;background-color: orange; color: white; border-radius: 4px; padding: 2px 5px;&quot;&gt;LAZY&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;EAGER는 편해 보이지만 join 폭탄 &amp;rarr; 실무에서는 지양&lt;/li&gt;
&lt;li&gt;컬렉션 연관관계는 dirty checking 시 &lt;b&gt;delete &amp;rarr; insert 패턴&lt;/b&gt;이 자주 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;연관관계의 주인(Owning Side)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ManyToMany에서 조인 테이블에 무엇이 저장될지는 &lt;b&gt;주인 엔티티에 달려 있다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Non-owner 쪽에서 &lt;code&gt;setTeams()&lt;/code&gt;를 해도 DB에는 반영되지 않는다&lt;/li&gt;
&lt;li&gt;CascadeType.PERSIST는 엔티티 저장을 전파할 뿐, 주인이 아닌 쪽의 연관관계를 대신 저장해주지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;toString(), LAZY, N+1 문제&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;양방향 연관관계에서 서로를 toString()에 넣으면 &lt;b&gt;StackOverflowError&lt;/b&gt; 발생&lt;/li&gt;
&lt;li&gt;LAZY는 &amp;ldquo;필요할 때만 DB 가기&amp;rdquo;라서 좋지만, 부주의하면 N+1 문제를 낳는다&lt;/li&gt;
&lt;li&gt;엔티티 설계 시 toString(), JSON 직렬화, fetch 전략을 함께 고민해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DDL 자동 생성 옵션&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;create&lt;/code&gt;는 연습용/초기 개발 단계에서만 사용 (데이터 날아감)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;update&lt;/code&gt;를 이용하면 기존 데이터를 유지하면서 구조만 맞춰갈 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 이틀 동안의 실습은 단순히 코드를 따라 치는 수준을 넘어서, &amp;ldquo;JPA가 내부에서 어떤 전략으로 동작하는지&amp;rdquo;를 직접 SQL 로그로 체험하면서 이해하는 시간이었다.&lt;br /&gt;특히 연관관계의 주인, LAZY/EAGER, dirty checking, cascade 같은 개념은 앞으로 Spring + JPA로 프로젝트를 진행할 때 성능과 안정성에 큰 영향을 주는 핵심 포인트들이라, 이번 정리를 통해 머릿속에 한 번 더 깊게 박아두는 계기가 되었다.&lt;/p&gt;</description>
      <category>Java/JSP</category>
      <author>Heyjinu_</author>
      <guid isPermaLink="true">https://norang2810.tistory.com/114</guid>
      <comments>https://norang2810.tistory.com/114#entry114comment</comments>
      <pubDate>Wed, 19 Nov 2025 22:48:17 +0900</pubDate>
    </item>
  </channel>
</rss>