<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>summeryoung_.log</title>
        <link>https://velog.io/</link>
        <description>이불 밖은 위험해.</description>
        <lastBuildDate>Thu, 03 Sep 2026 15:20:43 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>summeryoung_.log</title>
            <url>https://velog.velcdn.com/images/summeryoung_/profile/684b3081-fdf1-41d6-8794-3f342a24f852/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. summeryoung_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/summeryoung_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 15주차 스터디 (2)]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-15%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94-2</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-15%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94-2</guid>
            <pubDate>Thu, 03 Sep 2026 15:20:43 GMT</pubDate>
            <description><![CDATA[<h1 id="16장-트랜잭션과-락-2차-캐시">16장. 트랜잭션과 락 2차 캐시</h1>
<h2 id="161-트랜잭션과-락">16.1 트랜잭션과 락</h2>
<p>목표: 트랜잭션의 기초와 JPA가 제공하는 낙관적 락과 비관적 락</p>
<h3 id="1-트랜잭션과-격리-수준">(1) 트랜잭션과 격리 수준</h3>
<p>트랜잭션: ACID라 하는 원자성, 일관성, 격리성, 지속성을 보장해야함</p>
<h4 id="acid">ACID</h4>
<ul>
<li><p><strong>원자성</strong>: 트랜잭션 내에서 실행한 작업들은 마치 <strong>하나의 작업인 것처럼 모두 성공하든가 모든 실패해야함</strong></p>
</li>
<li><p><strong>일관성</strong>: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야함.</p>
<p>  예) 데이터베이스에서 정한 무결성 제약 조건을 항상 만족</p>
</li>
<li><p><strong>격리성</strong>: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리함. 동시성과 관련된 성능이슈로 인해 격리수준을 선택할 수 있음</p>
<p>  예) 동시에 같은 데이터 수정 방지.</p>
</li>
<li><p>지속성: 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야함. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해 성공한 트랜잭션 내용을 복구해야함</p>
</li>
</ul>
<p>트랜잭션에서 격리성을 완벽히 보장하기 위해서는 트랜잭션을 거의 차례대로 진행해야함</p>
<p>→ 단점: <strong>동시성 처리 성능</strong>이 매우 나빠짐</p>
<p>→ 따라서 ANSI 표준에서는 트랜잭션의 격리 수준을 4단계로 나누어 정의함.</p>
<h4 id="트랜잭션의-격리수준">트랜잭션의 격리수준</h4>
<ul>
<li>READ UNCOMMITED (커밋되지 않은 읽기)</li>
<li>READ COMMITED (커밋된 읽기)</li>
<li>REPEATABLE READ (반복 가능한 읽기)</li>
<li>SERIALIZABLE (직렬화 가능)</li>
</ul>
<p>READ UNCOMMITED의 격리수준이 가장 낮고, SERIALIZABLE의 격리 수준이 가장 높음.</p>
<p>격리 수준이 낮을 수록 동시성을 증가하지만 다양한 문제 발생 가능.</p>
<table>
<thead>
<tr>
<th>격리수준</th>
<th>DIRTY READ</th>
<th>NON-REPEATABLE READ</th>
<th>PHANTOM READ</th>
</tr>
</thead>
<tbody><tr>
<td>READ UNCOMMITED</td>
<td>O</td>
<td>O</td>
<td>O</td>
</tr>
<tr>
<td>READ COMMITED</td>
<td></td>
<td>O</td>
<td>O</td>
</tr>
<tr>
<td>REPEATABLE READ</td>
<td></td>
<td></td>
<td>O</td>
</tr>
<tr>
<td>SERIALIZABLE</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>- 격리수준에 따른 문제점</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>- DIRTY READ</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>- NON-REPEATABLE READ (반복 불가능한 읽기)</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>- PHANTOM READ</td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<h4 id="1-read-uncommited">1-READ UNCOMMITED</h4>
<ul>
<li>커밋하지 않은 데이터를 읽을 수 있음</li>
<li>DIRTY READ<ul>
<li>예) 트랜잭션1이 데이터를 수정 중이고 커밋X → 트랜잭션2가 수정 중인 데이터 조회</li>
<li>문제점: 트랜잭션2가 DIRTY READ한 데이터 사용 중 트랜잭션1이 롤백되면 데이터 정합성에 문제</li>
</ul>
</li>
<li>DIRTY READ를 허용하는 격리수준을 READ UNCOMMITED라함.</li>
</ul>
<h4 id="2-read-commited">2-READ COMMITED</h4>
<ul>
<li>커밋한 데이터만 읽을 수 있음 → 즉, DIRTY READ는 발생하지 않음</li>
<li>다만, NON-REPEATABLE READ는 발생할 수 있음<ul>
<li>예) 트랜잭션1이 회원 A를 조회 중 → 갑자기 트랜잭션2가 회원A를 수정 후 커밋 →  트랜잭션1이 다시 회원을 조회했을 때 수정된 데이터가 조회됨.</li>
<li>이렇게 “<strong>반복해서 같은 데이터를 읽을 수 없는 상태</strong>”를 NON-REPEATABLE READ라함.</li>
</ul>
</li>
<li>DIRTY READ는 허용하지 않지만, NON-REPEATABLE READ는 허용하는 격리수준.</li>
</ul>
<h4 id="3-repeatable-read">3-REPEATABLE READ</h4>
<ul>
<li>한 번 조회한 데이터를 반복해 조회해도 같은 데이터가 조회됨 → 하지만 PHANTOM READ는 발생할 수 있음</li>
<li>PHANTOM READ<ul>
<li>예) 트랜잭션1 10살 이하의 회원 조회 → 트랜잭션2가 5살 회원을 추가하고 커밋 → 트랜잭션1이 10살 이하의 회원을 조회했을 때, 회원 하나가 추가된 상태로 조회</li>
<li>이렇게 “<strong>반복 조회 시 결과 집합이 달라지는 것</strong>”을 PHANTOM READ라함.</li>
</ul>
</li>
<li>NON-REPEATABLE READ는 허용하지 않지만, PHANTOM READ는 허용하는 격리수준.</li>
</ul>
<h4 id="4-serializable">4-SERIALIZABLE</h4>
<ul>
<li>가장 엄격한 트랜잭션 격리수준으로 PHANTOM READ도 발생하지 않음</li>
<li>동시성 처리 능력이 급격히 떨어질 수 있음</li>
</ul>
<p>애플리케이션은 대부분 동시성 처리가 중요하기에 데이터베이스들은 보통 READ COMMITED 격리수준을 기본으로 사용함. 더 높은 수준의 격리 수준이 일부 비즈니스 로직에 필요하면, 데이터베이스 트랜잭션이 제공하는 잠금 기능을 제공을 사용.</p>
<hr>
<h3 id="2-낙관적-락과-비관적-락-기초">(2) 낙관적 락과 비관적 락 기초</h3>
<p>JPA는 READ COMMITED로 트랜잭션 격리 수준을 가정. 더 높은 격리 수준일 필요할 때에는 낙관적 락 또는 비관적 락 중 하나를 사용</p>
<h4 id="낙관적-락">낙관적 락</h4>
<ul>
<li>트랜잭션 대부분은 충돌이 발생하지 않는다고 낙관적으로 가정하는 방법</li>
<li>데이터베이스가 제공하는 락 기능이 아닌 JPA가 제공하는 버전 관리 기능을 사용함 (=즉, 애플리케이션이 제공하는 락)</li>
<li>트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없음</li>
</ul>
<h4 id="비관적-락">비관적 락</h4>
<ul>
<li>트랜잭션의 충돌이 발생한다고 가정하고 우선 락을 걸고 보는 방법</li>
<li>데이터베이스가 제공하는 락 기능을 사용</li>
<li>대표적으로는 <code>select for update</code> 구문 존재</li>
</ul>
<h4 id="두-번의-갱신-분실-문제">두 번의 갱신 분실 문제</h4>
<ul>
<li><p>데이터베이스 트랜잭션 범위를 넘어서는 문제 역시 존재.</p>
<p>  예) 사용자 A와 B가 모두 제목이 같은 공지사항 수정 → A가 먼저 완료 버튼을 누른 후, B가 완료 버튼 누름 </p>
<pre><code>   → 결과: 먼저 완료를 누른 A의 결과는 사라지고 B의 결과만 남음.</code></pre></li>
<li><p>3가지 해결방법 선택 가능:</p>
<ul>
<li><p>마지막 커밋만 인정</p>
</li>
<li><p>최초 커밋만 인정</p>
</li>
<li><p>충돌하는 갱신 내용 병합</p>
<p>⇒ 기본적으로는 마지막 커밋만 인정하는 것이 적용됨. 다만, 이는 상황마다 3가지 중 적합한 것이 변화하며 JPA가 제공하는 버전 관리 기능을 사용하면 최초 커밋만 인정하는 것 역시 구현 가능함. 충돌 내용을 병합하기 위해서는 애플리케이션 개발자가 직접 병합 방법을 제공해야함.</p>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="3-version">(3) @Version</h3>
<p>JPA가 제공하는 낙관적 락 사용을 위해서 <code>@Version</code> 어노테이션을 사용해 버전 관리 기능을 추가해야함</p>
<p><code>@Version</code> 어노테이션 적용 가능 타입: Long, Integer, Short, Timestamp</p>
<pre><code class="language-java">@Entity
public class Board {

    @Id
    private String id;
    private String title;

    @Version
    private Integer version;
}</code></pre>
<ul>
<li><code>@Version</code> 어노테이션 추가 이후에는 <strong>엔티티 수정마다 버전이 하나씩 자동으로 추가</strong>됨.</li>
<li>만약 엔티티 수정 시, 조회 시점의 버전과 수정 시점의 버전이 다르면 예외 발생 → 따라서 <strong>버전 정보를 사용하면 최초 커밋만 인정하기가 적용</strong>됨.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/c1379cf3-59ed-4558-bd66-65e5a6704139/image.png" alt=""></p>
<h4 id="버전-정보-비교-방법">버전 정보 비교 방법</h4>
<ul>
<li>JPA가 버전 정보를 비교하는 방법:<ul>
<li>엔티티 수정 후 트랜잭션을 커밋 → 영속성 컨텍스트를 플러시하며 UPDATE 쿼리 실행. 이때 버전을 사용하는 엔티티의 경우 <strong>검색 조건에 엔티티의 버전 정보</strong>를 추가</li>
<li>데이터베이스 버전과 엔티티 버전이 같으면 데이터를 수정하며 동시에 버전도 하나를 증가.</li>
<li>만약, 데이터베이스 버전이 이미 증가해 수정 중인 엔티티의 버전과 다르면 UPDATE 쿼리의 WHERE문에서 VERSION 값이 다름 → 수정할 대상이 없음. 이 경우 JPA가 예외를 발생시킴.</li>
</ul>
</li>
</ul>
<p>⇒ <strong>“버전은 엔티티의 값을 변경하면 증가”</strong></p>
<ul>
<li><p>값 타입인 임베디드 타입과 값 타입 컬렉션 → 논리적 개념상 해당 엔티티의 값이기에 수정하면 엔티티의 버전 증가</p>
</li>
<li><p>연관관계 필드: 외래키를 관리하는 연관관계 주인 필드를 수정할 때만 버전이 증가</p>
</li>
<li><p><code>@Version</code>으로 추가한 버전 관리 필드의 경우 JPA가 직접 관리하기에, 개발자가 임의로 수정해서는 안됨 (벌크 연산 제외)</p>
<ul>
<li><p>버전 값을 강제 증가하려면 특별한 락 옵션을 선택</p>
</li>
<li><p>) 벌크 연산은 버전을 무시. 벌크연산에서 버전을 증가하려면 버전 필드를 강제 증가시켜야함</p>
</li>
</ul>
</li>
</ul>
<hr>
<h3 id="4-jpa-락-사용">(4) JPA 락 사용</h3>
<p>JPA에서 제공하는 락은 다음 위치에 적용 가능</p>
<ul>
<li>EntityManager.lock(), EntityManager.find(), EntityManager.refresh()</li>
<li>Query.setLockMode()</li>
<li>@NamedQuery</li>
</ul>
<p>조회하면서 즉시 락을 걸 수도 있고, 필요 시에 락을 걸 수도 있음.</p>
<p>JPA에서 제공하는 락 옵션은 LockModeType에 정의되어있음.</p>
<hr>
<h3 id="5-jpa-낙관적-락">(5) JPA 낙관적 락</h3>
<ul>
<li>JPA가 제공하는 낙관적 락은 버전(<code>@Version</code>)을 사용함 → 낙관적 락은 트랜잭션을 커밋하는 시점에 충돌을 알 수 있음</li>
<li>낙관적 락에서 발생하는 예외:<ul>
<li>OptimisticLockException(JPA 예외)</li>
<li>StaleObjectStateException(하이버네이트 예외)</li>
<li>ObjectOptimisticLockingFailureException(스프링 예외 추상화)</li>
</ul>
</li>
</ul>
<p>락 옵션 없이 <code>@Version</code>만 있어도 낙관적 락이 적용됨.</p>
<h4 id="none">NONE</h4>
<ul>
<li>락 옵션을 적용하지 않아도 엔티티에 <code>@Version</code>이 적용된 필드만 있으면 낙관적 락이 적용됨<ul>
<li>용도: 조회한 엔티티를 수정할 때 다른 트랜잭션에 의해 변경(삭제)되지 않아야함. 조회 시점부터 수정 시점까지를 보장</li>
<li>동작: 엔티티 수정 시, 버전을 체크하며 버전을 증가함. 이때 데이터베이스의 버전값이 현재 버전이 아니면 예외 발생</li>
<li>이점: 두 번의 갱신 분실 문제를 예방</li>
</ul>
</li>
</ul>
<h4 id="optimistic">OPTIMISTIC</h4>
<ul>
<li><code>@Version</code>만 적용했을 때는 엔티티를 수정해야 버전을 체크하지만, 이 옵션을 추가하면 엔티티를 조회만 해도 버전을 체크함. 즉, <strong>한 번 조회한 엔티티는 트랜잭션 종료까지 다른 트랜잭션에서 변경하지 않음을 보장</strong>함<ul>
<li>용도: 조회한 엔티티는 트랜잭션이 끝날 때까지 다른 트랜잭션에 의해 변경되지 않아야함. 조회 시점부터 트랜잭션 종료까지 조회 엔티티가 변경되지 않음을 보장</li>
<li>동작: 트랜잭션 커밋 시 버전 정보를 조회해 현재 엔티티의 버전과 같은지 검증. 같지 않을 경우 예외</li>
<li>이점: DIRTY READ와 NON-REPEATABLE READ를 방지</li>
</ul>
</li>
</ul>
<pre><code class="language-java">//트랜잭션1 조회 title=&quot;제목A&quot;, verison = 1
Board board = em.find(Board.class, id, LockModeType.OPTIMISTIC);

//중간에 트랜잭션2에서 해당 게시물 수정해 title = &quot;제목C&quot;, version=2로 증가

//트랜잭션 1 커밋 시점에 버전 정보 검증, 예외 발생
//(데이터베이스 version=2, 엔티티 version=1)
tx.commit();</code></pre>
<ul>
<li>OPTIMISTIC 락으로 조회했기에, 트랜잭션 커밋 시 데이터베이스에 있는 버전 정보를 <strong>SELECT 쿼리로 조회해 처음 조회한 엔티티의 버전 정보와 비교</strong>. 이때 다르면 예외발생</li>
</ul>
<h4 id="optimistic_force_increment">OPTIMISTIC_FORCE_INCREMENT</h4>
<ul>
<li>낙관적 락을 사용하면서 버전 정보를 강제 증가<ul>
<li>용도: 논리적인 단위의 엔티티 묶음을 관리할 수 있음.</li>
<li>예) 게시물 - 첨부파일이 일대다, 다대일의 양방향 연관관계.<ul>
<li>첨부파일이 연관관계의 주인.</li>
<li>게시물을 수정하는데 단순히 첨부파일만 추가하면 게시물의 버전은 증가X. 해당 게시물은 물리적으로 변경되지 않았지만, 논리적으로 변경됨</li>
<li>게시물의 버전을 강제로 증가시키기 위해서 사용</li>
</ul>
</li>
<li>동작: 엔티티를 수정하지 않아도 트랜잭션 커밋 시 UPDATE 쿼리를 사용해 버전 정보를 강제로 증가시킴. 이때 데이터베이스의 버전이 엔티티의 버전과 다르면 예외 발생.</li>
<li>이점: 강제로 버전을 증가해 논리적 단위의 엔티티 묶음을 버전 관리 가능</li>
</ul>
</li>
</ul>
<hr>
<h3 id="6-jpa-비관적-락">(6) JPA 비관적 락</h3>
<ul>
<li>비관적 락 → 데이터베이스 트랜잭션 락 메커니즘에 의존하는 방법.</li>
<li>주로 SQL 쿼리에 <code>select for update</code> 구문을 사용하며 시작, 버전 정보는 사용하지 않음</li>
<li>비관적 락은 주로 PESSIMISTIC_WRITE 모드를 사용함</li>
<li>비관적 락의 특징:<ul>
<li>엔티티가 아닌 스칼라 타입을 조회할 때도 사용 가능</li>
<li>데이터 수정 즉시 트랜잭션 충돌 감지 가능</li>
</ul>
</li>
<li>비관적 락에서 발생하는 예외:<ul>
<li>PersistenceLockException (JPA 예외)</li>
<li>PessimisticLockingFailureException (스프링 예외 추상화)</li>
</ul>
</li>
</ul>
<h4 id="pessimistic_write">PESSIMISTIC_WRITE</h4>
<p>일반적인 비관적 락의 옵션. 데이터베이스에 쓰기 락을 걸 때 사용</p>
<ul>
<li>용도: 데이터베이스에 쓰기 락을 검</li>
<li>동작: 데이터베이스 <code>select for update</code>를 사용해 락을 검</li>
<li>이점: NON-REPEATABLE READ를 방지. 락에 걸린 로우는 다른 트랜잭션이 수정 불가</li>
</ul>
<h4 id="pessimistic_read">PESSIMISTIC_READ</h4>
<p>데이터를 반복 읽기만 하고 수정하지 않는 용도로 락을 걸 때 사용. 일반적으로 잘 사용하지 않음.</p>
<ul>
<li>MySQL: lock in share mode</li>
<li>PostgreSQL: for share</li>
</ul>
<h4 id="pessimistic_force_increment">PESSIMISTIC_FORCE_INCREMENT</h4>
<p>비관적 락 중 유일하게 버전 정보를 사용함. 버전 정보를 강제로 증가시킴. 하이버네이트는 <code>nowait</code>를 지원하는 데이터베이스에 대해 <code>for update nowait</code> 옵션을 적용함</p>
<ul>
<li>오라클: for update nowait</li>
<li>PostgreSQL: for update nowait</li>
<li>nowait를 지원하지 않으면 for update가 사용됨</li>
</ul>
<hr>
<h3 id="7-비관적-락-타임아웃">(7) 비관적 락 타임아웃</h3>
<p>비관적 락을 사용하는 경우 락을 획득할 때까지 트랜잭션이 대기 → 무한정 기다릴 수 없기에 타임아웃 시간을 줌.</p>
<p>응답이 없는 경우 <code>LockTimeoutException</code>이 발생. (다만, 데이터베이스 특성에 따라 동작하지 않을 수 있음)</p>
<pre><code class="language-java">Map&lt;String, Object&gt;properties = new HashMap&lt;String,Object&gt;();

//타임아웃 10초까지 대기 설정
properties.put(&quot;timeout&quot;, 10000);

Board board = em.find(Board.class, &quot;boardId&quot;, LockModeType.PESSIMISTIC_WRITE,properties);</code></pre>
<hr>
<h2 id="162-2차-캐시">16.2 2차 캐시</h2>
<h3 id="1-1차-캐시와-2차-캐시">(1) 1차 캐시와 2차 캐시</h3>
<h4 id="1차-캐시">1차 캐시</h4>
<ul>
<li><p>영속성 컨텍스트 내부 엔티티를 보관하는 저장소</p>
</li>
<li><p>일반적인 웹 애플리케이션 환경은 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효</p>
<p>  → 애플리케이션 전체의 관점에서는 데이터베이스 접근 횟수를 획기적으로 줄이지 못함</p>
</li>
<li><p>엔티티 매니저로 조회/변경하는 모든 엔티티는 1차 캐시에 저장됨</p>
</li>
<li><p>트랜잭션 커밋 또는 플러시 호출 시 1차 캐시에 있는 엔티티의 변경 내역을 데이터베이스에 동기화</p>
</li>
<li><p>트랜잭션 시작 시 영속성 컨텍스트 생성 → 트랜잭션 종료 시</p>
</li>
</ul>
<aside>

<p>📌 1차 캐시 특징</p>
<ul>
<li>같은 엔티티가 있으면 해당 엔티티를 그대로 반환. 따라서 1차 캐시는 객체 동일성(<code>a==b</code>)를 보장</li>
<li>기본적으로 영속성 컨텍스트 범위의 캐시</aside>

</li>
</ul>
<h4 id="2차-캐시">2차 캐시</h4>
<ul>
<li>하이버네이트 등 JPA 구현체에서 지원하는 <strong>애플리케이션 범위 캐시</strong></li>
<li>애플리케이션이 종료될 때까지 캐시가 유지됨.</li>
<li>분산 캐시 또는 클러스터링 환경의 캐시는 애플리케이션보다 더 오래 유지될 수도 있음</li>
<li>엔티티 매니저를 통해 데이터 조회 시 2차 캐시에서 우선 찾고 없으면 데이터베이스에서 찾음</li>
<li>동시성 극대화를 위해 캐시한 객체를 직접 반환하지 않고 복사본을 만들어 반환<ul>
<li>객체를 그대로 반환하면 여러 곳에서 같은 객체를 수정하는 문제 발생 가능</li>
<li>락에 비해 객체를 복사하는 비용은 저렴함</li>
</ul>
</li>
</ul>
<aside>

<p>📌 2차 캐시 특징</p>
<ul>
<li>영속성 유닛 범위의 캐시</li>
<li>조회한 객체를 그대로 반환하는 것이 아니라 복사본을 만들어 반환</li>
<li>데이터베이스 기본키를 기준으로 캐시하지만, 영속성 컨텍스트가 다르면 객체 동일성(<code>a==b</code>)을 보장하지 않음</aside>

</li>
</ul>
<hr>
<h3 id="2-jpa-2차-캐시-기능">(2) JPA 2차 캐시 기능</h3>
<p>JPA 2.0에서 캐시(2차 캐시) 표준을 정의함. JPA 캐시 표준은 여러 구현체가 공통으로 사용하는 부분만 표준화했기에, 세밀한 설정을 하려면 구현체에 의존적인 기능 사용 필요.</p>
<h4 id="1-캐시-모드-설정">1-캐시 모드 설정</h4>
<ul>
<li>2차 캐시 사용을 위해서는 <code>@Cacheable</code> 어노테이션을 사용.</li>
</ul>
<pre><code class="language-java">@Cacheable
@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
}</code></pre>
<ul>
<li>persistence.xml에 <code>shared-cache-mode</code>를 설정해 애플리케이션 전체에 캐시를 어떻게 적용할지 옵션을 정해야함</li>
</ul>
<pre><code class="language-xml">&lt;persistence-unit name=&quot;test&quot;&gt;
    &lt;shared-cache-mode&gt;ENABLE_SELECTIVE&lt;/shared-cache-mode&gt;
&lt;/persistence-unit&gt;</code></pre>
<ul>
<li>캐시모드는 SharedCacheMode에 정의. 보통 ENABLE_SELECTIVE를 사용<ul>
<li>ENABLE_SELECTIVE: Cacheable(true)로 설정된 엔티티만 캐시를 사용</li>
</ul>
</li>
</ul>
<pre><code class="language-xml">&lt;bean id=&quot;entityManagerFactory&quot; class=&quot;org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean&quot;&gt;
    &lt;property name=&quot;sharedCacheMode&quot; value=&quot;ENABLE_SELECTIVE&quot;/&gt;</code></pre>
<h4 id="2-캐시-조회-저장-방식-설정">2-캐시 조회, 저장 방식 설정</h4>
<p>캐시를 무시하고 데이터베이스를 직접 조회하거나 캐시를 갱신하려면 캐시 조회 모드와 캐시 보관 모드를 사용하면 됨.</p>
<pre><code class="language-java">em.setProperty(&quot;retrieveMode&quot;, CacheRetrieveMode.BYPASS);</code></pre>
<ul>
<li>캐시 조회 모드나 보관 모드에 따라 사용할 프로퍼티와 옵션이 다름.</li>
<li>프로퍼티 이름:<ul>
<li>retrieveMode: 캐시조회 모드</li>
<li>storeMode: 캐시보관 모드</li>
</ul>
</li>
<li>옵션:<ul>
<li>CacheRetrieveMode: 캐시 조회 모드 설정 옵션</li>
<li>CacheStoreMode: 캐시 보관 모드 설정 옵션</li>
</ul>
</li>
</ul>
<ol>
<li>캐시 조회 모드</li>
</ol>
<pre><code class="language-java">public enum CacheRetrieveMode {
    USE,
    BYPASS
}</code></pre>
<ul>
<li>USE: 캐시에서 조회. 기본값</li>
<li>BYPASS: 캐시를 무시하고 데이터베이스에 직접 접근</li>
</ul>
<ol>
<li>캐시 보관 모드</li>
</ol>
<pre><code class="language-java">public enum CacheStoreMode {
    USE,
    BYPASS,
    REFRESH
}</code></pre>
<ul>
<li>USE: 조회한 데이터를 캐시에 저장. 조회한 데이터가 이미 캐시에 있으면 캐시 데이터를 최신 상태로 갱신하지는 않음</li>
<li>BYPASS: 캐시에 저장하지 않음</li>
<li>REFRESH: USE 전략에 추가로 데이터베이스에서 조회한 엔티티를 최신 상태로 다시 캐시함</li>
</ul>
<p>⇒ 캐시 모드는 <code>EntityManager.setProperty()</code>로 엔티티 매니저 단위로 설정하거나, <code>EntityManager.find()</code>, <code>EntityManager.refresh()</code>에 더 세밀하게 설정할 수 있음. 또는 <code>Query.setHint()</code>에도 사용할 수 있음.</p>
<h4 id="3-jpa-캐시-관리-api">3-JPA 캐시 관리 API</h4>
<ul>
<li>JPA에서는 캐시 관리를 위한 Cache 인터페이스 제공. 이는 EntityManagerFactory에서 구할 수 있음</li>
</ul>
<pre><code class="language-java">Cache cache = emf.getCache();
boolean contains =
    cache.contains(TestEntity.class, testEntity.getId());
System.out.println(&quot;contains = &quot; + contains);</code></pre>
<ul>
<li>Cache 인터페이스의 기능</li>
</ul>
<pre><code class="language-java">public interface Cache {

    //해당 엔티티에 캐시가 있는지 여부 확인
    public boolean contains(Class cls, Object primaryKey);

    //해당 엔티티 중 특정 식별자를 가진 엔티티를 캐시에서 제거
    public void evict(Class cls, Object primaryKey);

    //해당 엔티티 전체를 캐시에서 제거
    public void evict(Class cls);

    //모든 캐시에서 데이터 제거
    public vodi evictAll();

    //JPA Cache 구현체 조회
    public &lt;T&gt; T unwrap(Class&lt;T&gt; cls);
}</code></pre>
<hr>
<h3 id="3-하이버네이트와-ehcache-적용">(3) 하이버네이트와 EHCACHE 적용</h3>
<p>하이버네이트가 지원하는 캐시 3가지</p>
<ul>
<li><strong>엔티티 캐시</strong>: 엔티티 단위로 캐시. 식별자로 엔티티를 조회하거나 컬렉션이 아닌 연관된 엔티티를 로딩할 때 사용</li>
<li><strong>컬렉션 캐시</strong>: 엔티티와 연관된 컬렉션을 캐시. <strong>컬렉션이 엔티티를 담고 있으면 식별자값만 캐시</strong></li>
<li><strong>쿼리 캐시</strong>: 쿼리와 파라미터 정보를 키로 사용해 캐시. <strong>결과가 엔티티면 식별자값만 캐시</strong></li>
</ul>
<h4 id="엔티티-캐시와-컬렉션-캐시">엔티티 캐시와 컬렉션 캐시</h4>
<pre><code class="language-java">**@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)**
@Entity
public class ParentMember {

    @Id @GeneratedValue
    private Long id;

    private String name;

    **@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)**
    @OneToMany(mappedBy = &quot;parentMember&quot;, cascade = CascadeType.ALL)
    private List&lt;ChildMember&gt; childMembers = new ArrayList&lt;ChildMember&gt;();
}</code></pre>
<ul>
<li><code>@Cacheable</code>: 엔티티를 캐시하기위해서 사용</li>
<li><code>@Cache</code>: 하이버네이트 전용 어노테이션. 캐시 관련 더 세밀한 설정을 위해 사용.</li>
</ul>
<h4 id="cache"><code>@Cache</code></h4>
<ul>
<li>Cache</li>
</ul>
<table>
<thead>
<tr>
<th>속성</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>usage</td>
<td>CacheConcurrencyStrategy를 사용해 캐시 동시성 전략을 설정</td>
</tr>
<tr>
<td>region</td>
<td>캐시 지역 설정</td>
</tr>
<tr>
<td>include</td>
<td>연관 객체를 캐시에 포함할지 선택. all, non-lazy 옵션을 선택. 기본값은 all.</td>
</tr>
<tr>
<td>- CacheConcurrencyStrategy</td>
<td></td>
</tr>
</tbody></table>
<table>
<thead>
<tr>
<th>속성</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>NONE</td>
<td>캐시 설정 X</td>
</tr>
<tr>
<td>READ_ONLY</td>
<td>읽기 전용으로 설정. 등록, 삭제는 가능하지만 수정은 불가능.</td>
</tr>
<tr>
<td>NONSTRICT_READ_WRITE</td>
<td>엄격하지 않은 읽고 쓰기 전략. 동시에 같은 엔티티 수정 시 데이터 일관성이 깨질 수 있음.</td>
</tr>
<tr>
<td>READ_WRITE</td>
<td>읽기 쓰기가 가능하고 READ COMMITED 정도의 격리 수준을 보장. EHCACHE는 데이터 수정하면 캐시 데이터도 같이 수정</td>
</tr>
<tr>
<td>TRANSACTIONAL</td>
<td>컨테이너 관리 환경에서 사용할 수 있음. 설정에 따라 REPEATABLE READ 정도의 격리 수준 보장</td>
</tr>
</tbody></table>
<h4 id="캐시-영역">캐시 영역</h4>
<p>캐시를 적용한 코드는 캐시 영역에 저장됨</p>
<ul>
<li>엔티티 캐시 영역: 기본값으로 [패키지 명 + 클래스 명]을 사용</li>
<li>컬렉션 캐시 영역: 엔티티 캐시 영역 이름에 컬렉션의 필드명이 추가됨</li>
</ul>
<p>→ 필요시 <code>@Cache(region = &quot;customRegion&quot;, ...)</code> 처럼 region 속성을 사용해 캐시 영역 직접 지정 가능</p>
<h4 id="쿼리-캐시">쿼리 캐시</h4>
<ul>
<li>쿼리와 파라미터 정보를 키로 하여 쿼리 결과를 캐시하는 방법</li>
<li>적용을 위해서는 영속성 유닛을 설정에 <code>hibernate.cache.use_query_cache</code> 옵션을 true로 설정해야함</li>
<li>쿼리 캐시를 적용하려는 쿼리마다 <code>cacheable</code>을 true로 설정</li>
</ul>
<pre><code class="language-java">em.createQuery(&quot;select i from Item i&quot;, Item.class)
    .setHint(&quot;cacheable&quot;, true)
    .getResultList();</code></pre>
<h4 id="쿼리-캐시-영역">쿼리 캐시 영역</h4>
<p>쿼리 캐시 활성화 시 두 캐시 영역이 추가됨</p>
<ul>
<li>StandardQueryCahce: 쿼리 캐시를 저장하는 영역. 쿼리, 쿼리 결과 집합, 실행 시점의 타임스탬프 보관</li>
<li>UpdateTimestampsCache: 쿼리 캐시가 유효한지 확인하기 위해 쿼리 대상 테이블의 가장 최근 변경 시간을 저장.</li>
</ul>
<p>쿼리 캐시는 캐시한 데이터 집합을 최신 데이터로 유지하려고 쿼리 캐시를 실행하는 시간과 쿼리 캐시가 사용하는 테이블들이 가장 최근에 변경된 시간을 비교함.</p>
<p>⇒ 쿼리 캐시는 빈번한 변경이 있는 테이블에 사용하면 성능이 저하될 수 있으니 수정이 거의 일어나지 않는 테이블에 사용해야 효과적임.</p>
<h4 id="쿼리-캐시와-컬렉션-캐시의-주의점">쿼리 캐시와 컬렉션 캐시의 주의점</h4>
<ul>
<li>엔티티 캐시: 엔티티 캐시 → 엔티티 정보를 모두 캐시</li>
<li><strong>쿼리 캐시, 컬렉션 캐시: 결과 집합의 식별자 값만 캐시</strong></li>
</ul>
<p>⇒ 결국 이 식별자값을 하나씩 엔티티 캐시에서 조회해 실제 엔티티를 찾음.</p>
<p>따라서 <strong>쿼리 캐시나 컬렉션 캐시 사용 시 결과 대상 엔티티에는 반드시 엔티티 캐시를 적용</strong>해야함</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 15주차 스터디 (1)]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-15%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-15%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Wed, 02 Sep 2026 11:19:32 GMT</pubDate>
            <description><![CDATA[<h1 id="15장-고급-주제와-성능-최적화">15장. 고급 주제와 성능 최적화</h1>
<ul>
<li>예외 처리: JPA 사용 시 발생할 때 다양한 예외와 예외에 따른 주의점</li>
<li>엔티티 비교: 엔티티를 비교할 때 주의점과 해결 방법을 설명</li>
<li>성능 최적화:<ul>
<li>N+1 문제</li>
<li>읽기 전용 쿼리의 성능 최적화</li>
<li>배치 처리</li>
<li>SQL 쿼리 힌트 사용</li>
<li>트랜잭션을 지원하는 쓰기 지연과 성능 최적화</li>
</ul>
</li>
</ul>
<h2 id="151-예외-처리">15.1 예외 처리</h2>
<h3 id="1-jpa-표준-예외-정리">(1) JPA 표준 예외 정리</h3>
<ul>
<li>JPA의 표준 예외들은 모두 PersistenceException의 자식 클래스이며, RuntimeException의 자식 클래스<ul>
<li>JPA 예외는 모두 언체크 예외</li>
</ul>
</li>
</ul>
<h4 id="jpa-표준-예외-종류">JPA 표준 예외 종류</h4>
<ul>
<li>트랜잭션 롤백을 표시하는 예외:<ul>
<li>트랜잭션 롤백을 표시하는 예외의 경우 심각한 예외이기에 복구해서는 안됨</li>
<li>해당 예외 발생 시, 트랜잭션을 강제 커밋해도 트랜잭션이 커밋되지 않고 RollbackException 예외가 발생</li>
</ul>
</li>
<li>트랜잭션 롤백을 표시하지 않는 예외<ul>
<li>심각하지 않은 예외로 개발자가 커밋 롤백의 여부를 판단</li>
</ul>
</li>
</ul>
<aside>

<p>트랜잭션 롤백을 표시하는 예외</p>
<table>
<thead>
<tr>
<th>예외</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>EntityExistsException</td>
<td>EntityManager.persist() 호출 시 이미 같은 엔티티가 있으면 발생.</td>
</tr>
<tr>
<td>EntityNotFoundException</td>
<td>EntityManager.getReference()를 호출했는데, 실제 사용 시 엔티티가 존재하지 않으면 발생, refresh(), lock()에서도 발생.</td>
</tr>
<tr>
<td>OptimisticLockException</td>
<td>낙관적 락 충돌 시 발생</td>
</tr>
<tr>
<td>PessimisticLockException</td>
<td>비관적 락 충돌 시 발생</td>
</tr>
<tr>
<td>RollbackException</td>
<td>EntityTransaction.commit() 실패 시 발생. 롤백</td>
</tr>
</tbody></table>
<p>트랜잭션 롤백을 표시하지 않는 예외</p>
<table>
<thead>
<tr>
<th>예외</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>NoResultException</td>
<td>Query.getSingleResult() 호출 시 결과가 하나도 없을 때 발생</td>
</tr>
<tr>
<td>NonUniqueResultException</td>
<td>Query.getSingleResult() 호출 시 결과가 둘 이상일 때 발생.</td>
</tr>
<tr>
<td>LockTimeoutException</td>
<td>비관적 락에서 시간초과 시 발생</td>
</tr>
<tr>
<td>QueryTimeoutException</td>
<td>쿼리 실행 시간 초과 시 발생</td>
</tr>
<tr>
<td></aside></td>
<td></td>
</tr>
</tbody></table>
<hr>
<h3 id="2-스프링-프레임워크와-jpa-예외-반환">(2) 스프링 프레임워크와 JPA 예외 반환</h3>
<ul>
<li><p>서비스 계층에서 데이터 접근 계층의 구현 기술에 직접 의존 = 좋지 않은 설계</p>
<p>  → 이런 특징은 예외에서도 적용됨</p>
<p>  예) 서비스 계층에서 JPA의 예외 직접 사용 시, JPA에 의존.</p>
</li>
<li><p>위의 문제를 해결하기 위해 스프링 프레임워크는 <strong>데이터 접근 계층에 대한 예외를 추상화해 개발자에게 제공</strong>함.</p>
<p>  예) PersistenceException → JpaSystemException, NoResultException → EmptyResultException</p>
</li>
</ul>
<hr>
<h3 id="3-스프링-프레임워크에-jpa-예외-변환기-적용">(3) 스프링 프레임워크에 JPA 예외 변환기 적용</h3>
<ul>
<li>JPA 예외를 스프링 프레임워크가 제공하는 추상화된 예외로 변경하기 위해서는 <code>PersistenceExceptionTranslationPostProcessor</code>를 스프링 빈으로 등록</li>
<li><code>@Repository</code> 어노테이션을 사용한 곳에 예외 변환 AOP를 적용해 JPA 예외를 스프링 프레임워크가 추상화한 예외로 변환해줌.</li>
</ul>
<p>설정방법:</p>
<pre><code class="language-java">&lt;bean class=&quot;org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor&quot; /&gt;</code></pre>
<p>JavaConfig를 사용하여 등록:</p>
<pre><code class="language-java">@Bean
public PersistenceExceptionTranslationPostProcessorexceptionTranslation(){
    return new PersistenceExceptionTranslationPostProcessor();
}</code></pre>
<p>예) 예외 변환 예제</p>
<pre><code class="language-java">@Repository
public class NoResultExceptionTestRepository {

    @PersistenceContext EntityManager em;

    public Member findMember() {
        //조회된 데이터 X
        return em.createQuery(&quot;select m from Member m&quot;, Member.class).getSingleResult();
    }
}</code></pre>
<ul>
<li><p><code>findMember()</code> 메소드에서 엔티티 조회를 위해 <code>getSingleResult()</code> 메소드를 사용</p>
<ul>
<li><p>해당 메소드는 조회 결과가 없을 때, NoResultException이 발생.</p>
</li>
<li><p>이 예외가 <code>findMember()</code> 메소드를 빠져나갈 때, PersistenceExceptionTranslationPostProcessor에 등록한 AOP 인터셉터가 동작해 해당 예외를 EmptyResultDataAccessException 예외로 변환해서 반환</p>
<p>  ⇒ 따라서 해당 메소드를 호출한 클라이언트는 스프링 프레임워크가 추상화한 예외를 받음.</p>
</li>
</ul>
</li>
</ul>
<ul>
<li>예외를 반환하지 않고 그대로 반환할 때: <code>throws</code> 절에 그대로 반환활 JPA 예외 또는 JPA 예외의 부모 클래스를 직접 명시</li>
</ul>
<p>예) 예외를 반환하지 않는 코드</p>
<pre><code class="language-java">@Repository
public class NoResultExceptionTestService {

    @PersistenceContext EntityManager em;

    public member findMember() throws NoResultException {
        return em.createQuery(&quot;select m from Member m&quot;, Member.class).getSingleResult();
    }
}</code></pre>
<hr>
<h3 id="4-트랜잭션-롤백-시-주의사항">(4) 트랜잭션 롤백 시 주의사항</h3>
<ul>
<li>트랜잭션의 롤백 = 데이터베이스의 반영사항만 롤백. <strong>수정한 자바 객체까지 원상태로 복구하지는 않음.</strong><ul>
<li>객체는 수정된 상태로 영속성 컨텍스트에 남아있음 → 따라서 트랜잭션이 롤백된 영속성 컨텍스트를 그대로 사용하는 것은 위험.</li>
<li>따라서 <strong>새로운 영속성 컨텍스트</strong>를 생성해 사용하거나 <strong><code>EntityManager.clear()</code>를 호출해 영속성 컨텍스트 초기화</strong> 후 사용.</li>
</ul>
</li>
</ul>
<h4 id="스프링-프레임워크의-예방">스프링 프레임워크의 예방</h4>
<p>위의 문제를 예방하기 위해 영속성 컨텍스트의 범위에 따라 다른 방법을 사용함</p>
<ul>
<li><p>기본 전략: 트랜잭션 당 영속성 컨텍스트 전략</p>
<p>  문제가 발생하면 트랜잭션 AOP 종료 시점에 트랜잭션을 롤백하며 <strong>영속성 컨텍스트도 함께 종료</strong>하여 문제 발생 예방</p>
</li>
<li><p>추가 문제점: OSIV같이 영속성 컨텍스트 범위를 트랜잭션 범위보다 넓게 사용해 <strong>여러 트랜잭션이 하나의 영속성 컨텍스트를 사용할 때</strong>는 트랜잭션 롤백으로 인해 영속성 컨텍스트에 이상이 발생해도 다른 트랜잭션에서 해당 영속성 컨텍스트를 그대로 사용함</p>
<ul>
<li>해결책: 영속성 컨텍스트 범위를 트랜잭션 범위보다 넓게 설정했을 때는, <strong>“트랜잭션 롤백시 영속성 컨텍스트를 초기화(<code>EntityManager.clear()</code>)”</strong>를 통해 잘못된 영속성 컨텍스트 사용 문제를 예방.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="152-엔티티-비교">15.2 엔티티 비교</h2>
<h4 id="1차-캐시">1차 캐시</h4>
<ul>
<li><p>영속성 컨텍스트 내부에 엔티티 인스턴스를 보관하기 위해 존재.</p>
</li>
<li><p>영속성 컨텍스트와 생명주기를 같이함</p>
</li>
<li><p>영속성 컨텍스트를 통해 데이터를 저장하거나 조회하면 1차 캐시에 엔티티가 저장됨</p>
</li>
<li><p>1차 캐시를 통해 변경감지 기능 동작</p>
</li>
<li><p>1차 캐시로 사용되어 DB를 통하지 않고 데이터 조회 가능</p>
</li>
<li><p>장점: “애플리케이션 수준의 반복 가능한 읽기” → 이걸 이해해야 영속성 컨텍스트를 더 잘 이해할 수 있음.</p>
<p>  예) 같은 영속성 컨텍스트의 엔티티를 조회하면 항상 같은 엔티티 인스턴스를 반환함.</p>
<pre><code class="language-java">  Member member1 = em.find(Member.class, &quot;1L&quot;);
  Member member2 = em.find(Member.class, &quot;1L&quot;);

  assertTrue(member1 == member2); //둘은 같은 인스턴스</code></pre>
</li>
</ul>
<hr>
<h3 id="1-영속성-컨텍스트가-같을-때-엔티티-비교">(1) 영속성 컨텍스트가 같을 때 엔티티 비교</h3>
<p>예) 회원가입 테스트 케이스</p>
<p>테스트는 트랜잭션 안에서 시작하기에 테스트의 범위와 트랜잭션의 범위가 아래와 같음.
즉, 테스트 전체에서 같은 영속성 컨텍스트에 접근함</p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/d7c31e89-82ac-47e6-a284-a834968f6b74/image.png" alt=""></p>
<pre><code class="language-java">@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = &quot;classpath:appConfig.xml&quot;)
@Transactional //트랜잭션 안에서 테스트를 실행
public class MemberServiceTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;


    @Test
    public void 회원가입() throws Exception {...}
}

@Transactional
public class MemberRepository {

    @PersistenceContext EntityManager em;

    public void save (Member member) {
        em.persist(member);
    }

    public Member findOne (Long id) {
        return em.find(Member.class, id);
    }
}</code></pre>
<ul>
<li>테스트 클래스에 <code>@Transactional</code>이 선언되어 있는 경우 트랜잭션을 먼저 시작하고 테스트 메소드를 실행함<ul>
<li>즉, 테스트 메소드인 <code>회원가입()</code>은 이미 트랜잭션 범위에 들어있고, 해당 메소드가 끝나면 트랜잭션이 종료됨</li>
<li>따라서 <code>회원가입()</code> 에서 사용된 코드는 항상 같은 트랜잭션과 같은 영속성 컨텍스트에 접근</li>
</ul>
</li>
</ul>
<aside>

<h4 id="📌-영속성-컨텍스트가-같을-때-엔티티-비교">📌 영속성 컨텍스트가 같을 때 엔티티 비교</h4>
<p>아래 3가지 조건을 모두 만족함</p>
<ul>
<li>동일성: <code>==</code> 비교가 같음</li>
<li>동등성: <code>equals()</code> 비교가 같음</li>
<li>데이터베이스 동등성: <code>@Id</code>인 데이터베이스 식별자가 같음</aside>

</li>
</ul>
<hr>
<h3 id="2-영속성-컨텍스트가-다를-때-엔티티-비교">(2) 영속성 컨텍스트가 다를 때 엔티티 비교</h3>
<p>예) 테스트 클래스에 <code>@Transactional</code>이 없고 서비스에만 <code>@Transactional</code>이 있을 때</p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/4862f9da-6498-45a8-adb8-3f1ca49430dc/image.png" alt=""></p>
<pre><code class="language-java">@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = &quot;classpath:appConfig.xml&quot;)
//@Transactional: 테스트에서 트랜잭션을 사용하지 않음
public class MemberServiceTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;


    @Test
    public void 회원가입() throws Exception {...}
}

@Transactional
public class MemberRepository {

    @PersistenceContext EntityManager em;

    public void save (Member member) {
        em.persist(member);
    }

    public Member findOne (Long id) {
        return em.find(Member.class, id);
    }
}</code></pre>
<ul>
<li><p>위의 경우처럼 테스트에 <code>@Transactional</code>이 붙어있는지 않는 경우 테스트는 실패함</p>
</li>
<li><p>진행과정</p>
<p>  1- 테스트코드에서 <code>memberService.join()</code>을 호출하여 회원가입 시도 → 트랜잭션이 서비스 계층에서 시작→ 영속성 컨텍스트1 생성 </p>
<p>  2- memberRepository에서 <code>em.persist()</code>를 호출하여 member 엔티티를 영속화</p>
<p>  3- 서비스 계층이 끝날 때 트랜잭션이 커밋되어 영속성 컨텍스트가 플러시 → 이때 트랜잭션과 영속성 컨텍스트 종료. 즉, <code>member</code> 엔티티 인스턴스는 준영속 상태.</p>
<p>  4- 테스트코드에서 <code>memberRepository.findOne()</code>을 호출해 엔티티를 조회 → 레포지토리 계층에서 새로운 트랜잭션이 시작 → 새로운 영속성 컨텍스트2 생성</p>
<p>  5- 저장된 회원을 조회 → but 새로 생긴 영속성 컨텍스트2에는 회원이 존재하지 않음</p>
<p>  6- 데이터베이스에서 회원을 찾아옴</p>
<p>  7- 데이터베이스에서 조회된 회원 엔티티를 영속성 컨텍스트에 보관 및 반환</p>
<p>  8- <code>memberRepository.findOne()</code> 메소드가 끝나며 트랜잭션이 종료. 영속성 컨텍스트2도 종료됨</p>
</li>
<li><p><code>member</code>와 <code>findMember</code>는 각각 다른 영속성 컨텍스트에서 관리 → 즉, 둘은 다른 인스턴스</p>
<p>  <code>assertTrue(member == findMember)</code>: 실패</p>
</li>
</ul>
<aside>

<h4 id="📌-영속성-컨텍스트가-다를-때-엔티티-비교">📌 영속성 컨텍스트가 다를 때 엔티티 비교</h4>
<ul>
<li>동일성: <code>==</code> 비교가 실패함</li>
<li>동등성: <code>equals()</code> 비교가 만족하지만, 반드시 <code>equals()</code>를 구현해야함. 보통 비즈니스키로 구현</li>
<li>데이터베이스 동등성: <code>@Id</code>인 데이터베이스 식별자가 같음</aside>

</li>
</ul>
<h4 id="정리">정리</h4>
<ul>
<li>같은 영속성 컨텍스트를 보장해야만 동일성 비교로 엔티티 비교 가능
예) OSIV처럼 요청의 시작부터 끝까지 같은 영속성 컨텍스트를 사용하는 경우</li>
<li>영속성 컨텍스트 변경 시 동일성 비교는 실패</li>
</ul>
<h4 id="대안1---데이터베이스-동등성-비교">대안1 - 데이터베이스 동등성 비교</h4>
<p><code>member.getId().equals(findMember.getId())</code> → 데이터베이스 식별자를 비교</p>
<ul>
<li>단점: 엔티티를 영속화해야 식별자를 얻을 수 있음. 즉, 엔티티 영속화 전에는 식별자 값이 null이기에 정확한 비교 불가. (단, 식별자값 직접 부여하는 방식에서는 가능. 하지만 이런 경우를 보장하기가 쉽지 않음)</li>
</ul>
<h4 id="대안2---equals를-이용한-동등성-비교">대안2 - <code>equals()</code>를 이용한 동등성 비교</h4>
<p>엔티티 비교에서는 <strong>비즈니스 키를 활용한 동등성 비교를 권장</strong>함</p>
<ul>
<li>동등성 비교를 위해 <code>equals()</code> 오버라이딩할 때 비즈니스 키가되는 필드들을 선택</li>
<li>비즈니스 키: 보통 중복되지 않고 거의 변하지 않는 데이터베이스 기본키 후보
예) 주민등록번호</li>
</ul>
<hr>
<h2 id="153-프록시-심화-주제">15.3 프록시 심화 주제</h2>
<h4 id="프록시">프록시</h4>
<ul>
<li>원본 엔티티를 상속받아 만들어지기에 엔티티를 사용하는 클라이언트의 경우 엔티티가 프록시인지 원본 엔티티인지를 구분하지 않고 사용 가능.</li>
<li>즉, 원본 엔티티 사용 중 지연로딩을 위해 프록시로 변경하여도 클라이언트의 비즈니스 로직 수정 불필요</li>
</ul>
<hr>
<h3 id="1-영속성-컨텍스트와-프록시">(1) 영속성 컨텍스트와 프록시</h3>
<p>의문점: 영속성 컨텍스트 = 관리하는 영속 엔티티들의 동일성을 보장 → 다만, 프록시로 조회한 엔티티의 동일성도 보장할까?</p>
<p>예) 프록시 조회 → 원본 엔티티 조회</p>
<pre><code class="language-java">@Test
public void 영속성컨텍스트와_프록시() {

    Member newMember = new Member(&quot;member1&quot;, &quot;회원1&quot;);
    em.persist(newMember);
    em.flush();
    em.clear();

    Member refMember = em.getReference(Member.class, &quot;member1&quot;);
    Member findMember = em.find(Member.class, &quot;member1&quot;);

    Assert.assertTrue(refMember == findMember); //성공
}</code></pre>
<ul>
<li>먼저 member1을 <code>getReference()</code> 메소드를 통해 프록시로 조회, 이후 같은 member1을 <code>em.find()</code>로 조회. 전자는 프록시, 후자는 원본 엔티티</li>
</ul>
<h4 id="문제점">문제점</h4>
<p>만일 이 둘을 다른 인스턴스로 보게되면 영속성 컨텍스트가 영속 엔티티의 동일성을 보장하지 못하는 문제 발생</p>
<h4 id="해결">해결</h4>
<p>영속성 컨텍스트에서 프록시로 조회된 엔티티에 대해 같은 엔티티를 찾는 요청이 들어올 때, 원본 엔티티가 아닌 처음 조회된 프록시를 반환함</p>
<p>⇒ 즉, 프록시로 조회할 때도 영속성 컨텍스트는 영속 엔티티의 동일성을 보장함</p>
<p>예) 원본 엔티티 조회 → 프록시 조회</p>
<pre><code class="language-java">@Test
public void 영속성컨텍스트와_프록시2() {

    Member newMember = new Member(&quot;member1&quot;, &quot;회원1&quot;);
    em.persist(newMember);
    em.flush();
    em.clear();

    Member findMember = em.find(Member.class, &quot;member1&quot;);
    Member refMember = em.getReference(Member.class, &quot;member1&quot;);

    Assert.assertTrue(refMember == findMember); //성공
}</code></pre>
<ul>
<li><p>결과: 원본 엔티티를 먼저 조회하는 경우, 영속성 컨텍스트는 원본 엔티티를 이미 데이터베이스에서 조회했기에 프록시를 반환할 이유가 없음. 즉, <code>getReference()</code>를 호출해도 프록시가 아닌 원본을 반환함.</p>
<p>  ⇒ 즉, 이런 경우에도 영속성 컨텍스트는 자신이 관리하는 영속 엔티티의 동일성을 보장함</p>
</li>
</ul>
<hr>
<h3 id="2-프록시-타입-비교">(2) 프록시 타입 비교</h3>
<p>프록시는 원본 엔티티를 상속 받아 만들어지기에 프록시로 조회한 엔티티 타입의 비교에는 <code>==</code> 비교가 아닌 <code>instanceof</code>를 사용해야함</p>
<pre><code class="language-java">@Test
public void 영속성컨텍스트와_프록시2() {

    Member newMember = new Member(&quot;member1&quot;, &quot;회원1&quot;);
    em.persist(newMember);
    em.flush();
    em.clear();

    Member refMember = em.getReference(Member.class, &quot;member1&quot;);

    Assert.assertFalse(refMember == findMember); //false
    Assert.assertTrue(refMember instanceof Member); //true
}</code></pre>
<ul>
<li><code>==</code> 비교: 부모 클래스와 자식 클래스를 비교하는 것. 즉, 결과가 false.</li>
</ul>
<hr>
<h3 id="3-프록시-동등성-비교">(3) 프록시 동등성 비교</h3>
<p>동등성 비교를 위해서는 비즈니스 키를 사용해 <code>equals()</code> 메소드를 오버라이딩하여 비교.</p>
<p>IDE/외부 라이브러리를 사용해 구현한 <code>equals()</code> 메소드를 사용해 엔티티 비교 시, 비교 대상이 원본 엔티티이면 괜찮지만, 프록시인 경우에는 문제 발생 가능</p>
<pre><code class="language-java">@Entity
public class Member {

    @Id
    private String id;
    private String name;

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

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (this.getClass() != obj.getClass()) return false;

        Member member = (Member) obj;

        if (name != null ? !name.equals(member.name) : member.name != null) return false;
    }

    @Override
    public int hashCode() {
        return name != null ? name.hashCode() : 0;
    }
}</code></pre>
<ul>
<li>위의 경우 회원 엔티티의 <code>name</code> 필드를 비즈니스 키로 사용해 <code>equals()</code> 메소드를 오버라이딩.</li>
</ul>
<h4 id="문제점-1">문제점</h4>
<ul>
<li>새로 생성한 회원 <code>newMember</code>와 프록시로 조회한 회원 <code>refMember</code>의 name 속성은 같지만, 동등성 비교 시 실패(false 반환).</li>
</ul>
<h4 id="원인-및-프록시-equals-비교-주의점">원인 및 프록시 equals() 비교 주의점</h4>
<p>문제1:</p>
<p><code>this.getClass() != obj.getClass()</code>의 부분은 타입을 동등성 비교함 → 즉, 프록시는 원본을 상속받은 자식 타입이기에 프록시의 타입을 비교할 때는 <code>instanceof</code>를 사용해야함.</p>
<p>문제2:</p>
<pre><code class="language-java">Member member = (Member) obj;
if(name != null ? !name.equals(member.name) : member.name != null) return false;</code></pre>
<ul>
<li><p><code>member.name</code>을 보면 프록시의 멤버변수에 직접 접근함</p>
</li>
<li><p><code>equals()</code> 메소드를 구현할 때는 일반적으로 멤버변수를 직접 비교하는데 프록시의 경우에는 문제가 생김</p>
<p>  ⇒ 왜냐하면 프록시는 <strong>실제 데이터를 가지고 있지 않음</strong>.</p>
</li>
<li><p>따라서 위처럼 프록시의 멤버변수에 직접 접근하면 아무값도 조회할 수 없어 null이 반환, <code>equals()</code>는 false 반환함</p>
</li>
</ul>
<p>해결: 프록시의 데이터 조회를 위해서는 접근자(getter)를 사용해야함.</p>
<aside>

<h4 id="정리-1">정리</h4>
<ul>
<li>프록시의 타입 비교는 <code>instanceof</code>를 사용해야함</li>
<li>프록시의 멤버변수에 직접 접근하면 안되고 <strong>접근자 메소드</strong>를 사용해야함</aside>

</li>
</ul>
<hr>
<h3 id="4-상속관계와-프록시">(4) 상속관계와 프록시</h3>
<h4 id="문제점-2">문제점</h4>
<p>프록시를 부모타입으로 조회할 시 문제가 발생할 수 있음.</p>
<aside>

<p>예) Item(부모 클래스) - Book(하위 클래스). Item을 조회하여 Book 타입인 경우 저자 이름을 출력하려할 때.</p>
<p><code>em.getReference()</code>를 통해 Item 엔티티를 프록시로 조회 → <code>instanceof</code> 연산을 통해 Book 클래스 타입인지를 확인 → Book 타입인 경우 다운캐스팅하여 Book 타입으로 변경 후 저자이름 출력.</p>
<p>⇒ 다만 결과로 저자는 출력되지 않음.</p>
</aside>

<ul>
<li>Item 엔티티를 프록시로 조회, 실제 조회 엔티티는 Book 타입 기반으로 원본 엔티티 인스턴스가 생성. 프록시인 proxyItem은 Item 타입을 기반으로 생성</li>
<li>따라서 <code>instanceof</code>의 비교가 false를 반환하게됨.</li>
<li>여기에 더해 직접 다운캐스팅을 하여도 문제 발생함. proxyItem이 Book 타입이 아니라 Item 타입 기반의 ItemProxy 타입이기에 ClassCastException 예외가 발생함.</li>
</ul>
<aside>

<h4 id="📌-문제점-정리">📌 문제점 정리</h4>
<p>프록시를 부모 타입으로 조회하면 부모의 타입을 기반으로 프록시가 생성되게 됨</p>
<ul>
<li>instanceof 연산 사용 불가</li>
<li>하위타입으로 다운캐스팅 불가</aside>

</li>
</ul>
<p>이런 문제는 주로 다형성을 다루는 도메인 모델에서 발생함.</p>
<h4 id="해결책-1---jpql로-대상-직접-조회">해결책 (1) - JPQL로 대상 직접 조회</h4>
<p>간단한 방법으로, 처음부터 자식 타입을 직접 조회하여 필요한 연산을 수행. 단점으로는 다형성을 활용할 수 없다는 한계 존재.</p>
<pre><code class="language-java">Book jpqlBook = em.createQuery(&quot;select b from Book b where b.id = :bookId&quot;, 
        Bookclass)
    .setParameter(&quot;bookId&quot;, item.getId())
    .getSingleResult();</code></pre>
<h4 id="해결책-2---프록시-벗기기">해결책 (2) - 프록시 벗기기</h4>
<p>하이버네이트에서 제공하는 기능을 사용해 프록시에서 원본 엔티티를 가져올 수 있음</p>
<pre><code class="language-java">...
Item item = orderItem.getItem();
**Item unProxyItem = unProxy(item);**

**if (unProxyItem instanceof Book)** {
    System.out.println(&quot;proxyItem instanceof Book&quot;);
    Book book = (Book) unProxyItem;
    System.out.println(&quot;책 저자 = &quot; + book.getAuthor());
}

Assert.assertTrue(item != unProxyItem);
}

//하이버네이트가 제공하는 프록시에서 원본 엔티티를 찾는 기능을 사용하는 메소드
**public static &lt;T&gt; T unProxy(Object entity) {
    if (entity instanceof HibernateProxy) {
        entity = ((HibernateProxy) entity)
                    .getHibernateLazyInitializer()
                    .getImplementation();
    }
    return (T) entity;
}**</code></pre>
<ul>
<li>영속성 컨텍스트는 한 번 프록시로 노출한 엔티티는 계속 프록시로 노출함 (이래야 동일성 보장 가능 + 클라이언트가 조회한 엔티티가 프록시인지 아닌지의 구분하지 않고 사용 가능)</li>
<li>다만, 위의 방법은 원본 엔티티를 프록시에서 직접 꺼내기에 추후 “프록시와 원본 엔티티의 동일성 비교가 실패한다는 문제점이 존재”함.<ul>
<li>따라서 해당 방법을 사용할 때, <strong>원본 엔티티가 꼭 필요한 곳에서 잠깐 사용하고 다른 곳에서 사용되지 않도록 하는 것</strong>이 중요함. <strong>원본 엔티티의 값을 직접 변경해도 변경 감지 기능은 동작.</strong></li>
</ul>
</li>
</ul>
<h4 id="해결책-3---기능을-위한-별도의-인터페이스-제공">해결책 (3) - 기능을 위한 별도의 인터페이스 제공</h4>
<pre><code class="language-java">public interface TitleView {
    String getTitle();
}

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = &quot;DTYPE&quot;)
**public abstract class Item implements TitleView** {

    @Id @GeneratedValue
    @Column(name = &quot;ITEM_ID&quot;)
    private Long id;

    private String name;
    private int price;
    private int stockQuantity;
    ...
}

@Entity
@DiscriminatorValue(&quot;B&quot;)
public class Book extends Item {
    private String author;
    private String isbn;

    @Override
    public String getTitle() {
        return &quot;[제목: &quot; + getName() + &quot; 저자:&quot; + author + &quot;]&quot;;
    }
}

@Entity
@DiscriminatorValue(&quot;M&quot;)
public class Movie extends Item {

    private String director;
    private String actor;

    @Override
    public String getTitle() {
        return &quot;[제목: &quot; + getName() + &quot; 감독:&quot; + director + &quot; 배우:&quot; + actor + &quot;]&quot;;
    }
}
</code></pre>
<ul>
<li>TitleView라는 공통 인터페이스를 만들고 자식 클래스들은 인터페이스의 <code>getTitle()</code> 메소드를 각각 구현.</li>
</ul>
<pre><code class="language-java">@Entity
public class OrderItem {

    @Id @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;ITEM_ID&quot;)
    private Item item;

    ...
}

OrderItem orderItem = em.find(OrderItem.class, saveOrderItem.getId());
orderItem.printItem();</code></pre>
<ul>
<li>위의 방법을 사용할 때는 <strong>프록시의 대상이 되는 타입에 인터페이스를 적용</strong>해야함. 위에서는 Item이 프록시 대상이기에 Item이 인터페이스를 받아야함</li>
</ul>
<h4 id="해결책-4---비지터-패턴-사용">해결책 (4) - 비지터 패턴 사용</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/6083b1c8-65c4-4815-a27c-ee417120f765/image.png" alt=""></p>
<ul>
<li>비지터(Visitor) 패턴: Visitor와 Visitor를 받아들이는 대상 클래스로 구성</li>
<li>위의 경우에서는 Item이 <code>accept(visitor)</code> 메소드를 사용해 Visitor를 받아들임. Item은 단순히 Visitor를 받아들이기만하고 실제 로직은 Visitor가 처리.</li>
</ul>
<p>1-Visitor 정의와 구현</p>
<pre><code class="language-java">public interface Visitor {
    void visit(Book book);
  void visit(Album album);
  void visit(Movie movie);
}</code></pre>
<ul>
<li>Visitor에는 <code>visit()</code> 메소드를 정의하고 모든 대상 클래스를 받아들이도록 작성</li>
<li>위의 경우에서는 Book, Album, Movie를 대상 클래스로 사용</li>
</ul>
<p>2- 대상 클래스 작성</p>
<p>Item에 Visitor를 받아들일 수 있도록 <code>accept(visitor)</code>  메소드 추가</p>
<pre><code class="language-java">@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = &quot;DTYPE&quot;)
public abstract class Item {

    **public abstract void accept(Visitor visitor);**
}

@Entity
@DiscriminatorValue(&quot;B&quot;)
public class Book extends Item {
    ...

    **@Override
    public void accept(Visitor visitor) {
        visitor.visit(this)
    }**
}</code></pre>
<ul>
<li>각각의 자식 클래스들은 부모에 정의한 <code>accept(visitor)</code> 메소드를 구현했는데, 구현 내용은 단순히 파라미터로 넘어온 Visitor의 <code>visit(this)</code> 메소드를 호출해 자신을 파라미터로 넘김</li>
<li>실제 로직 처리는 visitor에게 위임</li>
</ul>
<p>3-비지터 패턴 실행</p>
<pre><code class="language-java">@Test
public void 상속관계와_프록시_VisitorPattern() {
    OrderItem orderItem = em.find(OrderItem.class, orderItemId);
    Item item = orderItem.getItem();

    //PrintVisitor
    **item.accept(new PrintVisitor());**
}</code></pre>
<ul>
<li><code>item.accept()</code> 메소드를 호출하여 파라미터로 PrintVisitor를 넘겨줌.<ul>
<li>item은 프록시 이기에 먼저 프록시가 <code>accept()</code> 메소드를 받고 원본 엔티티의 <code>accept()</code>를 실행함.</li>
<li>원본 엔티티는 자신을 visitor 파라미터로 넘겨줌</li>
</ul>
</li>
</ul>
<p>⇒ 비지터 패턴을 사용하면 프록시에 대한 걱정 없이 안전하게 원본 엔티티에 접근할 수 있고 instanceof나 타입 캐스팅 없이 코드를 구현할 수 있음</p>
<aside>

<h3 id="📌-비지터-패턴-정리">📌 비지터 패턴 정리</h3>
<h4 id="비지터-패턴과-확장성">비지터 패턴과 확장성</h4>
<p>새로운 기능이 필요할 때 Visitor만 추가하면 되기에 기존 코드의 구조를 변경하지 않고 기능을 추가할 수 있는 장점.</p>
<h4 id="비지터-패턴-정리">비지터 패턴 정리</h4>
<p>장점:</p>
<ul>
<li>프록시에 대한 걱정 없이 안전하게 원본 엔티티에 접근 가능</li>
<li>instanceof와 타입캐스팅 없이 코드 구현 가능</li>
<li>알고리즘과 객체 구조를 분리해 구조를 수정하지 않고 새로운 동작 추가 가능</li>
</ul>
<p>단점:</p>
<ul>
<li>너무 복잡하고 더블 디스패치를 사용하기에 이해가 어려움</li>
<li>객체 구조가 변경되면 모든 Visitor를 수정해야함</aside>

</li>
</ul>
<hr>
<h2 id="154-성능-최적화">15.4 성능 최적화</h2>
<h3 id="1-n1-문제">(1) N+1 문제</h3>
<p>N+1 문제: JPA 애플리케이션 개발 시 성능 상 가장 주의해야하는 문제</p>
<p>예)</p>
<pre><code class="language-java">@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = &quot;member&quot;, fetch = FetchType.EAGER)
    private List&lt;Order&gt; orders = new ArrayList&lt;Order&gt;();
}

@Entity
@Table(name = &quot;ORDERS&quot;)
public class Order {

    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    private Member member;
}</code></pre>
<p>회원과 주문정보 → 1:N, N:1 양방향 관계 + 회원이 참조하는 주문정보인 Member.orders를 즉시로딩으로 설정.</p>
<h4 id="즉시로딩과-n1">즉시로딩과 N+1</h4>
<p>예시에서 특정 회원 하나를 <code>em.find()</code>로 조회하면 즉시로딩으로 설정한 주문정보들도 함께 조회하게됨.</p>
<p>이때 SQL을 두 번 실행하지 않고, 조인을 통해 하나의 SQL로 회원+주문정보를 모두 조회.</p>
<pre><code class="language-sql">SELECT M.*, O.*
FROM MEMBER M OUTER JOIN 
         ORDERS O ON M.ID=O.MEMBER_ID</code></pre>
<p>문제점: JPQL을 사용할 때 발생.</p>
<ul>
<li>JPA가 JPQL을 분석해 SQL을 생성 → 이때 즉시로딩, 지연로딩 고려하지 않고 JPQL만 사용.</li>
<li>회원 엔티티를 로딩한 후에, 연관된 주문 컬렉션이 무엇인지 찾음.<ul>
<li>만약 조회하는 회원이 N명이면?</li>
</ul>
</li>
</ul>
<p>N+1 문제: 처음 실행한 SQL의 결과 수만큼 추가로 SQL을 실행하는 문제</p>
<h4 id="지연로딩과-n1">지연로딩과 N+1</h4>
<ul>
<li><p>앞선 예시인 회원과 주문을 지연로딩으로 설정해도 N+1 문제에서 자유로울 수는 없음.</p>
</li>
<li><p>JPQL에서는 다만, N+1 문제가 발생하지 않음</p>
<pre><code class="language-java">  List&lt;Member&gt; members = 
      em.createQuery(&quot;select m from Member m&quot;, Member.class)
          .getResultList();</code></pre>
<ul>
<li>지연로딩이기에 데이터베이스에서 회원만 조회됨 → 이후 비즈니스 로직에서 주문 컬렉션을 실제 사용할 때 지연로딩 발생</li>
</ul>
</li>
<li><p>문제: 모든 회원에 대한 연관 주문 컬렉션 사용</p>
<pre><code class="language-java">  for (Member member : members) {
      //지연로딩 초기화
      System.out.println(&quot;member = &quot;+ member.getOrders().size());
  }</code></pre>
<p>  ⇒ 주문 컬렉션을 초기화하는 수만큼 SQL이 실행되며 N+1 문제 발생</p>
</li>
</ul>
<h4 id="해결책1---페치-조인-사용">해결책(1) - 페치 조인 사용</h4>
<p>N+1 문제를 해결하는 가장 일반적인 방법. 페치 조인은 SQL 조인을 사용해 연관 엔티티를 함께 조회하기에 N+1 문제가 발생하지 않음.</p>
<p>예)</p>
<pre><code class="language-java">select m from Member m join fetch m.orders

SELECT M.*, O.*
FROM MEMBER M
INNER JOIN ORDERS O ON M.ID = O.MEMBER_ID</code></pre>
<h4 id="해결책2---하이버네이트-batchsize">해결책(2) - 하이버네이트 @BatchSize</h4>
<p>하이버네이트에서 제공하는 <code>@BatchSize</code> 어노테이션을 사용하면 연관된 엔티티를 조회할 때 지정한 size만큼 SQL의 IN절을 사용해 조회함</p>
<pre><code class="language-java">@Entity
public class Member {
    ...

    **@BatchSize(size = 5)**
    @OneToMany(mappedBy = &quot;member&quot;, fetch = FetchType.EAGER)
    private List&lt;Order&gt; orders = new ArrayList&lt;Order&gt;();
}</code></pre>
<p>즉시로딩 설정: 10건의 데이터 모두 조회가 필요하면 SQL이 2번 실행됨</p>
<p>지연로딩 설정: 엔티티 최초 사용 시점에 SQL 1번 실행해 5건은 미리 로딩 → 이후 6번째 데이터 필요 시 추가 SQL 시행</p>
<pre><code class="language-sql">SELECT O
FROM ORDERS O
WEHRE MEMBER_ID IN (?,?,?,?,?)</code></pre>
<h4 id="해결책3---하이버네이트-fetchfetchmodesubselect">해결책(3) - 하이버네이트 @Fetch(FetchMode.SUBSELECT)</h4>
<p>하이버네이트에서 제공하는 <strong><code>@Fetch</code> 어노테이션에 FetchMode를 SUBSELECT로 사용</strong> → 연관된 데이터 조회 시 서브 쿼리를 사용해 N+1 문제를 해결</p>
<p>예)</p>
<pre><code class="language-java">@Entity
public class Member {

    @Fetch(FetchMode.SUBSELECT)
    @OneToMany(mappedBy = &quot;member&quot;, fetch = FetchType.EAGER)
    private List&lt;Order&gt; orders = new ArrayList&lt;Order&gt;();
}</code></pre>
<p>즉시로딩으로 설정 시 조회 시점에, 지연로딩으로 설정 시 엔티티를 사용하는 시점에 아래 SQL이 실행됨</p>
<pre><code class="language-sql">SELECT O
FROM ORDERS O
WEHRE O.MEMBER_ID IN (SELECT M.ID
                                            FROM MEMBER M
                                            WHERE M.ID &gt; 10)</code></pre>
<aside>

<h4 id="📌-정리">📌 정리</h4>
<p>추천하는 방법: 즉시로딩은 사용하지 않고 지연 로딩 + 페치 조인만 사용하는 것.</p>
<ul>
<li>즉시로딩의 문제점<ul>
<li>N+1 문제는 물론 비즈니스 로직에 필요하지 않은 엔티티를 로딩해야하는 상황이 자주 발생</li>
<li>성능 최적화가 어려움 → 엔티티 조회 시, 즉시로딩이 연속적으로 발생해 예상치 못한 SQL이 실행될 수 있음</li>
</ul>
</li>
<li>JPA의 글로벌 페치 전략 기본값<ul>
<li><code>@OneToOne</code>, <code>@ManyToOne</code>: 기본 페치 전략은 즉시 로딩</li>
<li><code>@OneToMany</code>, <code>@ManyToMany</code>: 기본 페치 전략은 지연 로딩</aside>

</li>
</ul>
</li>
</ul>
<hr>
<h3 id="2-읽기-전용-쿼리의-성능-최적화">(2) 읽기 전용 쿼리의 성능 최적화</h3>
<p>엔티티가 영속성 컨텍스트에서 관리 → 1차 캐시, 변경 감지 등 이점이 많지만 영속성 컨텍스트는 <strong>스냅샷 인스턴스를 보관하기에 더 많은 메모리를 사용한다는 단점</strong>이 존재함</p>
<p>예) 100건의 구매 내용을 출력하는 조회 화면 </p>
<ul>
<li><p>조회한 엔티티 재조회 필요없고, 수정없이 한 번만 읽어서 화면에 출력하기만하면됨</p>
<p>  ⇒ 이런 경우에는 <strong>읽기 전용으로 엔티티 조회</strong> 시 메모리 사용량 최적화 가능</p>
</li>
</ul>
<h4 id="스칼라-타입으로-조회">스칼라 타입으로 조회</h4>
<p>가장 확실한 방법으로 엔티티가 아닌 스칼라 타입으로 모든 필드를 조회하는 것. 스칼라 타입의 경우 영속성 컨텍스트가 결과를 관리하지 않음</p>
<pre><code class="language-sql">select o.id, o.name, o.price
from Order p</code></pre>
<h4 id="읽기-전용-쿼리-힌트-사용">읽기 전용 쿼리 힌트 사용</h4>
<p>하이버네이트 전용 힌트인 <code>readOnly</code>를 사용해 읽기 전용으로 조회 가능. 읽기 전용이기에 <strong>영속성 컨텍스트는 스냅샷을 보관하지 않음</strong> → 즉, 메모리 사용량 최적화 가능.</p>
<p>(단, 스냅샷이 없기에 엔티티를 수정해도 데이터베이스에 반영 X)</p>
<pre><code class="language-java">TypedQuery&lt;Order&gt; query = em.createQuery(&quot;select o from Order o&quot;, Order.class);
**query.setHint(&quot;readOnly&quot;, true);**</code></pre>
<h4 id="읽기-전용-트랜잭션-사용">읽기 전용 트랜잭션 사용</h4>
<p>스프링 프레임워크 사용 시 트랜잭션을 읽기 전용으로 설정할 수 있음</p>
<pre><code class="language-java">@Transactional(readOnly = true)</code></pre>
<p>위처럼 설정하면 스프링 프레임워크가 하이버네이트 세션의 플러시 모드를 MANUAL로 설정함. </p>
<ul>
<li>이러면 강제 플러시 호출 전까지는 플러시가 발생하지 않음.</li>
<li>즉, 트랜잭션을 커밋해도 영속성 컨텍스트를 플러시하지 않아 엔티티의 등록/수정/삭제는 동작하지 않음.</li>
<li>트랜잭션을 시작하긴했으니 트랜잭션 시작, 로직수행, 커밋의 과정은 발생.</li>
</ul>
<h4 id="트랜잭션-밖에서-읽기">트랜잭션 밖에서 읽기</h4>
<p>트랜잭션의 밖 = 트랜잭션 없이 엔티티를 조회. JPA에서 데이터 변경 시 트랜잭션은 필수이기에 조회가 목적일 때만 사용해야함.</p>
<pre><code class="language-java">@Transactional(propagation = Propagation.NOT_SUPPORTED)</code></pre>
<p>트랜잭션을 사용하지 않으면 플러시가 일어나지 않기에 조회 성능이 향상됨.</p>
<hr>
<h3 id="3-배치-처리">(3) 배치 처리</h3>
<p>수백만건의 데이터를 처리해야할 때 → 일반적인 방식은 영속성 컨텍스트에 많은 엔티티가 쌓여 메모리 부족 오류가 발생하기에 배치 처리는 적절한 단위로 영속성 컨텍스트를 초기화해야함. 또한, 2차 캐시를 사용하지 않고 있다면, 2차 캐시에 엔티티를 보관하지 않도록 주의 필요.</p>
<h4 id="jpa-등록-배치">JPA 등록 배치</h4>
<p>여러 엔티티를 한 번에 등록할 때의 주의점 = <strong>영속성 컨텍스트에 엔티티가 계속 쌓이지 않도록 일정 단위마다 영속성 컨텍스트의 엔티티를 데이터베이스에 플러시 → 영속성 컨텍스트 초기화</strong>할 필요 존재</p>
<p>위의 작업을 하지 않으면 영속성 컨텍스트에 너무 많은 엔티티가 저장되며 메모리 부족 오류가 발생할 수 있음</p>
<pre><code class="language-java">EntityManager em = entityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();

for (int i=0; i&lt;100000; i++) {
    Product product = new Product(&quot;item&quot; + i, 10000);
    em.persist(product);

    //100건마다 플러시와 영속성 컨텍스트 초기화
    if (i % 100 == 0) {
        em.flush();
        em.clear();
    }
}

tx.commit();
em.close();</code></pre>
<h4 id="jpa-페이징-배치-처리">JPA 페이징 배치 처리</h4>
<pre><code class="language-java">EntityManager em = entityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();

int pageSize = 100;
for (int i=0; i&lt;10; i++) {
    **List&lt;Product&gt; resultList = em.createQuery(&quot;select p from Product p&quot;, Product.class)
        .setFirstResult(i * pageSize)
        .setMaxResults(pageSize)
        .getResultList();**

    for (Product p : resultList) {
        p.setPrice(p.getPrice() + 100);
    }

    em.flush();
    em.clear();
}

tx.commit();
em.close();</code></pre>
<ul>
<li>페이지 단위마다 영속성 컨텍스트 플러시 후 초기화 진행</li>
<li>한 번에 100건씩 페이징 쿼리를 통해 조회</li>
</ul>
<h4 id="하이버네이트-scroll">하이버네이트 scroll</h4>
<p>JPA는 JDBC 커서를 지원하지 않기에, 커서 사용을 위해서는 하이버네이트 세션을 사용해야함. 하이버네이트는 scroll이라는 이름으로 JDBC 커서를 지원함</p>
<pre><code class="language-java">EntityTranscation tx = em.getTransaction();
Session session = em.unwrap(Session.class);
tx.begin();
ScrollableResults scroll = session.createQuery(&quot;select p from Product p&quot;,
        .setCacheMode(CacheMode.IGNORE) //2차 캐시 기능을 끔
        .scroll(ScrollMode.FORWARD_ONLY);
int count = 0;

while (scroll.next()) {
    Product p = (Product) scroll.get(0);
    p.setPrice(p.getPrice() + 100);

    count ++;
    if (count % 100 == 0) {
        session.flush(); //플러시
        session.clear(); //영속성 컨텍스트 초기화
    }
}</code></pre>
<ul>
<li>scroll: 하이버네이트 전용 기능으로 먼저 <code>em.unwrap()</code> 메소드를 통해 하이버네이트 세션을 구함</li>
<li>쿼리를 조회하면 <code>scroll()</code> 메소드로 <code>ScrollableResults</code> 객체를 반환받음. 해당 객체의 <code>next()</code> 메소드를 호출하여 엔티티를 하나씩 조회할 수 있음.</li>
</ul>
<h4 id="하이버네이트-무상태-세션-사용">하이버네이트 무상태 세션 사용</h4>
<p>무상태 세션: 영속성 컨텍스트를 만들지 않고, 2차 캐시를 사용하지 않음. 즉, 엔티티 수정을 위해서는 무상태 세션이 제공하는 <code>update()</code> 메소드를 직접 호출해야함</p>
<pre><code class="language-java">SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
StatelessSession session = sessionFactory.openStatelessSession();
Transaction tx = session.beginTransaction();
ScrollableResults scroll = session.createQuery(&quot;select p from Product p).scroll();

while(scroll.next()) {
    Product p = (Product) scroll.get(0);
    p.setPrice(p.getPrice() + 100);
    **session.update(p);** //직접 호출 필요
}

tx.commit();
session.close();</code></pre>
<p>하이버네이트 무상태 세션은 일반 하이버네이트 세션과 거의 비슷하지만 영속성 컨텍스트가 없음. 즉, 영속성 컨텍스트를 플러시나 초기화할 필요가 없고 대신 엔티티 수정 시 <code>update()</code> 메소드를 직접 호출해야함.</p>
<hr>
<h3 id="4-sql-쿼리-힌트-사용">(4) SQL 쿼리 힌트 사용</h3>
<p>JPA는 데이터베이스 SQL 힌트 기능을 제공하지 않기에, SQL 힌트 사용을 위해서는 <strong>하이버네이트를 직접 사용</strong>해야함.</p>
<p>SQL 힌트는 하이버네이트 쿼리가 제공하는 <code>addQueryHint()</code> 메소드를 사용함.</p>
<pre><code class="language-java">Session session = em.unwrap(Session.class); //하이버네이트 직접 사용

List&lt;Member&gt; list = session.createQuery(&quot;select m from Member m&quot;)
        .addQueryHint(&quot;FULL (MEMBER)&quot;) //SQL HINT 추가
        .list();

//실행된 SQL
select
    /*+FULL (MEMBER) */ m.id, m.name
from Member m</code></pre>
<p>+현재 하이버네이트 4.3.10 버전에는 오라클 방언에 대한 힌트만 적용되어 있음. 다른 데이터베이스 SQL 힌트 사용을 위해서는 각 방언에서 Dialect에 있는 아래의 메소드를 오버라이딩하여 구현해야함</p>
<pre><code class="language-java">public String getQueryHintStirng(String query, List&lt;String&gt; hints) {
    return query;
}</code></pre>
<hr>
<h3 id="5-트랜잭션을-지원하는-쓰기-지연과-성능-최적화">(5) 트랜잭션을 지원하는 쓰기 지연과 성능 최적화</h3>
<h4 id="트랜잭션을-지원하는-쓰기-지연과-성능-최적화">트랜잭션을 지원하는 쓰기 지연과 성능 최적화</h4>
<p>예)</p>
<pre><code class="language-java">insert(member1);
insert(member2);
insert(member3);
insert(member4);
insert(member5);

commit();</code></pre>
<p>네트워크 호출 한 번은 단순 메소드 수만 번 호출보다 더 큰 비용이 듬. 위의 코드는 5번의 INSERT SQL과 1번의 커밋을 통해 6번 데이터베이스와 통신.</p>
<p>→ 최적화를 위해서 5번의 INSERT SQL을 모아서 한 번에 데이터베이스로 보내면됨.</p>
<ul>
<li>JDBC가 제공하는 SQL 배치 기능을 사용하면 SQL을 모아서 데이터베이스에 한 번에 보낼 수 있음. 다만, 이를 위해서는 코드의 많은 부분을 수정해야함<ul>
<li>비즈니스 로직이 복잡하게 얽힌 곳에서는 사용이 쉽지 않고 코드가 지저분해짐</li>
<li>따라서 보통 수백, 수천건 이상의 데이터를 변경하는 특수 상황에 SQL 배치 기능을 사용</li>
<li>JPA는 플러시 기능이 있으므로 SQL 배치 기능 효과적으로 사용 가능</li>
</ul>
</li>
<li>SQL 배치 최적화 전략은 구현체마다 다름.</li>
</ul>
<h4 id="트랜잭션을-지원하는-쓰기-지연과-애플리케이션-확장성">트랜잭션을 지원하는 쓰기 지연과 애플리케이션 확장성</h4>
<p>트랜잭션을 지원하는 쓰기 지연과 변경 감지 기능 → 성능과 개발의 편의성을 줌과 동시에 <strong>데이터베이스 테이블 로우(row)에 락(lock)이 걸리는 시간 최소화</strong>한다는 장점 존재</p>
<ul>
<li>트랜잭션 커밋해서 영속성 컨텍스트를 플러시하기 전까지는 데이터베이스에 등록, 수정, 삭제하지 않음</li>
<li>커밋 직전까지 데이터베이스 로우에 락을 걸지 않음</li>
</ul>
<p>예)</p>
<pre><code class="language-java">update(memberA); //UPDATE SQL A
비즈니스로직A(); //UPDATE SQL
비즈니스로직B(); //INSERT SQL
commit();</code></pre>
<ul>
<li>JPA를 사용하지 않고, SQL을 직접 다루면 <code>update(memberA)</code>를 호출할 때 UPDATE SQL을 실행하며 데이터베이스 테이블 로우에 락을 검.<ul>
<li>해당 락은 비즈니스 로직 A,B와 커밋이 모두 끝날때까지 유지됨.</li>
<li>커밋된 읽기(Read Committed) 격리 수준 또는 그 이상에서는 데이터베이스에 현재 수정 중인 데이터(row)를 수정하려는 다른 트랜잭션은 락이 풀릴 때까지 대기.</li>
</ul>
</li>
<li>JPA는 커밋을 해야 플러시를 호출하고 데이터베이스에 수정 쿼리를 보냄.<ul>
<li>commit()을 호출하는 시점에 UPDATE SQL을 실행해 바로 데이터베이스에 트랜잭션을 커밋</li>
<li>쿼리를 보내고 바로 커밋하므로 데이터베이스에 락이 걸리는 시간을 최소화함.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 14주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-14%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-14%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sat, 29 Aug 2026 17:05:28 GMT</pubDate>
            <description><![CDATA[<h1 id="14장-컬렉션과-부가기능">14장. 컬렉션과 부가기능</h1>
<h2 id="141-컬렉션">14.1 컬렉션</h2>
<p>JPA는 자바에서 기본으로 제공하는 <code>Collection</code>, <code>List</code>, <code>Set</code>, <code>Map</code> 컬렉션을 지원하고 아래의 경우에 사용 가능함.</p>
<ul>
<li><code>@OneToMany</code>, <code>@ManyToMany</code>를 사용해 일대다 또는 다대다 엔티티 관계를 매핑할 때</li>
<li><code>@ElementCollection</code>을 사용해 값 타입을 하나 이상 보관할 때</li>
</ul>
<h4 id="자바-컬렉션-인터페이스의-특징">자바 컬렉션 인터페이스의 특징</h4>
<ul>
<li>Collection: 최상위 컬렉션. 하이버네이트에서는 <strong>중복을 허용하고 순서를 보장하지 않는다고</strong> 가정.</li>
<li>Set: <strong>중복을 허용하지 않고 순서를 보장하지 않는 컬렉션</strong></li>
<li>List: 순서가 존재하고 중복을 허용하는 컬렉션</li>
<li>Map: 키(key)와 값(value) 구조로 되어 있는 특수한 컬렉션.</li>
</ul>
<h3 id="1-jpa와-컬렉션">(1) JPA와 컬렉션</h3>
<h4 id="특징">특징</h4>
<ul>
<li><p>하이버네이트는 엔티티를 영속 상태로 만들 때, 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으로 감싸 사용함.</p>
</li>
<li><p>컬렉션의 효율적 관리를 위해 엔티티를 영속 상태로 만들 때, <strong>원본 컬렉션을 감싸고 있는 내장 컬렉션을 생성 → 해당 내장 컬렉션을 사용하도록 참조를 변경</strong>함.</p>
<ul>
<li>하이버네이트가 제공하는 내장 컬렉션은 래퍼 컬렉션으로도 부름.</li>
</ul>
</li>
<li><p>이런 특징으로 인해 <strong>컬렉션을 사용할 때 즉시 초기화해 사용하는 것을 권장함</strong>.</p>
<p>  예) Collection<Member> members = new ArrayList<Member>();</p>
</li>
</ul>
<p>예시:</p>
<pre><code class="language-java">@Entity
public class Team {

    @Id
    private String id;

    @OneToMany
    @JoinColumn
    private Collection&lt;Member&gt; members = new ArrayList&lt;Member&gt;();
}</code></pre>
<p>아래의 코드를 통해 Team을 영속 상태로 만들면</p>
<pre><code class="language-java">Team team = new Team();

System.out.println(&quot;before persist = &quot; + team.getMembers().getClass());
System.out.println(&quot;after persist = &quot; + team.getMembers().getClass());</code></pre>
<p>출력 결과가 아래와 같다.</p>
<pre><code>before persist = class.java.util.**ArrayList**
after persist = class.org.hibernate.collection.internal.**PersistentBag**</code></pre><p>결과를 보면 원래 <code>ArrayList</code> 타입이었던 컬렉션이 엔티티를 영속상태로 만든 직후 하이버네이트가 제공하는 <code>PersistentBag</code> 타입으로 변경됨.</p>
<h4 id="인터페이스에-따른-래퍼-컬렉션">인터페이스에 따른 래퍼 컬렉션</h4>
<table>
<thead>
<tr>
<th>컬렉션 인터페이스</th>
<th>내장 컬렉션</th>
<th>중복 허용</th>
<th>순서 보관</th>
</tr>
</thead>
<tbody><tr>
<td>Collection.List</td>
<td>PersistenceBag</td>
<td>O</td>
<td>X</td>
</tr>
<tr>
<td>Set</td>
<td>PersistenceSet</td>
<td>X</td>
<td>X</td>
</tr>
<tr>
<td>List + @OrderColumn</td>
<td>PersistentList</td>
<td>O</td>
<td>O</td>
</tr>
</tbody></table>
<pre><code class="language-java">//PersistenceBag
@OneToMany
Collection&lt;Member&gt; collection = new ArrayList&lt;Member&gt;();

//PersistenceBag
@OneToMany
List&lt;Member&gt; list = new ArrayList&lt;Member&gt;();

//PersistenceSet
@OneToMany
Set&lt;Member&gt; set = new HashSet&lt;Member&gt;();

//PersistentList
@OneToMany @OrderColumn
List&lt;Member&gt; orderColumnList = new ArrayList&lt;Member&gt;();</code></pre>
<hr>
<h3 id="2-collection-list">(2) Collection, List</h3>
<ul>
<li>Collection과 List 인터페이스는 <strong>중복을 허용하는 컬렉션</strong>으로 PersistentBag를 래퍼 컬렉션으로 사용함.</li>
<li>해당 인터페이스는 <code>ArrayList</code>로 초기화</li>
<li>중복을 허용하는 것을 가정하기에 객체 추가 <code>add()</code> 메소드는 내부에서 어떤 비교도 하지 않고 항상 true를 반환.</li>
<li>같은 엔티티가 있는지 찾거나 삭제할 때는 <code>equals()</code> 메소드를 사용.</li>
</ul>
<pre><code class="language-java">@Entity
public class Parent {

    @Id @GeneratedValue
    private Long id;

    @OneToMany
    @JoinColumn
    private Collection&lt;CollectionChild&gt; collection = 
        new ArrayList&lt;CollectionChild&gt; ();

    @OneToMany
    @JoinColumn
    private List&lt;ListChild&gt; list = new ArrayList&lt;ListChild&gt;();

    ...

}</code></pre>
<p>Collection, List는 엔티티를 추가할 때 <strong>중복된 엔티티가 있는지 비교하지 않고 단순히 저장</strong>만 하면 됨. 따라서 <strong>엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않음</strong>.</p>
<hr>
<h3 id="3-set">(3) Set</h3>
<ul>
<li><p>Set은 중복을 허용하지 않는 컬렉션</p>
</li>
<li><p>하이버네이트는 PersistentSet을 컬렉션 래퍼로 사용함</p>
</li>
<li><p>해당 인터페이스는 <code>HashSet</code>으로 초기화</p>
</li>
<li><p>메소드로 객체를 추가할 때마다 <code>equals()</code> 메소드로 같은 객체가 있는지의 여부를 비교.</p>
<p>  (HashSet의 경우, 해시 알고리즘을 사용하기에 <code>hashcode()</code>도 함께 사용해 비교)</p>
<ul>
<li>같은 객체 존재X → 객체 추가 후 true 반환</li>
<li>같은 객체 이미 존재 → 추가 실패 후 false 반환</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Entity
public class Parent {

    @OneToMany
    @JoinColumn
    private Set&lt;SetChild&gt; set = new HashSet&lt;SetChild&gt;();
    ...
}</code></pre>
<p><strong>Set은 엔티티를 추가할 때 중복된 엔티티가 있는지 비교해야하기에 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화함.</strong></p>
<hr>
<h3 id="4-list--ordercolumn">(4) List + @OrderColumn</h3>
<ul>
<li>List 인터페이스에 <code>@OrderColumn</code>을 추가하면 <strong>순서가 있는 특수한 컬렉션으로 인식</strong>함</li>
<li>순서가 있다의 의미 = <strong>데이터베이스에 순서 값을 저장해 조회할 때 사용</strong>한다는 의미</li>
<li>하이버네이트는 내부 컬렉션인 PersistentList를 사용함</li>
</ul>
<pre><code class="language-java">@Entity
public class Board {

    @Id @GeneratedValue
    private Long id;

    private String title;
    private String content;

    @OneToMany(mappedBy = &quot;board&quot;)
    @OrderColumn(name = &quot;POSITION&quot;)
    private List&lt;Comment&gt; comments = new ArrayList&lt;Comment&gt;();

    ...
}

@Entity
public class Comment {

    @Id @GeneratedValue
    private Long id;

    private String comment;

    @ManyToOne
    @JoinColumn(name = &quot;BOARD_ID&quot;)
    private Baord board;

    ...
}</code></pre>
<ul>
<li>Board.comments에 List 인터페이스를 사용하고 <code>@OrderColumn</code>을 추가. 따라서 Board.comments는 순서가 있는 컬렉션으로 인식됨</li>
<li>자바가 제공하는 List 컬렉션은 내부에 위치 값을 가지고 있음. 따라서 List의 위치값을 활용 가능<ul>
<li><code>list.add(1, data1);</code> → 1번 위치에 data1 저장</li>
<li><code>list.get(10);</code> → 10번 위치에 있는 값을 조회</li>
</ul>
</li>
<li><strong>순서가 있는 컬렉션의 경우 데이터베이스에 순서값도 함께 관리</strong><ul>
<li>위에서는 <code>@OrderColumn</code>의 name 속성에 POSITION이라는 값을 줌.</li>
<li>JPA는 List의 위치값을 테이블의 POSITION 컬럼에 보관함</li>
<li>Board.comments 컬렉션은 Board 엔티티에 있지만, <strong>테이블의 일대다 관계 특성상 위치값은 다(N)쪽에 지정</strong>해야함. 따라서 실제 POSITION 컬럼은 COMMENT 테이블에 매핑됨.</li>
</ul>
</li>
</ul>
<h4 id="ordercolumn의-단점">@OrderColumn의 단점</h4>
<p>실무에서 잘 사용하지 않는 이유의 단점들이 존재</p>
<ul>
<li>@OrderColumn을 Board 엔티티에서 매핑하기에 Comment는 POSITION의 값을 알 수 없음. 따라서 Comment를 INSERT할 때는 POSITION 값이 저장되지 않음.<ul>
<li>POSITION은 Board.comments의 위치값이기에 해당값을 사용해 POSITION 값을 사용해 POSITION의 값을 UPDATE하는 SQL이 추가로 발생함</li>
</ul>
</li>
<li>List 변경 시 연관된 많은 위치값을 변경해야함</li>
<li>중간에 POSITION값이 없으면 조회한 List에 널(null)이 보관됨</li>
</ul>
<hr>
<h3 id="5-orderby">(5) OrderBy</h3>
<ul>
<li>@OrderBy는 데이터베이스의 ORDER BY절을 사용해 컬렉션을 정렬함. 즉, 순서용 컬럼을 매핑하지 않아도됨.</li>
<li>@OrderBy는 모든 컬렉션에서 사용 가능</li>
</ul>
<pre><code class="language-java">@Entity
public class Team {

    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = &quot;team&quot;)
    @OrderBy(&quot;username desc, id asc&quot;)
    private Set&lt;Member&gt; members = new HashSet&lt;Member&gt;();
    ...

}

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    @Column(name = &quot;MEMBER_NAME&quot;)
    private String username;

    @ManyToOne
    private Team team;
    ...
}</code></pre>
<ul>
<li>Team.members를 보면 @OrderBy를 적용함. 그 값으로 username desc, id asc를 사용</li>
<li>OrderBy의 값은 JPQL의 order by절 처럼 <strong>엔티티의 필드를 대상</strong>으로 함.</li>
</ul>
<hr>
<h2 id="142-converter">14.2 @Converter</h2>
<h4 id="컨버터converter">컨버터(Converter)</h4>
<p>사용 시 엔티티의 데이터를 변환해 데이터베이스에 저장</p>
<p>예) 회원의 VIP 여부를 boolean 타입을 사용할 때. JPA를 사용하면 0 또는 1인 숫자로 저장됨.</p>
<p>→ 이때 데이터베이스에 숫자 대신 Y/N으로 저장하려면 컨버터를 사용하면 됨.</p>
<pre><code class="language-java">@Entity
public class Member {

    @Id
    private String id;
    private String username;

    @Convert(converter = BooleanToYNConverter.class)
    private boolean vip;

    ...

}

@Convert
public class BooleanToYNConverter implements AttributeConverter&lt;Boolean, String&gt; {

    @Override
    public String convertToDatabaseColumn(Boolean attribute) {
        return (attribute != null &amp;&amp; attribute) ? &quot;Y&quot; : &quot;N&quot;;
    }

    @Override
    public Boolean convertToEntityAttribute(String dbData) {
        return &quot;Y&quot;.equals(dbData);
    }
}</code></pre>
<h4 id="특징-1">특징</h4>
<ul>
<li>컨버터 클래스는 <code>@Converter</code> 어노테이션을 사용하고 AttributeConverter 인터페이스를 구현해야함. 또 제네릭에 현재 타입과 반환할 타입을 지정해야함.<ul>
<li>위에서는 &lt;Boolean, String&gt;으로 지정해 Boolean → String 타입의 변환을 실행.</li>
</ul>
</li>
<li>컨버터는 클래스 레벨에도 적용이 가능하지만, 이때는 반드시 <strong>attributeName 속성을 사용해 어떤 필드에 컨버터를 적용할 것인지 명시</strong>해야함.</li>
</ul>
<h4 id="attributeconverter-인터페이스">AttributeConverter 인터페이스</h4>
<p>아래 두 가지 메소드를 구현해야함.</p>
<ul>
<li><code>convertToDatabaseColumn()</code>: 엔티티의 데이터를 데이터베이스 컬럼에 저장할 데이터로 변환.</li>
<li><code>convertToEntityAttribute()</code>: 데이터베이스에서 조회한 컬럼 데이터를 엔티티 데이터로 변환.</li>
</ul>
<h3 id="1-글로벌-설정">(1) 글로벌 설정</h3>
<p>모든 Boolean 타입에 컨버터를 적용하기 위해서는 <strong><code>@Converter(autoApply = true)</code></strong> 옵션을 적용하면 됨.</p>
<pre><code class="language-java">**@Convert(autoApply = true)**
public class BooleanToYNConverter implements AttributeConverter&lt;Boolean, String&gt; {

    @Override
    public String convertToDatabaseColumn(Boolean attribute) {
        return (attribute != null &amp;&amp; attribute) ? &quot;Y&quot; : &quot;N&quot;;
    }

    @Override
    public Boolean convertToEntityAttribute(String dbData) {
        return &quot;Y&quot;.equals(dbData);
    }
}</code></pre>
<ul>
<li>글로벌 설정 시, @Convert를 지정하지 않아도 모든 Boolean 타입에 대해 자동으로 컨버터가 적용됨.</li>
</ul>
<h4 id="converter-속성-정리">@Converter 속성 정리</h4>
<table>
<thead>
<tr>
<th>속성</th>
<th>기능</th>
<th>기본값</th>
</tr>
</thead>
<tbody><tr>
<td>converter</td>
<td>사용할 컨버터를 지정</td>
<td></td>
</tr>
<tr>
<td>attributeName</td>
<td>컨버터를 적용할 필드를 지정</td>
<td></td>
</tr>
<tr>
<td>disableConversion</td>
<td>글로벌 컨버터나 상속 받은 컨버터를 사용하지 않음</td>
<td>false</td>
</tr>
</tbody></table>
<hr>
<h2 id="143-리스너">14.3 리스너</h2>
<p>JPA 리스너 기능을 사용하면 <strong>엔티티의 생명주기에 따른 이벤트</strong>를 처리할 수 있음.</p>
<p>예) 모든 엔티티를 대상으로 언제, 어떤 사용자가 삭제를 요청했는지 모두 로그로 남겨야하는 요구사항</p>
<p>→ 모든 애플리케이션의 삭제 로직마다 로그를 남기는 것은 효율적.</p>
<h3 id="1-이벤트-종류">(1) 이벤트 종류</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/7704597f-7a6b-4f1a-aee5-5ee7a723a987/image.png" alt=""></p>
<ol>
<li>PostLoad: 엔티티가 영속성 컨텍스트에 조회된 직후 또는 refresh 호출 후</li>
<li>PrePersist: <code>persist()</code> 메소드를 호출해 엔티티를 영속성 컨텍스트에 관리하기 직전 호출. (식별자 생성 전략 사용 시 식별자는 아직 존재X). 새로운 인스턴스 merge 시에도 수행</li>
<li>PreUpdate: <code>flush</code> 또는 <code>commit</code>을 호출해 엔티티를 데이터베이스에 수정하기 직전 호출</li>
<li>PreRemove: <code>remove()</code> 메소드를 호출해 엔티티를 영속성 컨텍스트에서 삭제하기 직전 호출. 삭제 명령어로 영속성 전이가 일어날 때도 호출. (orphanRemoval에 대해서는 flush/commit할 때에 호출됨)</li>
<li>PostPersist: flush나 commit을 호출해 엔티티를 데이터베이스 저장한 직후 호출.</li>
<li>PostUpdate: flush나 commit을 호출해 엔티티를 데이터베이스에 수정한 직후 호출.</li>
<li>PostRemove: flush나 commit을 호출해 엔티티를 데이터베이스에 삭제한 직후 호출.</li>
</ol>
<hr>
<h3 id="2-이벤트-적용-위치">(2) 이벤트 적용 위치</h3>
<p>이벤트는 엔티티에서 직접 받거나 별도의 리스너를 등록할 수 있음.</p>
<ul>
<li>엔티티에 직접 적용</li>
<li>별도 리스너 등록</li>
<li>기본 리스너 사용</li>
</ul>
<h4 id="엔티티에-직접-적용">엔티티에 직접 적용</h4>
<p>엔티티에 이벤트가 발생할 때마다 어노테이션으로 지정한 메소드가 실행됨.</p>
<pre><code class="language-java">@Entity
public class Duck {

    @Id @GeneratedValue
    public Long id;

    private String name;

    @PrePersist
    public void prePersist() {
        System.out.println(&quot;Duck.prePersist id=&quot; + id);
    }

    @PostPersist
    public void postPersist() {
        System.out.println(&quot;Duck.postPersist id=&quot; + id);
    }

    @PostLoad
    public void postLoad() {
        System.out.println(&quot;Duck.postLoad id=&quot; + id);
    }

    @PreRemove
    public void preRemove() {
        System.out.println(&quot;Duck.preRemove id=&quot; + id);
    }

    @PostRemove
    public void postRemove() {
        System.out.println(&quot;Duck.postRemove id=&quot; + id);
    }
}</code></pre>
<h4 id="별도의-리스너-등록">별도의 리스너 등록</h4>
<p>리스너의 경우 대상 엔티티를 파라미터로 받을 수 있다. 이때 반환 타입은 void로 설정해야함.</p>
<pre><code class="language-java">@Entity
**@EntityListeners(DuckListener.class)**
public class Duck {...}

public class DuckListener {

    @PrePersist
    //특정 타입인 것이 확실하면 특정 타입을 받을 수 있음.
    public void prePersist(Object obj) {
        System.out.println(&quot;Duck.prePersist obj&quot; + obj);
    }

    @PostPersist
    //특정 타입인 것이 확실하면 특정 타입을 받을 수 있음.
    public void postPersist(Object obj) {
        System.out.println(&quot;Duck.postPersist obj=&quot; + obj);
    }
}</code></pre>
<h4 id="기본-리스너-사용">기본 리스너 사용</h4>
<p>모든 엔티티의 이벤트를 처리하기 위해서는 META-INF/orm.xml에 기본 리스너로 등록하면 됨.</p>
<p>여러 리스너를 등록하면 아래의 순서대로 이벤트가 호출됨.</p>
<aside>

<p>기본 리스너 → 부모 클래스 리스너 → 리스너 → 엔티티</p>
</aside>

<p>+) 더 세밀한 설정</p>
<ul>
<li>ExcludeDefaultListeners: 기본 리스너를 무시</li>
<li>ExcludeSuperclassListeners: 상위 클래스 이벤트 리스너 무시</li>
</ul>
<hr>
<h2 id="144-엔티티-그래프">14.4 엔티티 그래프</h2>
<p>엔티티 조회 시 연관 엔티티를 함께 조회하기 위해서는 글로벌 옵션을 <code>FetchType.EAGER</code>로 설정</p>
<pre><code class="language-java">@Entity
class Order {

    @ManyToOne(fetch = FetchType.EAGER)
    Member member;
    ...
}</code></pre>
<p>또는 JPQL에서 페치 조인을 사용하면 됨.</p>
<pre><code class="language-java">select o from Order o join fetch o.member</code></pre>
<p>⇒ 글로벌 fetch 옵션의 경우 애플리케이션 전체에 영향을 주고 변경할 수 없는 단점이 존재. </p>
<p>→ 따라서 글로벌 fetch 옵션은 <code>FetchType.LAZY</code>를 사용하고 엔티티를 조회할 때 연관 엔티티를 함께 조회할 필요가 있으면 JPQL의 페치조인을 사용함</p>
<ul>
<li>단점: 페치 조인 사용 시에는 같은 JPQL을 중복해서 작성하는 경우가 많음.</li>
</ul>
<p>예) 주문 상태를 검색조건으로 주문 엔티티를 조회하는 JPQL 작성</p>
<ul>
<li>기본<ul>
<li>select o from Order o where o.status = ?</li>
</ul>
</li>
<li>주문과 회원을 함께 조회<ul>
<li>select o from Order o
  join fetch o.member
where o.status = ?</li>
</ul>
</li>
<li>주문과 주문상품을 함께 조회<ul>
<li>select o from Order o
  join fetch o.orderItems
where o.status = ?</li>
</ul>
</li>
</ul>
<p>⇒ 3가지 JPQL 모두 주문을 조회하는 JPQL이지만, 함께 조회할 엔티티에 따라 다른 JPQL을 사용해야함.</p>
<ul>
<li>원인: 위의 문제점은 <strong>JPQL이 데이터 조회 기능 + 연관 엔티티를 함께 조회하는 기능</strong>을 모두 제공하기 때문.</li>
<li>해결책: JPA 2.1에 추가된 <strong>엔티티 그래프 기능</strong>을 사용하면 엔티티를 조회하는 시점에 함께 조회할 연관 엔티티 선택 가능.</li>
</ul>
<h4 id="엔티티-그래프">엔티티 그래프</h4>
<ul>
<li>엔티티 그래프 기능은 <strong>엔티티 조회시점에 연관된 엔티티들을 함께 조회하는 기능</strong>임.</li>
<li>엔티티 그래프에는 정적으로 정의하는 <strong>Named 엔티티 그래프</strong>와 동적으로 정의하는 엔티티 그래프가 존재.</li>
</ul>
<h3 id="1-named-엔티티-그래프">(1) Named 엔티티 그래프</h3>
<pre><code class="language-java">**@NamedEntityGraph(name = &quot;Order.withMember&quot;, attributeNodes = {
    @NamedAttributeNode(&quot;member&quot;)
})**
@Entity
@Table(name = &quot;ORDERS&quot;)
public class Order {

    @Id @GeneratedValue
    @Column(name = &quot;ORDER_ID&quot;)
    private Long id;

    **@ManyToOne(fetch = FetchType.LAZY, optional = false)**
    @JoinColumn(name = &quot;MEMBER_ID&quot;)
    private Member member;
    ...
}</code></pre>
<ul>
<li>Named 엔티티 그래프의 경우 <code>@NamedEntityGraph</code>로 정의.<ul>
<li>name: 엔티티 그래프의 이름을 정의함</li>
<li>attributeNodes: 함께 조회할 속성을 선택. 이때 <code>@NamedAttributeNode</code>를 사용하고 그 값으로 함께 조회할 속성을 선택.</li>
</ul>
</li>
<li>위에서 Order.member가 지연로딩 설정되어있지만, 엔티티 그래프에서 함께 조회할 속성으로 member를 선택했기에 해당 엔티티 그래프를 사용하면 Order를 조회할 때 연관 member 역시 조회 가능.</li>
<li>둘 이상 정의할 때에는 <code>@NamedEntityGraphs</code> 이용</li>
</ul>
<hr>
<h3 id="2-emfind에서-엔티티-그래프-사용">(2) em.find()에서 엔티티 그래프 사용</h3>
<pre><code class="language-java">EntityGraph graph = em.getEntity(&quot;Order.withMember&quot;);

Map hints = new HashMap();
hints.put(&quot;javax.persistence.fetchgraph&quot;, graph);

Order order = em.find(Order.class, orderId, hints);</code></pre>
<ul>
<li>Named 엔티티 그래프를 사용하기 위해서는 정의한 엔티티 그래프를 <code>em.getEntityGraph(&quot;Order.withMember&quot;)</code>를 통해 찾아오면 됨</li>
<li>엔티티 그래프는 JPA의 힌트 기능을 사용해 동작. 힌트키로는 fetchgraph를 사용하고, 힌트의 값으로 찾아온 엔티티 그래프를 사용하면 됨.</li>
<li><code>em.find()</code>를 통해 엔티티를 조회할 때 힌트 정보 역시 포함함.</li>
</ul>
<hr>
<h3 id="3-subgraph">(3) subgraph</h3>
<p>목표: Order → OrderItem → Item 조회.</p>
<p>Order → OrderItem의 경우에 Order가 관리하는 필드지만, OrderItem → Item은 Order가 관리하는 필드가 아님 ⇒ 이런 경우에 subgraph를 사용</p>
<pre><code class="language-java">@NameEntityGraph(name = &quot;Order.withAll&quot;, attributeNodes = {
        @NameAttributeNode(&quot;member&quot;),
        @NameAttributeNode(value = &quot;orderItems&quot;, subgraph = &quot;orderItems&quot;)
        },
        **subgraphs = @NamedSubgraph(name = &quot;orderItems&quot;, attributeNodes = {
                @NameAttributeNode(&quot;item&quot;)
        }**)
)
@Entity
@Table(name =&quot;ORDERS&quot;)
public class Order {
        @Id @GeneratedValue
        @Column(name = &quot;ORDER_ID&quot;)
        private Long id;

        @ManyToOne(fetch = FetchType.LAZY, optional = false)
        @JoinColumn(name = &quot;MEMBER_ID&quot;)
        private Member member; //주문 회원

        @OneToMany(mappedBy = &quot;order&quot;, cascade = CascadeType.ALL)
        private List&lt;OrderItem&gt; orderItems = new ArrayList&lt;OrderItem&gt;();

        ...
}

@Entity
@Table(name = &quot;ORDER_ITEM&quot;)
public class OrderItem {
        @Id @GeneratedValue
        @Column(name = &quot;ORDER_ITEM_ID&quot;)
        private Long id;

        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = &quot;ITEM_ID&quot;)
        private Item item; //주문 상품

        ...
}</code></pre>
<ul>
<li>Order.withAll이라는 Named 엔티티 그래프 정의. 해당 그래프는 Order → Member, Order → OrderItem, OrderItem → Item의 객체 그래프를 함께 조회함.<ul>
<li><strong>이때 OrderItem → Item은 Order의 객체 그래프가 아니기에 subgraph 속성으로 정의</strong></li>
</ul>
</li>
</ul>
<hr>
<h3 id="4-jpql에서-엔티티-그래프-사용">(4) JPQL에서 엔티티 그래프 사용</h3>
<p>JPQL에서 엔티티 그래프를 사용할 때에는 <code>em.find()</code>와 동일하게 힌트만 사용하면 됨.</p>
<pre><code class="language-java">List&lt;Order&gt; resultList = 
        em.createQuery(&quot;select o from Order o where o.id = :orderId&quot;, Order.class)
                .setParameter(&quot;orderId&quot;, orderId)
                **.setHint(&quot;javax.persistence.fetchgraph&quot;, em.getEntityGraph(&quot;Order.withAll&quot;)**)
                .getResultList();</code></pre>
<hr>
<h3 id="5-동적-엔티티-그래프">(5) 동적 엔티티 그래프</h3>
<p>엔티티 그래프를 동적으로 구성하기 위해서는 <code>createEntityGraph()</code> 메소드를 사용</p>
<pre><code class="language-java">public &lt;T&gt; EntityGraph&lt;T&gt; createEntityGraph(Class&lt;T&gt; rootType);</code></pre>
<p>예) 앞의 Named 엔티티 그래프의 동적 구성</p>
<pre><code class="language-java">**EntityGraph&lt;Order&gt; graph = em.createEntityGraph(Order.class);**
graph.addAttributeNodes(&quot;member&quot;);

Map hints = new HashMap()
hints.get(&quot;javax.persistence.fetchgraph&quot;, graph);

Order order = em.find(Order.class, orderId, hints);</code></pre>
<ul>
<li><code>createEntityGraph()</code> 메소드를 사용해 동적으로 엔티티 그래프를 만든 후 <code>addAttributeNodes()</code>를 통해 Order.member 속성을 엔티티 그래프에 포함</li>
</ul>
<p>예) 동적 엔티티 그래프 구성 + subgraph 추가</p>
<pre><code class="language-java">EntityGraph&lt;Order&gt; graph = em.createEntityGraph(Order.class);
graph.addAttributeNodes(&quot;member&quot;);
**Subgraph&lt;OrderItem&gt; orderItems = graph.addSubgraph(&quot;orderItems&quot;);**
**orderItems.addAttributeNodes(&quot;item&quot;);**

Map hints = new HashMap()
hints.get(&quot;javax.persistence.fetchgraph&quot;, graph);

Order order = em.find(Order.class, orderId, hints);</code></pre>
<hr>
<h3 id="6-엔티티-그래프-정리">(6) 엔티티 그래프 정리</h3>
<ul>
<li><p>ROOT에서 시작:</p>
<p>  엔티티 그래프는 항상 조회하는 엔티티의 루트(ROOT)에서 시작해야함.</p>
</li>
<li><p>이미 로딩된 엔티티:</p>
<p>  영속성 컨텍스트에 해당 엔티티가 이미 로딩되어 있음녀 엔티티 그래프가 적용되지 않음.
  (아직 초기화되지 않은 프록시에는 적용됨)</p>
<pre><code class="language-java">  Order order1 = em.find(Order.class, orderId); //이미 조회
  hints.put(&quot;javax.persistence.fetchgraph&quot;, em.getEntityGraph(&quot;Order.withMember&quot;));
  Order order2 = em.find(Order.class, orderId, hints);</code></pre>
<p>  ⇒ 위의 경우 조회된 order2에는 엔티티 그래프가 적용되지 않고 처음 조회한 order1과 같은 인스턴스가 반환됨</p>
</li>
<li><p>fetchgraph, loadgraph의 차이</p>
<ul>
<li><strong>fetchgraph</strong>: 엔티티 그래프에 선택한 속성만 함께 조회</li>
<li><strong>loadgraph</strong>: 선택한 속성뿐 아니라 글로벌 fetch 모드가 FetchType.EAGER로 설정된 연관관계도 포함해서 조회.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 13주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-13%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-13%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sun, 23 Aug 2026 06:46:20 GMT</pubDate>
            <description><![CDATA[<h1 id="131-트랜잭션-범위의-영속성-컨텍스트">13.1 트랜잭션 범위의 영속성 컨텍스트</h1>
<ul>
<li>J2SE 환경에서 JPA 사용 시 개발자가 <strong>직접 엔티티 매니저를 생성하고 트랜잭션도 관리</strong>해야함.</li>
<li>스플이 또는 J2SE 컨테이너 환경 → JPA 사용 시, 컨테이너가 제공하는 전략 따라야함.</li>
</ul>
<h2 id="1-스프링-컨테이너의-기본-전략">(1) 스프링 컨테이너의 기본 전략</h2>
<aside>

<p><strong>트랜잭션 범위의 영속성 컨텍스트 전략:</strong></p>
<p>트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 동일함.</p>
<p>즉, 트랜잭션 시작 시, 영속성 컨텍스트를 생성하고 트랜잭션 종료 시 영속성 컨텍스트를 종료함. 또한, 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근함.</p>
</aside>

<ul>
<li>스프링 프레임워크의 경우 비즈니스 로직을 시작하는 서비스 계층에 <code>@Transactional</code> 어노테이션을 선언해 트랜잭션을 시작<ul>
<li>메소드 실행 직전 스프링의 트랜잭션 AOP가 먼저 동작함</li>
<li>대상 <strong>메소드 정상 종료 시에는 트랜잭션을 커밋</strong>하면서 종료</li>
</ul>
</li>
<li>트랜잭션 커밋 시:<ul>
<li><strong>JPA는 영속성 컨텍스트를 플러시해 변경 내용을 데이터베이스에 반영한 후, 트랜잭션을 커밋.</strong></li>
<li>즉, 영속성 컨텍스트의 변경 내용이 정상 반영됨</li>
</ul>
</li>
<li>예외 발생 시:<ul>
<li><strong>트랜잭션을 롤백하고 종료. 이때는 플러시를 호출하지 않음</strong></li>
</ul>
</li>
</ul>
<h3 id="예제-분석">예제 분석</h3>
<pre><code class="language-java">@Controller
class HelloController {
    @Autowired HelloService helloService;

    public void hello() {
        Member member = helloService.logic();
    }
}

@Service
class HelloService {

    @PersistenceContext //엔티티 매니저 주입
    EntityManager em;

    @Autowired Repository1 repository1;
    @Autowired Repository2 repository2;

    //트랜잭션 시작
    **@Transactional**
    public void logic() {
        repository.hello();

        //member는 영속 상태
        Member member = repository2.findMember();
        return member;
    } // 트랜잭션 종료
}

@Repository
class Repository1 {

    @PersistenceContext
    EntityManager em;

    public void hello() {
        em.xxx(); //영속성 컨텍스트 접근
    }
}

@Repository
class Repository2 {

    @PersistenceContext
    EntityManager em;

    public Member findMember() {
        return em.find(Member.class, &quot;id1&quot;); //영속성 컨텍스트 접근
    }
}</code></pre>
<ul>
<li><code>@PersistenceContext</code> 어노테이션: 스프링 컨테이너가 <strong>엔티티 매니저를 주입</strong></li>
</ul>
<h4 id="helloservicelogic">HelloService.logic()</h4>
<ul>
<li><code>@Transactional</code>을 선언해 메소드 호출 시, <strong>트랜잭션을 우선 시작</strong></li>
<li><code>repository2.findMember()</code>를 통해 조회한 member 엔티티는 <strong>트랜잭션의 범위 내에 존재, 따라서 영속성 컨텍스트의 관리를 받음</strong> → 즉, 현재 영속 상태.</li>
<li><code>@Transactional</code> 선언 메소드 정상 종료 시, <strong>트랜잭션을 커밋</strong>. 이때 <strong>영속성 컨텍스트 역시 종료</strong>. 따라서 조회한 엔티티는 <strong>준영속 상태</strong>가 됨.</li>
</ul>
<h4 id="트랜잭션이-같으면-같은-영속성-컨텍스트-사용">트랜잭션이 같으면 같은 영속성 컨텍스트 사용</h4>
<ul>
<li>다양한 위치에서 엔티티 매니저를 주입받아 사용해도 트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용함</li>
<li>위 코드의 두 번의 영속성 컨텍스트 접근 모두 동일한 영속성 컨텍스트를 사용함.</li>
</ul>
<h4 id="트랜잭션이-다르면-다른-영속성-컨텍스트를-사용">트랜잭션이 다르면 다른 영속성 컨텍스트를 사용</h4>
<ul>
<li>여러 스레드에서 동시에 요청이 와서 <strong>같은 엔티티 매니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다름</strong></li>
<li>즉, 스프링 컨테이너는 <strong>스레드마다 각각 다른 트랜잭션을 할당</strong>해 같은 엔티티 매니절ㄹ 호출해도 접근하는 영속성 컨텍스트가 달라, 멀티 스레드 상황에서도 안전함.</li>
</ul>
<hr>
<h2 id="132-준영속-상태와-지연-로딩">13.2 준영속 상태와 지연 로딩</h2>
<ul>
<li>조회한 엔티티가 서비스와 레포지토리 계층에서는 영속성 컨텍스트에 관리되며 영속 상태를 유지하지만, <strong>컨트롤러나 뷰 같은 프레젠테이션 계층에서는 준영속 상태</strong>가 됨.</li>
</ul>
<pre><code class="language-java">@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne (fetch = FetchType.LAZY) //지연로딩 전략
    private Member member;
}</code></pre>
<ul>
<li>프레젠테이션 계층에서 엔티티가 준영속 상태 ⇒ 변경 감지와 지연로딩이 동작하지 않음.</li>
<li>즉, 지연 로딩 시점에서 예외 발생 가능</li>
</ul>
<h4 id="준영속-상태와-변경-감지">준영속 상태와 변경 감지</h4>
<ul>
<li>변경감지기능의 경우 데터를 주로 보여주는 역할인 프레젠테이션 계층에서는 거의 발생하지 않음.</li>
<li>책임의 모호성이 발생하지 않게하기 위해서라도, 프레젠테이션 계층에서 데이터를 변경하지 안않고, 서비스 계층에서 비즈니스 로직을 처리하는 것이 좋음</li>
</ul>
<h4 id="준영속-상태와-지연-로딩">준영속 상태와 지연 로딩</h4>
<ul>
<li><p>뷰를 렌더링 할 때, 연관된 엔티티 역시 함께 사용해야하는데 이때, 연관 엔티티를 지연로딩으로 설정해 프록시 객체로 조회했을 때</p>
<p>  ⇒ 아직 초기화하지 않은 프록시 객체 사용 시, 실제 데이터를 불러오기 위해 초기화를 시도</p>
<ul>
<li>하지만 준영속 상태는 영속성 컨텍스트가 없기에 지연 로딩이 불가 → 예외가 발생함</li>
</ul>
</li>
<li><p>해결 방법:</p>
<ul>
<li>뷰가 필요한 엔티티를 미리 로딩하기</li>
<li>OSIV를 사용해 엔티티를 항상 영속 상태로 유지하기</li>
</ul>
</li>
</ul>
<h3 id="해결-방법1-뷰가-필요한-엔티티를-미리-로딩">해결 방법1: 뷰가 필요한 엔티티를 미리 로딩</h3>
<p>: 엔티티가 준영속 상태여도, 미리 로딩하거나 초기화해 반환하기에 지연 로딩이 발생하지 않음.</p>
<p>어디에서 미리 로딩하는지에 따라 3가지 방법이 존재</p>
<ul>
<li>글로벌 페치 전략 수정</li>
<li>JPQL 페치 조인</li>
<li>강제 초기화</li>
</ul>
<h4 id="1-글로벌-페치-전략-수정">(1) 글로벌 페치 전략 수정</h4>
<p>글로벌 페치 전략을 아래처러 지연 로딩에서 즉시로딩으로 변경</p>
<pre><code class="language-java">@Entity
public class Order {

    @Id @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.EAGER) //즉시 로딩 전략
    private Member member;
}</code></pre>
<ul>
<li><p>fetch 타입을 변경하면 애플리케이션 전체에서 해당 엔티티를 로딩할 때마다 해당 전략을 사용 ⇒ 그렇기에 글로벌 페치 전략이라고 부름</p>
</li>
<li><p>단점:</p>
<ul>
<li><p>사용하지 않는 엔티티를 로딩해야함</p>
</li>
<li><p>N+1 문제가 발생함</p>
<ul>
<li><p>N+1 문제: 처음 조회한 데이터 수만큼 다시 SQL을 사용해 조회하는 문제 → 조회 성능에 치명적이기에 최우선적인 최적화 대상</p>
<p>⇒ JPQL 페치 조인으로 해결 가능</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h4 id="2-jpql-페치-조인">(2) JPQL 페치 조인</h4>
<ul>
<li>JPQL 호출 시점에 함께 로딩할 엔티티를 선택하는 방식</li>
<li>글로벌 페치 전략을 즉시 로딩으로 설정할 시 애플리케이션 전체에 영향을 줘 비효율적.</li>
</ul>
<aside>

<p>페치 조인 사용 전</p>
<pre><code class="language-java">JPQL: select o from Order o
SQL: select * from Order</code></pre>
<p>페치 조인 사용 후</p>
<pre><code class="language-java">JPQL:
    select o
    from Order o
    **join fetch** o.member</code></pre>
</aside>

<ul>
<li>페치 조인 사용 시, SQL JOIN을 사용해 페치 조인 대상까지 함께 조회 → N+1 문제가 발생하지 않음. (연관 엔티티를 이미 로딩했기에 글로벌 페치 전략은 무의미.)</li>
<li>단점<ul>
<li>무분별하게 사용 시, 화면에 맞춘 레포지토리 메소드가 증가 → 프레젠테이션 계층이 데이터 접근 계층을 침법하는 것</li>
</ul>
</li>
</ul>
<h4 id="3-강제-초기화">(3) 강제 초기화</h4>
<ul>
<li>영속성 컨텍스트가 살아있을 때, 프레젠테이션 계층이 필요한 엔티티를 강제 초기화해 반환하는 방법</li>
</ul>
<pre><code class="language-java">class OrderService {

    @Transactional
    public Order findOrder (Long id) {
        Order order = orderRepository.findOrder(id);
        **order.getMember().getName(); //프록시 객체를 강제로 초기화함**

        return order;
    }
}</code></pre>
<ul>
<li>프레젠테이션 계층에서 필요한 프록시 객체를 <strong>영속성 컨텍스트가 살아 있을 때, 강제로 초기화해서 반환</strong>하면 이미 초기화했기에 준영속 상태에서도 사용 가능</li>
<li>하이버네이트 사용 시, <code>initialize()</code> 메소드를 사용해 프록시를 강제로 초기화 가능<ul>
<li>JPA 표준에는 프록시 초기화 메소드가 없으며, 초기화 여부만 확인 가능</li>
</ul>
</li>
</ul>
<h3 id="4-facade-계층-추가">(4) FACADE 계층 추가</h3>
<ul>
<li>프레젠테이션 계층과 서비스 계층 사이에 FACADE 계층을 하나 더 두는 방법</li>
<li>뷰를 위한 프록시 초기화는 서비스 계층이 아니라 FACADE 계층에서 담당</li>
</ul>
<aside>

<h4 id="facade-계층의-역할과-특징">FACADE 계층의 역할과 특징</h4>
<ul>
<li><p>프레젠테이션 계층과 도메인 모델 계층 간의 <strong>논리적 의존성을 분리</strong></p>
</li>
<li><p>프레젠테이션 계층에서 <strong>필요한 프록시 객체를 초기화</strong></p>
</li>
<li><p>서비스 계층을 호출해 비즈니스 로직을 실행</p>
</li>
<li><p>레포지토리를 직접 호출해 뷰가 요구하는 엔티티를 찾음</p>
</aside>
</li>
<li><p>예제코드</p>
</li>
</ul>
<pre><code class="language-java">class OrderFacade {

    @Autowired OrderService orderService;

    public Order findOrder(Long id) {
        Order order = orderService.findOrder(id);
        order.getMember().getName();

        return order;
    }
}

class OrderService {

    public Order findOrder(id) {
        return orderRepository.findOrder(id);
    }
}</code></pre>
<ul>
<li>이전에 OrderService에 있던 프록시 초기화 코드가 OrderFacade로 이동함</li>
</ul>
<h3 id="준영속-상태와-지연-로딩-문제점">준영속 상태와 지연 로딩 문제점</h3>
<ul>
<li>뷰를 개발할 때 필요한 엔티티를 미리 초기화하는 방법은 생각보다 오류 발생 가능성이 높음<ul>
<li>이유: 뷰 개발 시에는 엔티티 클래스를 보고 개발을 하는 것이지, 초기화 여부 확인을 위해 FACADE나 서비스 클래스까지 열어보는 것이 번거롭고 놓치기 쉬워서</li>
</ul>
</li>
<li>이런 단점을 해결하는 방법이 OSIV.</li>
</ul>
<hr>
<h2 id="133-osiv">13.3 OSIV</h2>
<p><strong>OSIV</strong>: 영속성 컨텍스트를 뷰까지 열어둔다는 의미.</p>
<ul>
<li>영속성 컨텍스트가 살아있으면 엔티티가 영속 상태로 유지되기 때문에, 뷰에서 지연로딩을 사용할 수 있음</li>
</ul>
<h3 id="1-과거-osiv-요청-당-트랜잭션">(1) 과거 OSIV: 요청 당 트랜잭션</h3>
<ul>
<li><p>단순한 구현방법: 클라이언트의 요청이 들어오자마자, 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하고, 요청이 끝날 때 트랜잭션 역시 종료하는 것</p>
<p>  ⇒ 이것을 <strong>요청 당 트랜잭션 방식의 OSIV</strong>라함.</p>
<ul>
<li>영속성 컨텍스트가 처음부터 끝까지 살아있기에 조회한 엔티티도 영속 상태를 유지함</li>
<li>뷰에서도 지연로딩이 가능하기에 엔티티를 미리 초기화할 필요가 없음</li>
</ul>
</li>
</ul>
<h4 id="문제점">문제점</h4>
<ul>
<li><p>컨트롤러나 뷰 같은 프레젠테이션 계층이 엔티티를 변경할 수 있음</p>
<p>  예) 보안상의 이유로 고객의 이름을 XXX로 변경해 뷰에 전달 → 뷰 렌더링 이후, 트랜잭션을 커밋해 고객의 이름이 변경된 고객의 이름 XXX로 변경되는 문제가 발생함.</p>
</li>
<li><p>프레젠테이션 계층에서 데이터 수정을 막는 방법:</p>
<ul>
<li>엔티티를 읽기 전용 인터페이스로 제공:<ul>
<li>엔티티를 직접 노출하는 대신에 읽기 전용 메소드만 제공하는 인터페이스를 프레젠테이션 계층에 제공</li>
</ul>
</li>
<li>엔티티 래핑<ul>
<li>엔티티의 읽기 전용 메소드만 가진 엔티티를 감싼 객체를 만들고 이것을 프레젠테이션 계층에 반환하는 방법</li>
</ul>
</li>
<li>DTO만 반환<ul>
<li>가장 전통적인 방법으로 프레젠테이션 계층에 엔티티 대신에 단순히 데이터만 전달하는 객체인 DTO를 생성해서 반환하는 것</li>
<li>단점: OSIV를 사용하는 장점을 살릴 수 없고, 엔티티를 거의 복사하는 듯한 DTO 클래스를 만들어야함</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="2-스프링-osiv-비즈니스-계층-트랜잭션">(2) 스프링 OSIV: 비즈니스 계층 트랜잭션</h3>
<h4 id="스프링-프레임워크가-제공하는-osiv-라이브러리">스프링 프레임워크가 제공하는 OSIV 라이브러리</h4>
<p>OSIV를 서블릿 필터에서 적용할지, 스프링 인터셉터에서 적용할지에 따라 원하는 클래스를 선택해 사용하면 됨.</p>
<ul>
<li>하이버네이트 OSIV 서블릿 필터</li>
<li>하이버네이트 OSIV 스프링 인터셉터</li>
<li>JPA OEIV 서블릿 필터</li>
<li>JPA OEIV 스프링 인터셉터</li>
</ul>
<h4 id="스프링-osiv-분석">스프링 OSIV 분석</h4>
<ul>
<li><p>스프링 프레임워크의 OSIV = 비즈니스 계층에서 트랜잭션을 사용하는 OSIV.</p>
<p>  ⇒ 즉, OSIV는 사용하지만, 트랜잭션은 비즈니스 계층에서만 사용함</p>
</li>
</ul>
<h4 id="동작-원리">동작 원리</h4>
<ul>
<li>클라이언트 요청이 들어오면 <strong>영속성 컨텍스트를 생성 (이때 트랜잭션은 시작X)</strong></li>
<li>서비스 계층에서 트랜잭션 시작 → 생성해둔 영속성 컨텍스트에 트랜잭션을 시작</li>
<li>비즈니스 로직을 실행하고 <strong>서비스 계층 종료 후, 트랜잭션을 커밋하면서 영속성 컨텍스트를 플러시</strong><ul>
<li>이때 트랜잭션만 종료하고, 영속성 컨텍스트는 살려둠</li>
</ul>
</li>
<li>클라이언트의 요청이 끝날 때, 영속성 컨텍스트를 종료</li>
</ul>
<h4 id="트랜잭션-없이-읽기">트랜잭션 없이 읽기</h4>
<ul>
<li>영속성 컨텍스트를 통한 모든 변경은 트랜잭션 안에서 이루어져야함<ul>
<li>만약, 트랜잭션 없이 엔티티를 변경하고 영속성 컨텍스트를 플러시하면 예외 발생</li>
</ul>
</li>
<li><strong>트랜잭션 없이 읽기</strong>: 엔티티를 변경하지 않고 단순히 조회만 할 때는 트랜잭션이 없어도 되는데 이를 이르는 말.</li>
<li>프레젠테이션 계층에서는 트랜잭션이 없기에, 엔티티를 수정할 수 없음. → 이전의 단점 보완.</li>
<li>트랜잭션 없이 읽기를 사용해 <strong>프레젠테이션 계층에서 지연된 로딩 사용 가능</strong></li>
</ul>
<aside>

<p><strong>스프링이 제공하는 비즈니스 계층 트랜잭션 OSIV 정리</strong></p>
<ul>
<li>영속성 컨텍스트를 프레젠테이션 계층까지 유지</li>
<li>프레젠테이션 계층에는 트랜잭션이 없으므로 엔티티 수정 불가</li>
<li>프레젠테이션 계층에는 트랜잭션이 없지만, 트랜잭션 없이 읽기를 사용해 지연 로딩 가능</aside>

</li>
</ul>
<h4 id="스프링-osiv-주의사항">스프링 OSIV 주의사항</h4>
<ul>
<li>프레젠테이션 계층에서 엔티티를 수정한 직후 트랜잭션을 시작하는 서비스 계층 호출 시 문제가 발생</li>
</ul>
<hr>
<h2 id="134-너무-엄격한-계층">13.4 너무 엄격한 계층</h2>
<p>아래 예제는 상품 구매 후 구매 결과 엔티티를 조회하기 위해 <strong>컨트롤러에서 레포지토리를 직접 접근</strong></p>
<pre><code class="language-java">class OrderController {
    @Autowired OrderService orderService;
    @Autowired OrderRepository orderRepository;

    public String orderRequest (Order order, Model model) {
        long Id = orderService.order(order); //상품 구매

        //레포지토리 직접 접근
        Order orderResult = orderRepository.findOne(id);
        model.addAttribute(&quot;order&quot;, orderResult);
        ...
    }
}

@Transactional
class OrderService {
    @Autowired OrderRepository orderRepository;

    public Long order(order) {
        //...비즈니스 로직
        return orderRepository.save(order);
    }
}

class OrderRepository {
    @PersistenceContext EntityManager em;

    public Order findOne(Long id) {
        return em.find(Order.class, id);
    }
}</code></pre>
<ul>
<li>OSIV 사용 이전: 프레젠테이션 계층에서 사용할 지연 로딩 엔티티를 미리 초기화 해야했음<ul>
<li>초기화: 서비스 계층/FACADE 계층이 담당 (영속성 컨텍스트가 살아있는 계층)</li>
</ul>
</li>
</ul>
<p>⇒ OSIV 사용: 영속성 컨텍스트가 프레젠테이션 계층까지 살아있기에 미리 초기화할 필요X</p>
<ul>
<li>단순한 엔티티 조회의 경우, 컨트롤러에서 레포지토리 직접 호출해도 문제 없음</li>
<li>OSIV를 사용하면 과거 EJB 시절 DTO를 만들어 반환하고, 엔티티가 계층을 뛰어넘기 힘들다는 불편함을 줄이고 더 유연하고 실용적인 관점으로 접근할 수 있음.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 12주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-12%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-12%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Fri, 14 Aug 2026 04:55:05 GMT</pubDate>
            <description><![CDATA[<h2 id="121-스프링-데이터-jpa-소개">12.1 스프링 데이터 JPA 소개</h2>
<h4 id="스프링-데이터-jpa">스프링 데이터 JPA</h4>
<p>스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트.</p>
<ul>
<li><p>CRUD를 처리하기 위한 공통 인터페이스 제공</p>
</li>
<li><p>레포지토리 개발 시 → 인터페이스만 작성하면 실행 시점에서 구현 객체를 동적으로 생성해 주입.</p>
<p>  ⇒ <strong>데이터 접근 계층 개발 시, 구현 클래스 없이 인터페이스만으로 개발 완료 가능.</strong></p>
</li>
</ul>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    Member findByUsername(String username);
}</code></pre>
<ul>
<li>일반적은 CRUD 메소드 = <code>JpaRepository</code> 인터페이스가 공통으로 제공</li>
<li>직접 작성해 공통 처리가 불가능한 메소드 = 스프링 데이터 JPA가 메소드 이름을 분석해 JPQL을 실행</li>
</ul>
<pre><code class="language-java">MemberRepository.findByUsername();
// select m from Member m where username =: username;</code></pre>
<h3 id="1-스프링-데이터-프로젝트">(1) 스프링 데이터 프로젝트</h3>
<h4 id="스프링-데이터-프로젝트">스프링 데이터 프로젝트</h4>
<p>JPA, MongoDB, NEO4J, REDIS, HADOOP, GEMFIRE 등 다양한 데이터 저장소에 대한 접근을 추상화</p>
<ul>
<li>스프링 데이터 JPA는 스프링 데이터 프로젝트의 하위 프로젝트 중 하나로, JPA에 특화된 기능을 제공.</li>
</ul>
<hr>
<h2 id="122-스프링-데이터-jpa-설정">12.2 스프링 데이터 JPA 설정</h2>
<ul>
<li><p>필요한 라이브러리</p>
<pre><code class="language-groovy">  &lt;dependency&gt;
      &lt;groupId&gt;org.springframework.data&lt;/groupId&gt;
      &lt;artifactId&gt;spring-data-jpa&lt;/artifactId&gt;
      &lt;version&gt;1.8.0.RELEASE&lt;/version&gt;
  &lt;/dependency&gt;</code></pre>
</li>
<li><p>환경설정</p>
<ul>
<li><p>스프링 설정에 XML 사용 시 <code>&lt;jpa:repositories&gt;</code>를 사용하고 레포지토리를 검색할 <code>base-package</code>를 적음. → 해당 패키지와 그 하위 패키지를 검색함</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
  xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
  xmlns:jpa=&quot;http://www.springframework.org/schema/data/jpa&quot;
  xsi:schemaLocation=&quot;http://www.sprintframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/data/jpa
      http://www.springframework.org/schema/jpa/spring-jpa.xsd&quot;&gt;

  &lt;jpa:repositories base-package=&quot;jpabook.jpashop.repository&quot;/&gt;
&lt;/beans&gt;</code></pre>
</li>
<li><p>스프링 설정에 JavaConfig를 사용하면 <code>@EnableJpaRepositories</code>  어노테이션을 추가하고 basePackages에는 레포지토리를 검색할 패키지 위치를 적음</p>
<pre><code class="language-java">@Configuration
@EnableJpaRepositories(basePackages = &quot;jpabook.jpashop.repository&quot;)
public class AppConfig {}</code></pre>
</li>
</ul>
</li>
</ul>
<p>⇒ 스프링 데이터 JPA는 애플리케이션 실행 시 basePackage에 있는 레포지토리 인터페이스들을 찾아 해당 인터페이스를 구현한 클래스를 동적으로 생성한 후 스프링 빈으로 등록. 즉, 개발자가 직접 구현 클래스를 만들지 않아도 됨.</p>
<hr>
<h2 id="123-공통-인터페이스-기능">12.3 공통 인터페이스 기능</h2>
<ul>
<li><p>스프링 데이터 JPA에서 제공하는 JpaRepository 인터페이스 사용을 위해서는 <strong>해당 인터페이스를 상속받고, 제너릭에 &lt;엔티티 클래스, 해당 클래스가 사용하는 식별자 타입&gt;을 지정</strong>하면 됨.</p>
<pre><code class="language-java">  public interface JpaRepository&lt;T, ID extends Serializable&gt; extends PagingAndSortingRepository&lt;T, ID&gt;

  public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {}</code></pre>
</li>
</ul>
<img src="https://velog.velcdn.com/images/summeryoung_/post/87d77924-fb63-45b0-a587-0a33f831bf51/image.png" width=70%>


<ul>
<li>위: 스프링 데이터 모듈, 그 안에 Repository, CrudRepository, PagingAndSortingRepository → 이 인터페이스는 스프링 데이터 프로젝트가 공통으로 사용하는 인터페이스.</li>
<li>T: 엔티티, ID: 엔티티의 식별자 타입, S: 엔티티와 그 자식 타입</li>
</ul>
<h4 id="주요-메소드">주요 메소드</h4>
<ul>
<li><code>save(S)</code>: 새로운 엔티티는 저장하고 이미 있는 엔티티는 수정<ul>
<li>엔티티에 식별자 값이 없으면 새로운 엔티티로 판단해 <code>EntityManager.persist()</code>를 호출, 식별자 값이 있으면 이미 있는 엔티티로 판단해 <code>EntityManager.merge()</code>를 호출.</li>
</ul>
</li>
<li><code>delete(T)</code>: 엔티티 하나를 삭제. 내부에서는 <code>EntityManager.remove()</code>를 호출.</li>
<li><code>findOne(ID)</code>: 엔티티 하나를 조회. 내부에서 <code>EntityManager.getReference()</code>를 호출.</li>
<li><code>findAll(...)</code>: 모든 엔티티를 조회</li>
</ul>
<hr>
<h2 id="124-쿼리-메소드-기능">12.4 쿼리 메소드 기능</h2>
<p>대표적인 쿼리 메소드 기능으로는 <strong>이름만으로 쿼리를 생성하는 기능</strong>이 존재. 인터페이스에 메소드만 선언하면 <strong>해당 메소드의 이름으로 적절한 JPQL을 생성해 실행</strong>함.</p>
<p>스프링 데이터 JPA에서 제공하는 쿼리 메소드 기능은 크게 3가지 존재.</p>
<ul>
<li>메소드 이름으로 쿼리 생성</li>
<li>메소드 이름으로 JPA NamedQuery 호출</li>
<li><code>@Query</code> 어노테이션을 사용해 레포지토리 인터페이스에 쿼리 직접 정의</li>
</ul>
<h4 id="1-메소드-이름으로-쿼리-작성">(1) 메소드 이름으로 쿼리 작성</h4>
<p>예) 이메일과 이름으로 회원 조회</p>
<pre><code class="language-java">pulibc interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    List&lt;Member&gt; findByEmailAndName(String email, String name);
}</code></pre>
<ul>
<li><p>위의 메소드를 실행하면 스프링 데이터 JPA는 메소드 이름을 분석해 JPQL을 생성하고 실행</p>
</li>
<li><p>실행된 JPQL</p>
<pre><code class="language-java">  select m from Member m where m.email = ?1 and m.name = ?2;</code></pre>
</li>
<li><p>단, 정해진 규칙에 따라 메소드 이름을 지어야함.</p>
</li>
</ul>
<h3 id="2-jpa-namedquery">(2) JPA NamedQuery</h3>
<p>쿼리에 이름을 부여해 사용하는 방법으로 어노테이션 또는 XML에 쿼리를 정의할 수 있음. 같은 방법으로 Named 네이티브 쿼리 역시 지원.</p>
<h4 id="namedquery-어노테이션-사용"><code>@NamedQuery</code> 어노테이션 사용</h4>
<pre><code class="language-java">@Entity
@NamedQuery(
    name = &quot;Member.findByUsername&quot;
    query = &quot;select m from Member m where m.username = :username&quot;)
public class Member {
    ...
}</code></pre>
<h4 id="ormxml의-xml-사용">orm.xml의 XML 사용</h4>
<pre><code class="language-groovy">&lt;named-query name=&quot;Member.findByUsername&quot;&gt;
    &lt;query&gt;&lt;CDATA[
        select m
        from Member m
        where m.username = :username
    ]&gt;&lt;/query&gt;
&lt;named-query&gt;</code></pre>
<p>위처럼 정의한 Named 쿼리를 JPA에서 직접 호출하기 위해서는 아래처럼 코드를 작성해야함</p>
<pre><code class="language-java">public class MemberRepository {

    public List&lt;Member&gt; findByUsername(String username) {
        ...
        List&lt;Member&gt; resultList =
            em.createNamedQuery(&quot;Member.findByUsername&quot;, Member.class)
            .setParameter(&quot;username&quot;, &quot;회원1&quot;)
            .getResultList();
    }
}</code></pre>
<h4 id="스프링-데이터-jpa-사용">스프링 데이터 JPA 사용</h4>
<p>메소드 이름만으로 Named 쿼리 호출 가능</p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    List&lt;Member&gt; findByUsername(@Param(&quot;username&quot; String username);
}</code></pre>
<ul>
<li>스프링 데이터 JPA는 선언한 <strong>“도메인 클래스 + .(점) + 메소드이름”</strong>으로 Named 쿼리를 찾아서 실행</li>
<li>실행할 Named 쿼리가 없으면 메소드 이름으로 쿼리 생성 전략을 사용.</li>
<li><strong><code>@Param</code>: 이름기반 파라미터를 바인딩하기 위해서 사용하는 어노테이션.</strong></li>
</ul>
<h3 id="3-query-레포지토리-메소드에-쿼리-정의">(3) <code>@Query</code>, 레포지토리 메소드에 쿼리 정의</h3>
<p>레포지토리 메소드에 직접 쿼리를 정의하기 위해서 <code>@Query</code> 어노테이션을 사용.</p>
<p>정적 쿼리를 직접 작성하는 방법으로 이름 없는 Named 쿼리라 볼 수 있음. 또한, JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다는 장점 존재.</p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {

    @Query(&quot;select m from Member m where m.username = ?1&quot;)
    Member findByUsername(String username);
}</code></pre>
<p>네이티브 SQL 사용시에는 <code>@Query</code> 어노테이션에 <code>nativeQuery = true</code>를 설정.</p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {

    @Query(value = &quot;SELECT * FROM MEMBER WHERE USERNAME = ?0&quot;,
        nativeQuery = true)
    Member findByUsername(String username);
}</code></pre>
<h3 id="4-파라미터-바인딩">(4) 파라미터 바인딩</h3>
<p>스프링 데이터 JPA는 위치 기반 파라미터 바인딩과 이름 기반 파라미터 바인딩을 모두 지원함</p>
<pre><code class="language-groovy">select m from Member m where m.username = ?1 //위치기반
select m from Member m whre m.username = :name //이름기반</code></pre>
<ul>
<li>기본값은 위치 기반으로 파라미터 순서로 바인딩.</li>
<li>이름 기반 파라미터 바인딩 사용을 위해서는 <code>@Param</code> 어노테이션을 사용</li>
</ul>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {

    @Query(&quot;select m from Member m where m.username = :name&quot;)
    Member findByUsername(@Param(&quot;name&quot;) String username);
}</code></pre>
<h3 id="5-벌크성-수정-쿼리">(5) 벌크성 수정 쿼리</h3>
<ul>
<li>JPA로 작성한 벌크성 수정 쿼리</li>
</ul>
<pre><code class="language-java">int bulkPriceUp (String stockAmount) {
    ...
    String qlString =
        &quot;update Product p set p.price = p.price * 1.1 
            where p.stockAmount &lt; :stockAmount&quot;;

        int resultCount = em.createQuery(qlString)
                                                .setParameter(&quot;stockAmount&quot;, stockAmount);
                                                .executeUpdate();
}</code></pre>
<ul>
<li>스프링 데이터 JPA를 사용한 벌크성 수정 쿼리<ul>
<li>스프링 데이터 JPA에서 <strong>벌크성 수정, 삭제 쿼리는 <code>@Modifying</code> 어노테이션을 사용</strong>하면 됨.</li>
<li>벌크성 쿼리 실행 후, 영속성 컨텍스트 초기화를 위해서는 <code>@Modifying(clearAutomatically=true</code>) 처럼 <code>clearAutomatically</code> 옵션을 true로 설정하면 됨. (기본값은 false)</li>
</ul>
</li>
</ul>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {

    **@Modifying**
    @Query(&quot;update Product p set p.price = p.price * 1.1 &quot;+
        &quot;where p.stockAmount &lt; :stockAmount&quot;)
    int bulkPriceUp(@Param(&quot;stockAmount&quot;) String stockAmount);
}</code></pre>
<h3 id="6-반환-타입">(6) 반환 타입</h3>
<ul>
<li>스프링 데이터 JPA는 유연한 반환 타입을 지원<ul>
<li>결과가 한건 이상 → 컬렉션 인터페이스를 사용</li>
<li>결과가 단건 → 반환 타입을 지정</li>
</ul>
</li>
<li>조회 결과가 없는 경우<ul>
<li>컬렉션 → 빈 컬렉션 반환</li>
<li>단건 → null 반환</li>
</ul>
</li>
<li>단건을 기대하고 반환 타입을 지정했는데, 결과가 2건 이상 조회되는 경우에는 <code>NonUniqueResultException</code> 예외가 발생.</li>
</ul>
<p>+) 단건으로 지정한 메소드 호출 시, 스프링 데이터 JPA는 내부에서 JPQL의 <code>Query.getSingleResult()</code> 메소드를 호출함. 조회 결과 없을 때에는 <code>NoResultException</code> 예외가 발생하는데 스프링 데이터 JPA에서는 이 예외 발생시 예외를 무시하고 null을 반환.</p>
<h3 id="7-페이징과-정렬">(7) 페이징과 정렬</h3>
<p>스프링 데이터 JPA는 쿼리 메소드에 <strong>페이징과 정렬 기능</strong>을 사용할 수 있도록 2가지 특별한 파라미터를 제공함</p>
<ul>
<li>Sort: 정렬 기능</li>
<li>Pageable: 페이징 기능 (내부에 Sort 포함)</li>
</ul>
<p><code>Pageable</code>을 사용하면 반환 타입으로 List나 Page를 사용할 수 있음. 반환 타입을 Page를 사용하면 스프링 데이터 JPA는 페이징 기능 제공을 위해 검색된 전체 데이터 건수를 조회하는 count 쿼리를 추가로 호출함.</p>
<pre><code class="language-java">///count 쿼리 사용
Page&lt;Member&gt; findByName(String name, Pageable pageable);

//count 쿼리 사용X
List&lt;Member&gt; findByName(String name, Pageable pageable);

List&lt;Member&gt; findByName(String name, Sort sort);</code></pre>
<aside>

<p>예시:</p>
<ul>
<li>검색 조건: 이름이 김으로 시작하는 회원</li>
<li>정렬 조건: 이름으로 내림차순</li>
<li>페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 10건</aside>

</li>
</ul>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {

    Page&lt;Member&gt; findByNameStartingWith(String name, Pageable Pageable);
}

//페이징 조건과 정렬 조건 설정
PageRequest pageRequest = 
    new PageRequest(0, 10, new Sort(Direction.DESC, &quot;name&quot;);

Page&lt;Member&gt; result =
    memberRepository.findByNameStartingWith(&quot;김&quot;, pageRequest);

List&lt;Member&gt; members = result.getContent(); //조회된 데이터
int totalPages = result.getTotalPages(); //전체 페이지 수
boolean hasNextPage = result.hasNextPage(); //다음 페이지 존재 여부
</code></pre>
<ul>
<li>여기서 <code>Pageable</code>은 인터페이스로 실제 사용할 때는 이 인터페이스를 구현한 <code>PageRequest</code>를 사용함.</li>
</ul>
<h4 id="pagerequest">PageRequest</h4>
<ul>
<li><strong>생성자의 첫 번째 파라미터 = 현재 페이지, 두 번째 파라미터 = 조회할 데이터의 수, 추가적으로 정렬 정보 역시 파라미터로 사용 가능.</strong></li>
</ul>
<h4 id="page-인터페이스의-메소드">Page 인터페이스의 메소드</h4>
<pre><code class="language-java">public interface Page&lt;T&gt; extends Iterable&lt;T&gt; {

    int getNumber(); //현재 페이지
    int getSize(); //페이지 크기
    int getTotalPages(); //전체 페이지 수
    int getNumberOfElements(); //현재 페이지에 나올 데이터 수
    long getTotalElements(); //전체 데이터 수

    boolean hasPreviousPage(); //이전 페이지 여부
    boolean isFirstPage(); //현재 페이지가 첫번째인지의 여부
    boolean hasNextPage(); //다음 페이지 여부
    boolean isLastPage(); //마지막 페이지인지의 여부

    Pageable previousPageable(); //이전 페이지 객체, 없으면 null
    Pageable nextPageable(); //다음 페이지 객체, 없으면 null

    List&lt;T&gt; getContent(); //조회된 데이터
    boolean hasContent(); //조회된 데이터 존재 여부

    Sort getSort(); //정렬 정보
}</code></pre>
<p>Pageable과 Page를 사용하면 반복적인 페이징 처리를 손쉽게 개발 가능함.</p>
<h3 id="8-힌트">(8) 힌트</h3>
<p>JPA 쿼리 힌트를 사용하기 위해서는 <code>@QueryHints</code> 어노테이션을 사용하면 됨. SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트.</p>
<pre><code class="language-java">@QueryHints(value = {
        @QueryHint(name = &quot;readOnly&quot;, value =&quot;true&quot;)}, forCounting = true)
        Page&lt;Member&gt; findByName(String name, Pageable pageable);
    }</code></pre>
<p><code>forCounting</code> 속성은 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리에도 쿼리힌트를 적용할지 설정하는 옵션 (기본값은 true)</p>
<h3 id="9-lock">(9) Lock</h3>
<p>쿼리 시 락을 걸기위해서는 <code>@Lock</code> 어노테이션을 사용하면 됨.</p>
<pre><code class="language-java">@Lock(LockModeType.PESSIMISTIC_WRITE)
List&lt;Member&gt; findByName(String name);</code></pre>
<hr>
<h2 id="125-명세">12.5 명세</h2>
<p>명세라는 개념을 스프링 데이터 JPA는 JPA Criteria로 해당 개념을 사용할 수 있도록 지원함.</p>
<p>명세: 핵심 단어는 술어로 단순히 참이나 거짓으로 평가됨. 이는 AND/OR 같은 연산자로 조합할 수 있음. 예로는 데이터 검색을 위한 제약조건 하나하나를 술어라 할 수 있음.</p>
<p>→ 이 술어를 <code>Specification</code> 클래스로 정의함.</p>
<ul>
<li>컴포지트 패턴으로 구성되어, 여러 Specification을 조합할 수 있음. → 다양한 검색 조건을 조립해 새로운 검색 조건을 쉽게 만들 수 있음.</li>
</ul>
<p>명세 기능 사용을 위해서는 <code>JpaSpecificationExecutor</code> 인터페이스를 상속받으면 됨.</p>
<pre><code class="language-java">public interface OrderRepository extends JpaRepository&lt;Order, Long&gt;, JpaSpecificationExecutor&lt;Order&gt; {}</code></pre>
<pre><code class="language-java">public interface JpaSpecificationExecutor&lt;T&gt; {

    T findOne(Specification&lt;T&gt; spec);
    List&lt;T&gt; findAll(Specification&lt;T&gt; spec);
    Page&lt;T&gt; findAll(Specification&lt;T&gt; spec, Pageable pageable);
    List&lt;T&gt; findAll(Specification&lt;T&gt; spec, Sort sort);
    long count(Specifiaction&lt;T&gt; spec);
}</code></pre>
<ul>
<li>JpaSpecificationExecutor의 메소드들은 Specification을 파라미터로 받아서 검색 조건으로 사용함.</li>
</ul>
<h4 id="명세-사용-예제">명세 사용 예제</h4>
<pre><code class="language-java">public List&lt;Order&gt; findOrders(String name) {

    List&lt;Order&gt; result = orderRepository.findAll(
        where(memberName(name)).and(isOrderStatus())
    );

    return result;
}</code></pre>
<ul>
<li><code>Specifiaction</code>은 명세들을 조립할 수 있도록 도와주는 클래스로 <code>where()</code>, <code>and()</code>, <code>or()</code>, <code>not()</code> 메소드를 제공함.</li>
<li>위의 findAll을 보면 회원 이름 명세(memberName)와 주문 상태 명세(isOrderStatus)를 and로 조합해 검색 조건으로 사용</li>
</ul>
<h4 id="orderspec을-정의하는-코드">OrderSpec을 정의하는 코드</h4>
<ul>
<li>명세 정의를 위해서는 <code>Specifiaction</code> 인터페이스를 구현하면 됨.</li>
<li>명세를 정의할 때는 <code>toPredicate()</code>  메소드를 구현하면 됨.<ul>
<li>이때, JPA Criteria의 Root, CriteriaQuery, CriteriaBuilder 클래스가 모두 파라미터로 주어짐.</li>
<li>위의 파라미터들을 활용해 적절한 검색 조건을 반환하면 됨.</li>
</ul>
</li>
</ul>
<pre><code class="language-java">public class OrderSpec {

    public static Specification&lt;Order&gt; memberName(final String memberName) {

        return new Specification&lt;Order&gt;() {
            public Predicate toPredicate(Root&lt;Order&gt; root, CriteriaQuery&lt;?&gt; query, 
            CriteriaBuilder builder) {

                if (StringUtils.isEmpty(memberName)) return null;

                Join&lt;Order, Member&gt; m = root.join(&quot;member&quot;, JoinType.INNER); //회원과 조인

                return builder.equal(m.get(&quot;name&quot;), memberName);
            }
        };

    }

    public static Specifiaction&lt;Order&gt; isOrderStatus() {

        return new Specification&lt;Order&gt;() {
            public Predicate toPredicate(Root&lt;Order&gt; root, CriteriaQuery&lt;?&gt; query, 
            CriteriaBuilder builder) {

                return builder.equal(root.get(&quot;status&quot;), OrderStatus.ORDER);
            }
        };
    }
}</code></pre>
<hr>
<h2 id="126-사용자-정의-레포지토리-구현">12.6 사용자 정의 레포지토리 구현</h2>
<p>스프링 데이터 JPA로 레포지토리를 개발할 때 인터페이스의 정의만으로 가능하기도 하지만, <strong>메소드를 직접 구현</strong>해야하는 경우 역시 존재. 이때 공통 인터페이스가 제공하는 기능까지 모두 구현을 해야하는데, 스프링 데이터 JPA에서 해당 문제를 우회해 필요한 메소드만 구현할 수 있는 방법을 제공함.</p>
<h4 id="1-사용자-정의-인터페이스-작성">1. 사용자 정의 인터페이스 작성</h4>
<p>직접 구현할 메소드를 위한 사용자 정의 인터페이스를 작성. 이때 이름은 자유.</p>
<pre><code class="language-java">public interface MemberRepositoryCustom {
    public List&lt;Member&gt; findMemberCustom();
}</code></pre>
<h4 id="2-사용자-정의-인터페이스-구현-클래스-작성">2. 사용자 정의 인터페이스 구현 클래스 작성</h4>
<p>이때는 클래스 이름을 짓는 규칙 존재 → <strong>“레포지토리 인터페이스 이름 + Impl”</strong>로 지어야함. 이렇게 해야 스프링 데이터 JPA가 사용자 정의 구현 클래스로 인식함.</p>
<ul>
<li><p>사용자 정의 구현 클래스 이름 끝에 Impl 대신 다른 이름을 붙이려면 repository-impl-postfix 속성을 변경하면 됨. (기본값은 Impl)</p>
<ul>
<li><p>JavaConfig 설정:</p>
<p>  <code>@EnableJpaRepositories(basePackages = &quot;jpabook.jpashop.repository&quot;, repositoryImplementationPostfix = &quot;Impl&quot;)</code></p>
</li>
</ul>
</li>
</ul>
<pre><code class="language-java">public class MemberRepositoryImpl implements MemberRepositoryCustom {

    @Override
    public List&lt;Member&gt; findMemberCustom() {
        ..//사용자 정의 구현
    }
}</code></pre>
<h4 id="3-레포지토리-인터페이스에서-사용자-정의-인터페이스-상속">3. 레포지토리 인터페이스에서 사용자 정의 인터페이스 상속</h4>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt;, MemberRepositoryCustom {

}</code></pre>
<hr>
<h2 id="127-web-확장">12.7 Web 확장</h2>
<p>스프링 데이터 프로젝트는 스프링 MVC에서 사용 가능한 편리한 기능을 제공. 식별자로 도메인 클래스를 바로 바인딩해주는 도메인 클래스 컨버터 기능, 페이징과 정렬 기능 존재.</p>
<h3 id="1-설정">(1) 설정</h3>
<p>스프링 데이터가 제공하는 Web 확장 기능을 활성화하기 위해서는 <code>SpringDataWebConfiguration</code>을 스프링 빈으로 등록해야함</p>
<pre><code class="language-groovy">&lt;bean class=&quot;org.springframework.data.web.config.SpringDataWebConfiguration&quot; /&gt;</code></pre>
<p>JavaConfig를 사용할 경우 아래처럼 <code>@EnableSpringDataWebSupport</code> 어노테이션을 사용하면 됨.</p>
<pre><code class="language-java">@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
public class WebAppConfig {
    ...
}</code></pre>
<p>설정 완료 시 도메인 클래스 컨버터와 페이징, 정렬을 위한 <code>HandlerMethodArgumentResolver</code>가 스프링 빈으로 등록됨. 등록되는 도메인 클래스 컨버터는 <code>DomainClassConverter</code>.</p>
<h3 id="2-도메인-클래스-컨버터-기능">(2) 도메인 클래스 컨버터 기능</h3>
<p>도메인 클래스 컨버터: HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아 바인딩 해줌.</p>
<p>예) 특정 회원 수정 화면을 보여줄 때, 컨트롤러는 HTTP 요청으로 넘어온 회원의 아이디를 사용해 레포지토리를 통해 회원 엔티티를 조회해야함.</p>
<h4 id="적용-전">적용 전</h4>
<p>수정 화면 요청 URL: /member/memberUpdateForm?Id=1</p>
<ul>
<li>컨트롤러에서 파라미터로 넘어온 회원 아이디로 회원 엔티티를 찾음 → 찾은 회원 엔티티를 <code>model</code>을 사용해 뷰에 넘겨줌</li>
</ul>
<pre><code class="language-java">@Controller
public class MemberController {

    @Autowired MemberRepository memberRepository;

    @RequestMapping(&quot;member/memberUpdateForm&quot;)
    public String memberUpdateForm(@RequestParam(&quot;id&quot;) Long id, Model model) {

        Member member = memberRepository.findOne(id); //회원을 찾음
        model.addAttribute(&quot;member&quot;, member);

        return &quot;member/memberSaveForm&quot;;
    }
}</code></pre>
<h4 id="적용-후">적용 후</h4>
<pre><code class="language-java">@Controller
public class MemberController {

    @RequestMapping(&quot;member/memberUpdateForm&quot;)
    public String memberUpdateForm(@RequestParam(&quot;id&quot;) Member member, Model model){

        model.addAttribute(&quot;member&quot;, member);

        return &quot;member/memberSaveForm&quot;;
    }
}</code></pre>
<p><code>@RequestParam(&quot;id&quot;) Member member</code> </p>
<ul>
<li>HTTP 요청으로 회원 아이디를 받지만, 도메인 클래스 컨버터가 중간에 동작해 아이디를 회원 엔티티 객체로 변환해 넘겨줘 컨트롤러에서 단순하게 사용 가능</li>
<li>도메인 클래스 컨버터는 해당 엔티티와 관련된 레포지토리를 사용해 엔티티를 찾음.</li>
</ul>
<h3 id="3-페이징과-정렬-기능">(3) 페이징과 정렬 기능</h3>
<p>스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있도록 <code>HandlerMethodArgumentResolver</code>를 제공</p>
<ul>
<li>페이징 기능: PageableHandlerMethodArgumentResolver</li>
<li>정렬 기능: SortHandlerMethodArgumentResolver</li>
</ul>
<pre><code class="language-java">@RequestMapping(value=&quot;/members&quot;, method=RequestMethod.GET)
public String list(Pageable pageable, Model model) {

    Page&lt;Member&gt; page = memberService.findMembers(pageable);
    model.addAttribute(&quot;members&quot;, page.getContent());

    return &quot;members/memberList&quot;;
}</code></pre>
<ul>
<li>파라미터로 Pageable을 받고, Pageable은 다음 요청 파라미터 정보로 만들어짐.</li>
<li>요청 파라미터:<ul>
<li>page: 현재 페이지, 0부터 시작</li>
<li>size: 한 페이지에 노출할 데이터 건수</li>
<li>sort: 정렬 조건을 정의. 정렬 방향을 변경하려면 <code>sort</code> 파라미터를 추가하면됨.   예) ASC, DESC</li>
</ul>
</li>
</ul>
<h4 id="접두사">접두사</h4>
<p>사용해야할 페이징 정보가 둘 이상일 때 접두사를 사용해 구분. 접두사는 스프링 프레임워크가 제공하는 <code>@Qualifier</code> 어노테이션을 사용. “{접두사명}_”으로 구분.</p>
<pre><code class="language-java">public String list(
    @Qualifier(&quot;member&quot;) Pageable memberPageable,
    @Qualifier(&quot;order&quot;) Pageable orderPageable, ...
)</code></pre>
<p>예: /members?member_page=0&amp;order_page=1</p>
<h4 id="기본값">기본값</h4>
<p>Pageable의 기본값은 page=0, size-20. 기본값 변경을 위해서는 <code>@PageableDefault</code> 어노테이션을 사용</p>
<pre><code class="language-java">@RequestMapping(value=&quot;members_page&quot;, method=RequestMethod.GET)
public String list(@PageableDefault(size=12, sort=&quot;name&quot;, 
                                        direction=Sort.Direction.DESC) Pageable pageable) {
    ...
}</code></pre>
<hr>
<h2 id="128-스프링-데이터-jpa가-사용하는-구현체">12.8 스프링 데이터 JPA가 사용하는 구현체</h2>
<p>스프링 데이터 JPA가 제공하는 공통 인터페이스는 <code>SimpleJpaRepository</code> 클래스가 구현</p>
<ul>
<li><code>@Repository</code> 적용: JPA 예외를 스프링이 추상화한 예외로 변환</li>
<li><code>@Transactional</code> 트랜잭션 적용: JPA의 모든 변경은 트랜잭션 안에서 이루어져야함. 스프링 데이터 JPA가 제공하는 공통 인터페이스를 사용하면 데이터를 변경(등록/수정/삭제)하는 메소드에 <code>@Transactional</code>로 트랜잭션 처리가 되어있음.</li>
<li><code>@Transactional(readOnly=true)</code>: 데이터를 조회하는 메소드에 적용. 데이터 변경하지 않는 트랜잭션에서 적용해 플러시를 생략해 약간의 성능 향상을 얻을 수 있음.</li>
<li><code>save()</code> 메소드: 저장할 엔티티가 새로운 엔티티이면 이미 저장, 아니면 병합. 필요 시 <code>Persistable</code> 인터페이스 구현해 판단 로직 변경 가능.</li>
</ul>
<hr>
<h2 id="129-jpa-샵에-적용">12.9 JPA 샵에 적용</h2>
<p>기존의 웹 애플리케이션에 적용. 환경설정 → 레포지토리 리팩토링 → 명세 적용 → 기타.</p>
<h3 id="1-환경설정">(1) 환경설정</h3>
<ul>
<li>pom.xml에 spring-data-jpa 라이브러리를 추가.</li>
</ul>
<pre><code class="language-groovy">&lt;!-- 스프링 데이터 JPA --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.data&lt;/groupId&gt;
            &lt;artifactId&gt;spring-data-jpa&lt;/artifactId&gt;
            &lt;version&gt;${spring-data-jpa.version}&lt;/version&gt;
        &lt;/dependency&gt;</code></pre>
<ul>
<li>appConfig에 <a href="jpa:repositories">jpa:repositories</a>를 추가하고 base-package 속성에 레포지토리 위치를 지정</li>
</ul>
<pre><code class="language-groovy">&lt;jpa:repositories base-package=&quot;jpabook.jpashop.repository&quot; /&gt;</code></pre>
<h3 id="2-레포지토리-리팩토링">(2) 레포지토리 리팩토링</h3>
<h4 id="회원-레포지토리">회원 레포지토리</h4>
<pre><code class="language-groovy">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {

    List&lt;Member&gt; findByName(String name);
}</code></pre>
<p>같은 방식으로 상품 등 다른 레포지토리 역시 리팩토링.</p>
<h3 id="3-명세-적용">(3) 명세 적용</h3>
<p>명세 검색 기능을 위해 레포지토리에 JpaSpecificationExecutor을 추가로 상속. 명세 작성을 위해 <code>OrderSpec</code>을 추가.</p>
<pre><code class="language-groovy">public class OrderSpec {

    public static Specification&lt;Order&gt; memberNameLike(final String memberName) {
        return new Specification&lt;Order&gt;() {
            public Predicate toPredicate(Root&lt;Order&gt; root, CriteriaQuery&lt;?&gt; query, CriteriaBuilder builder) {

                if (StringUtils.isEmpty(memberName)) return null;

                Join&lt;Order, Member&gt; m = root.join(&quot;member&quot;, JoinType.INNER); //회원과 조인
                return builder.like(m.&lt;String&gt;get(&quot;name&quot;), &quot;%&quot; + memberName + &quot;%&quot;);
            }
        };
    }

    public static Specification&lt;Order&gt; orderStatusEq(final OrderStatus orderStatus) {
        return new Specification&lt;Order&gt;() {
            public Predicate toPredicate(Root&lt;Order&gt; root, CriteriaQuery&lt;?&gt; query, CriteriaBuilder builder) {

                if (orderStatus == null) return null;

                return builder.equal(root.get(&quot;status&quot;), orderStatus);
            }
        };
    }
}</code></pre>
<ul>
<li>OrderSearch 객체에 자신이 가진 검색조건으로 Specification을 생성하도록 코드 추가</li>
</ul>
<pre><code class="language-groovy">public class OrderSearch {

    private String memberName;      //회원 이름
    private OrderStatus orderStatus;//주문 상태

    public String getMemberName() {
        return memberName;
    }

    public void setMemberName(String memberName) {
        this.memberName = memberName;
    }

    public OrderStatus getOrderStatus() {
        return orderStatus;
    }

    public void setOrderStatus(OrderStatus orderStatus) {
        this.orderStatus = orderStatus;
    }

    **public Specifications&lt;Order&gt; toSpecification() {
        return where(memberNameLike(memberName))
                .and(orderStatusEq(orderStatus));
    }**

}</code></pre>
<ul>
<li>기존 레포지토리 검색 코드가 명세를 파라미터로 넘기도록 변경</li>
</ul>
<pre><code class="language-groovy">public List&lt;Order&gt; findOrders(OrderSearch orderSearch) {
        return orderRepository.findAll(orderSearch.toSpecification());
}</code></pre>
<hr>
<h2 id="1210-스프링-데이터-jpa와-querydsl-통합">12.10 스프링 데이터 JPA와 QueryDSL 통합</h2>
<p>스프링 데이터 JPA가 QueryDSL을 지원하는 방법은 2가지 존재.</p>
<ul>
<li>QueryDslPredicateExecutor</li>
<li>QueryDslRepositorySupport</li>
</ul>
<h3 id="1-querydslpredicateexecutor-사용">(1) QueryDslPredicateExecutor 사용</h3>
<ul>
<li>레포지토리에서 <code>QueryDslPredicateExecutor</code>를 상속받으면 됨.</li>
</ul>
<pre><code class="language-groovy">public interface ItemRepository extends JpaRepository&lt;Item, Long&gt;, QueryDslPredicateExecutor&lt;Item&gt; {}</code></pre>
<h4 id="querydsl-사용-예시">QueryDSL 사용 예시</h4>
<pre><code class="language-groovy">QItem item = QItem item;
Iterable&lt;Item&gt; result = itemRepository.findAll(
    item.name.contains(&quot;장난감&quot;).and(item.price.between(10000, 20000))
);</code></pre>
<h4 id="querydslpredicateexecutor-사용-예시">QueryDslPredicateExecutor 사용 예시</h4>
<p>QueryDSL을 검색 조건으로 사용하며 스프링 데이터 JPA가 제공하는 페이징과 정렬 기능도 함께 사용 가능.</p>
<pre><code class="language-groovy">public interface QueryDslPredicateExecutor&lt;T&gt; {

    T findOne(Predicate predicate);
    Iterable&lt;T&gt; findAll(Predicate predicate);
    Iterable&lt;T&gt; findAll(Predicate predicate, OrderSpecifier&lt;?&gt;...orders);
    Page&lt;T&gt; findAll(Predicate predicate, Pageable pageable);
    long count(Predicate predicate);
}</code></pre>
<ul>
<li>한계점: join, fetch를 사용할 수 없음.</li>
<li>따라서 QueryDSL의 다양한 기능을 사용하기 위해서는 JPAQuery를 직접 사용하거나 스프링 데이터 JPA가 제공하는 QueryDslRepositorySupport를 사용해야함</li>
</ul>
<h3 id="2-querydslrepositorysupport-사용">(2) QueryDslRepositorySupport 사용</h3>
<p>QueryDSL의 모든 기능을 사용하기 위해서는 JPAQuery 객체를 직접 생성해 사용하면 됨. 이때 스프링 데이터 JPA가 제공하는 QueryDslRepositorySupport를 상속받으면 더 편리하게 사용 가능함.</p>
<h4 id="예제">예제</h4>
<ul>
<li>CustomOrderRepository: 사용자 정의 레포지토리<ul>
<li>스프링 데이터 JPA가 제공하는 공통 인터페이스는 직접 구현할 수 없기에 사용자 정의 레포지토리를 만듦</li>
</ul>
</li>
</ul>
<pre><code class="language-groovy">public interface CustomOrderRepository {

    public List&lt;Order&gt; search(OrderSearch orderSearch);
}</code></pre>
<ul>
<li>QueryDslRepositorySupport 사용</li>
</ul>
<pre><code class="language-groovy">public class OrderRepositoryImpl extends QueryDslRepositorySupport implements CustomOrderRepository {

    public OrderRepositoryImpl() {
        super(Order.class);
    }

    @Override
    public List&lt;Order&gt; search(OrderSearch orderSearch) {
        QOrder order = QOrder.order;
        QMember member = QMember.member;

        JPQLQuery query = from(order);

        if (StringUtils.hasText(orderSearch.getMemberName())) {
            query.leftJoin(order.member, member)
                .where(member.name.contains(orderSearch.getMemberName()));
        }

        if (orderSearch.getOrderStatus() != null) {
            query.where(order.status.eq(orderSearch.getOrderStatus()));
        }

        return query.list(order);
    }
}</code></pre>
<h4 id="querydslrepositorysupport의-기능">QueryDslRepositorySupport의 기능</h4>
<p>검색 조건에 따라 동적으로 쿼리를 생성. 생성자에서 QueryDslRepositorySupport에 엔티티 클래스 정보를 넘겨줘야함.</p>
<pre><code class="language-groovy">@Repository
public abstract class QueryDslRepositorySupport {

    //엔티티 매니저 반환
    protected EntityManager getEntityManager() {
        return entityManager;
    }

    //from절 반환
    protected JPQLQuery from(EntityPath&lt;?&gt; ... paths) {
        return querydsl.createQuery(paths);
    }

    //QueryDSL delete절 반환
    protected DeleteClause&lt;JPADeleteClause&gt; delete(EntityPath&lt;?&gt; path) {
        return new JPADeleteClause(entityManager, path);
    }

    //QueryDSL update절 반환
    protected UpdateClause&lt;JPAUpdatedClause&gt; update(EntityPath&lt;?&gt; path) {
        return new JPAUpdateClause(entityManager, path);
    }

    //스프링 데이터 JPA가 제공하는 Querydsl을 편하게 사용하도록 돕는 헬퍼 객체 반환
    protected QueryDsl getQueryDsl() {
        return this.querydsl;
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[작업기록2. Spring Security/JWT]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%91%EC%97%85%EA%B8%B0%EB%A1%9D2.-Spring-SecurityJWT</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%91%EC%97%85%EA%B8%B0%EB%A1%9D2.-Spring-SecurityJWT</guid>
            <pubDate>Wed, 12 Aug 2026 12:48:58 GMT</pubDate>
            <description><![CDATA[<h2 id="spring-securityjwt-기반-인증인가-구현">Spring Security/JWT 기반 인증/인가 구현</h2>
<h3 id="security-filter-chain">Security Filter Chain</h3>
<p>Spring Security에서는 인증/인가를 Security Filter Chain에서 담당하며, 1개의 필터가 아니라 여러 개의 필터를 순서대로 처리하는 방식으로 진행됨.</p>
<p>구현을 빨리하는게 목표였어서 스프링 시큐리티의 필터 체인 부분을 뭔가 제대로 이해하지 못한 거 같아서 아쉬운. 제대로 공부한다면 사실 스프링 시큐리티하고 필터체인에 대해서는 엄청 많은 내용이 있을 것 같다.</p>
<p>전체적인 흐름만 대충 이해한 바로는 이미 존재하는 필터 체인들 사이에 커스텀으로 필터 하나를 넣는 느낌. 유튜브에서 본 것을 기반으로는 이 필터 위치가 UsernamePasswordAuthentication Filter 앞에 넣는 거였다.</p>
<h3 id="1-jwtutil">(1) JwtUtil</h3>
<ul>
<li>이전 로그인 부분 구현할 때 대부분 작성해두었었는데, 추가적으로 다른 내용들이 필요해 작성하였다.</li>
<li>추가적으로 작성한 부분은 ‘남은 시간(만료 시간) 반환하는 메소드, <strong>토큰 검증하는 메소드, HTTP 요청 헤더에서 토큰을 추출하는 메소드</strong>’ 였다.</li>
</ul>
<pre><code class="language-groovy">@Component
@Slf4j
public class JwtUtil {

    private final SecretKey key;
    private final Long accessExpirationMs;
    private final Long refreshExpirationMs;

    ...

    public Long getAccessExpirationMs() {return accessExpirationMs;}

    //토큰 유효성 검증
    public Claims validateToken(String token) {
        try {
            return Jwts.parser()
                    .verifyWith(key)
                    .build()
                    .parseSignedClaims(token)
                    .getPayload();

        } catch (ExpiredJwtException e) {
            //만료된 토큰
            log.debug(&quot;만료된 토큰&quot;);
            return null;
        } catch (MalformedJwtException | SignatureException | UnsupportedJwtException e) {
            //형식이 잘못된 경우/서명이 다른 경우/미지원 토큰
            log.debug(&quot;유효하지 않은 토큰 (형식 오류/서명 불일치/미지원)&quot;);
            return null;
        } catch (IllegalArgumentException e) {
            //토큰이 비거나 null인 경우
            log.debug(&quot;토큰이 비어있거나 null&quot;);
            return null;
        }
    }

    //HTTP 요청 헤더에서 토큰 추출
    public static String resolveToken(HttpServletRequest request) {
        String header = request.getHeader(&quot;Authorization&quot;);
        if (header != null &amp;&amp; header.startsWith(&quot;Bearer &quot;)) return header.substring(7);
        return null;
    }
}
</code></pre>
<h4 id="토큰-검증-메소드-validatetoken"><strong>토큰 검증 메소드: <code>validateToken()</code></strong></h4>
<ul>
<li><code>Jwts</code>: jjwt 라이브러리의 진입점 클래스로 토큰을 만들 때는 <code>Jwts.builder()</code>, 토큰을 검증/해석할 때는 <code>Jwts.parser()</code>를 쓰도록 설계되어있음.</li>
<li><code>Claims</code>: jjwt 라이브러리에서 제공하는 인터페이스로. <strong>JWT 토큰 안에 들어있는 내용물(payload)를 담는 것</strong>. memberId, issuedAt, expiration 같은 정보들이 들어있음.<ul>
<li><code>verifyWith()</code>: 해당 서명 키로 검증하라고 지정하는 것. 이때 키는 JwtUtil의 생성자에서 만든 SecretKey.</li>
<li><code>build()</code>: 설정 종료. 실제 파서(parser) 객체를 완성한 것.</li>
<li><code>parseSignedClaims()</code>: 진짜 검증 작업.</li>
<li>서명이 맞지 않으면 SignatureException, 만료되었으면 ExpiredJwtException이 던져짐</li>
<li><code>getPayload()</code>: 검증 통과 시, 토큰 내에 있던 내용을 꺼냄</li>
</ul>
</li>
<li>예외 정리<ul>
<li>MalformedJwtException: 토큰 형식 자체가 이상할 때. JWT 구조가 깨져있음</li>
<li>UnsupportedJwtException: 지원하지 않는 형식의 JWT.</li>
</ul>
</li>
</ul>
<pre><code class="language-java">public Claims validateToken(String token) {
        try {
            return Jwts.parser()
                    .verifyWith(key)
                    .build()
                    .parseSignedClaims(token)
                    .getPayload();

        } catch (ExpiredJwtException e) {
            //만료된 토큰
            log.debug(&quot;만료된 토큰&quot;);
            return null;
        } catch (MalformedJwtException | SignatureException | UnsupportedJwtException e) {
            //형식이 잘못된 경우/서명이 다른 경우/미지원 토큰
            log.debug(&quot;유효하지 않은 토큰 (형식 오류/서명 불일치/미지원)&quot;);
            return null;
        } catch (IllegalArgumentException e) {
            //토큰이 비거나 null인 경우
            log.debug(&quot;토큰이 비어있거나 null&quot;);
            return null;
        }
    }</code></pre>
<h4 id="토큰-추출하는-메소드-resolvetoken"><strong>토큰 추출하는 메소드: <code>resolveToken()</code></strong></h4>
<ul>
<li><strong>HttpServletRequest</strong>: Servlet 표준 API. 웹 서버가 HTTP 요청 하나를 자바 객체로 표현할 때 사용하는 표준 인터페이스.<ul>
<li>톰캣(내장 서블릿 컨테이너)가 요청 내용(헤더, body, URL, 파라미터)를 HttpServletRequest 객체로 감싸서 코드에 전달.</li>
</ul>
</li>
<li><code>getHeader()</code>를 통해 요청의 헤더 부분 중 “Authorization”의 값 가져옴.</li>
<li>헤더값이 널(null)이 아니라면, “Bearer “ 7자 이후부터의 문자열을 부분문자열로 만들어서 리턴함으로써 토큰을 추출하는 것</li>
</ul>
<pre><code class="language-groovy">public static String resolveToken(HttpServletRequest request) {
        String header = request.getHeader(&quot;Authorization&quot;);
        if (header != null &amp;&amp; header.startsWith(&quot;Bearer &quot;)) return header.substring(7);
        return null;
    }</code></pre>
<h3 id="2-jwtfilter">(2) JwtFilter</h3>
<ul>
<li><code>OncePerReqeustFilter</code>: 요청 하나 당 딱 한 번만 실행되도록 보장해주는 필터 베이스 클래스. 스프링이 제공하는 추상 클래스.<ul>
<li>JwtFilter는 토큰을 검증하고, SecurityContext를 채우는 로직이기에 딱 한 번만 실행되어야 유의미하고, 여러 번 실행되면 불필요한 중복 작업 또는 부작용이 생길 수 있음.</li>
<li><code>doFilterInternal()</code>을 오버라이드해야함</li>
</ul>
</li>
<li>필드주입이 아닌 생성자 주입 방식 사용 + 롬복 없이 수동으로 작성.</li>
</ul>
<pre><code class="language-java">@Slf4j
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

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

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = JwtUtil.resolveToken(request);

        if (token == null) {
            log.debug(&quot;토큰 없음 - URI: {}&quot;, request.getRequestURI());
        }
        else{
            Claims claims = jwtUtil.validateToken(token);

            if (claims == null) log.warn(&quot;유효하지 않은 토큰 - URI: {}&quot;, request.getRequestURI());
            else {
                Long memberId = claims.get(&quot;memberId&quot;, Long.class);

                CustomUserDetails userDetails = new CustomUserDetails(memberId);
                Authentication authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authToken);

                log.debug(&quot;인증 성공 - memberId: {}, URI: {}&quot;, memberId, request.getRequestURI());
            }
        }

        filterChain.doFilter(request, response);
    }
}
</code></pre>
<ul>
<li>앞에서는 일단 JwtUtil에 만들어둔 토큰 추출하는 메소드를 호출해서 토큰을 꺼냄</li>
<li>이후에 JwtUtil에 있는 토큰 검증 메소드를 사용해서, 검증 성공 시에 Claims를 가져와 요청(request)의 내용을 가져옴.<ul>
<li>이 Claims 안에 있는 memberId 가져오고, 이를 바탕으로 <code>CustomUserDetails</code> 로 감싸서 객체 만들기</li>
<li><code>CustomDetails</code>를 인자로 해서, <code>Authentication</code> 만들고 이를 <code>SecurityContextHolder</code>에 등록 → 이걸 <code>@AuthMember</code>가 이를 활용.</li>
</ul>
</li>
<li>마지막에 항상 실행되는 부분: <code>filterChain.doFilter(request, response)</code>: SecurityContext를 채운 여부에 관계없이 <strong>다음 필터로 요청 전달</strong></li>
</ul>
<h3 id="3-customuserdetails">(3) CustomUserDetails</h3>
<ul>
<li><p>Spring Security에서 제공하는 인터페이스인 UserDetails를 구현(implements)</p>
<ul>
<li><p>인증된 사용자가 누구인지 표현하기 위해 정의해둔 표준 인터페이스.</p>
</li>
<li><p>Security 내부 컴포넌트들이 로그인된 사용자 정보를 다룰 때에 항상 해당 타입을 기준으로 동작. (예: AuthenticationManager, SecurityContext)</p>
</li>
<li><p>UserDetails 인터페이스</p>
<pre><code class="language-java">  public interface UserDetails extends Serializable {
      //권한 목록
      Collection&lt;? extends GrantedAuthority&gt; getAuthorities();
      String getPassword(); //비밀번호 (해시)
      String getUsername(); //식별자(보통 아이디 또는 이메일)
      boolean isAccountNonExpired(); //계정 만료 여부
      boolean isAccountNonLocked(); //계정 잠금 여부
      boolean isCredentialsNonExpired(); //비밀번호 만료 여부
      boolean isEnabled(); //계정 활성화 여부
  }</code></pre>
</li>
</ul>
</li>
</ul>
<pre><code class="language-java">public class CustomUserDetails implements UserDetails {

    private final Long memberId;

    public CustomUserDetails(Long memberId) {this.memberId = memberId;}

    //MemberID 반환 메소드
    public Long getMemberId () {return this.memberId;}

    //불필요한 메소드 (ROLE/비밀번호/사용자 이름 등)
    @Override
    public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {
        return List.of(new SimpleGrantedAuthority(&quot;ROLE_USER&quot;));
    }

    @Override
    public @Nullable String getPassword() {return null;}

    @Override
    public String getUsername() {return String.valueOf(memberId);}

    //불필요한 메소드: 만료일/계정 정지/계정 탈퇴 등 구현 시 필요
    @Override
    public boolean isAccountNonExpired() {return true;}
    @Override
    public boolean isAccountNonLocked() {return true;}
    @Override
    public boolean isCredentialsNonExpired() {return true;}
    @Override
    public boolean isEnabled() {return true;}
}</code></pre>
<ul>
<li>해당 프로젝트에서는 JWT 기반 인증을 사용하고 있으니 비밀번호, 문자열 아이디를 매 요청 들고 다니지X.<ul>
<li><code>memberId</code>(Long)을 핵심 데이터로 들고</li>
<li><code>getPassword()</code>의 경우 null 반환 ⇒ JWT 인증 이후에는 비밀번호 재검증 불필요</li>
<li><code>getUsername()</code>의 경우 <code>memberId</code>를 문자열로 변환해서 반한 (인터페이스 스펙 상 String 타입이 강제되니, 식별자로 <code>memberId</code> 재활용)</li>
<li>아직 계정 탈퇴/정지는 미구현이라 <code>true</code> 반환으로 고정.</li>
</ul>
</li>
<li>이 CustomUserDetails를 통해 헤더에 담긴 accessToken을 토대로 memberId를 꺼내서 사용해야하는데? 커스텀 어노테이션을 만들어서 사용할 생각으로 해당 부분까지 구현함.<ul>
<li>그냥 CustomUserDetails에서 꺼내쓰는건 <code>@AuthenticationPrincipal</code> 어노테이션을 사용해서 컨트롤러에서 꺼내쓸 수 있음.</li>
<li><code>@AuthenticationPrincipal</code>: 내부적으로 <code>SecurityContextHolder.getContext().getAuthentication().getPrincipal()</code>을 꺼내서 CustomUserDetails로 캐스팅하여 넣어주는 어노테이션.</li>
</ul>
</li>
</ul>
<h3 id="4-authmember-커스텀-어노테이션">(4) @AuthMember 커스텀 어노테이션</h3>
<ul>
<li><p>스프링 기본 제공 방법</p>
<p>  기본적으로 Spring Security에서 제공하는 방법은 <code>@AuthenticationPrincipal</code> 어노테이션을 사용해 컨트롤러의 메소드 쪽에서 CustomUserDetails를 받는 방식. </p>
<p>  별도로 ArgumentResolver나 커스텀 어노테이션을 만들지 않고 스프링이 구현한 것을 가져다 쓰면 됨. CustomUserDetails에서 <code>getMemberId()</code> 메소드를 호출해 회원의 ID를 가져옴</p>
</li>
<li><p>다만 CustomUserDetails에서 memberId를 가져오지 않고 커스텀 어노테이션 + Resolver를 통해 memberId를 파라미터 쪽에서 바로 받아서 사용할 수 있도록 구현</p>
</li>
</ul>
<pre><code class="language-java">@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthMember {
}</code></pre>
<ul>
<li><p><code>@Target(ElementType.PARAMETER)</code>: 해당 어노테이션을 <strong>메소드의 파라미터 위치에만</strong> 붙일 수 있게 제한</p>
</li>
<li><p><code>@Retention(RetentionPolicy.RUNTIME)</code>: 어노테이션 정보를 런타임까지 유지하겠다는 의미.</p>
<p>  ⇒ 스프링이 요청 처리 중에 해당 파라미터에 @AuthMember가 붙어있는지를 확인해야하기에 RUNTIME 설정이 필수적. (SOURCE/CLASS이면 이미 정보가 사라져서 못 읽는다고함.)</p>
</li>
<li><p>내용이 빈 마커 어노테이션으로 별도의 속성값이 없이, 표시 역할만 수행 → 따라서 짝이 되는 <code>HandlerMethodArgumentResolver</code>가 따로 실제 값을 채워주는 역할 담당.</p>
</li>
<li><p>추가적으로 <code>WebMvcConfigurer</code>에 등록해야 실제로 동작한다고함.</p>
</li>
</ul>
<h3 id="5-authmemberresolver">(5) AuthMemberResolver</h3>
<pre><code class="language-java">public class AuthMemberResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        boolean hasAnnotation = parameter.hasParameterAnnotation(AuthMember.class);
        boolean isLongType = Long.class.equals(parameter.getParameterType());

        return hasAnnotation &amp;&amp; isLongType;
    }

    @Override
    public @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                                            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || !((authentication.getPrincipal()) instanceof CustomUserDetails userDetails)) {
            throw new BusinessException(GlobalErrorCode.UNAUTHORIZED);
        }
        return userDetails.getMemberId();
    }
}</code></pre>
<ul>
<li><p><code>supportsParameter()</code>:</p>
<p>  스프링이 컨트롤러의 메소드를 호출하기 전, 각각의 파라미터마다 <code>supportsParameter()</code> 메소드를 호출해 해당 파라미터를 처리가능한지 확인함</p>
<p>  아래에서는 아래 두 가지 조건을 검사하여 모두 만족해야 true를 반환 → 이래야 스프링이 Resolver의 <code>resolveArgument</code>를 호출.</p>
<ul>
<li>파라미터에 <code>@AuthMember</code>가 붙어있는지</li>
<li>파라미터 타입이 정확히 Long인지.</li>
</ul>
</li>
<li><p><code>resolveArgument()</code>:</p>
<p>  실제로 파라미터에 값을 만들어서 반환해주는 메소드.</p>
<p>  1- SecurityContextHolder를 통해 현재 요청의 인증 정보를 가져옴. → JwtFilter가 채워둔 <code>Authentication</code> 객체.</p>
<p>  2- 인증정보(<code>Authentication</code> 객체)가 널(null)이거나 principal이 CustomUserDetails 타입이 아니면(=인증X 또는 예상과 다른 타입) BusinessException(401 UNAUTHORIZED)를 던짐</p>
<p>  3-위의 과정 통과시 <code>userDeatils.getMemberId()</code>를 통해 회원의 ID를 반환해줌</p>
</li>
</ul>
<p>⇒ 실질적으로 매 컨트롤러 메소드에서 CustomUserDetails를 통해 회원ID를 꺼내 쓰는 것을 조금 더 간편화하기 위해 이런걸 만든건데..? 실제로 사용하기에 편하긴 했던..? 중복 코드도 좀 줄이고…? 그런데 왠지 공통적으로 스프링에서 애초에 제공하는걸 사용하는 것도 괜찮을 것 같기도한 느낌..?</p>
<h4 id="6-webconfig">(6) WebConfig</h4>
<p>앞에서 설명한 WebConfig 부분에 커스텀으로 만든 Resolver를 등록해줘야 실제 사용이 가능하기에 아래처럼 등록을 해줌.</p>
<ul>
<li><p>❓WebMvcConfigurer?</p>
<p>  Spring MVC가 기본적으로 제공하는 인터페이스로 Spring MVC의 기본 동작을 커스터마이징 할 수 있는 훅(hook)들을 모아둠. 스프링 부트가 알아서 해주는 기본설정(auto-configuration) 중 특정 부분을 바꿀 때 인터페이스를 구현해 오버라이드하는 방식으로 사용.</p>
</li>
</ul>
<pre><code class="language-java">@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List&lt;HandlerMethodArgumentResolver&gt; resolvers) {
        resolvers.add(new AuthMemberResolver());
    }
}</code></pre>
<h4 id="8-securityconfig">(8) SecurityConfig</h4>
<p>SecurityFilterChain에 앞에서 만든 JwtFilter를 등록하는 과정 역시 필요. 앞에서 말했듯이 UsernamePasswordAuthenticationFilter 앞의 위치에 삽입.</p>
<pre><code class="language-java">@Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        ...

        //필터 등록
        http.addFilterBefore(new JwtFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);

        ...

        return http.build();
    }</code></pre>
<h3 id="7-전체-흐름">(7) 전체 흐름</h3>
<h4 id="로그인">로그인</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/f078ed4c-6529-41b3-a7e2-6f12dff5d009/image.png" alt=""></p>
<h4 id="인증인가">인증/인가</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/f90e571a-307a-471c-8c9b-d31a78e837ad/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[작업기록1. 카카오 로그인 API 구현]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%91%EC%97%85%EA%B8%B0%EB%A1%9D1.-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8-API-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%91%EC%97%85%EA%B8%B0%EB%A1%9D1.-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8-API-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Fri, 31 Jul 2026 00:35:56 GMT</pubDate>
            <description><![CDATA[<h3 id="1-액세스-토큰--db-저장-oauthidkakaoid">(1) 액세스 토큰 &amp; DB 저장: OauthID(KakaoID)</h3>
<ul>
<li><p>유튜브에서 공부했던 건 외부 API로 로그인하는게 아니라 직접 서버에서 아이디, 비밀번호를 관리하는 것이었는데 외부 API를 호출해서 처리하는거랑은 또 달라서? 어떻게 해야할지?</p>
<ul>
<li><p>기존은 ‘아이디, 비밀번호’를 DB에 암호화해서 저장하는 방식</p>
</li>
<li><p>외부 API는 카카오에서 ‘액세스 토큰’을 받아서 그걸로 인증하는 방식 → DB에 액세스 토큰을 저장하면 될 듯?</p>
<ul>
<li><p>액세스 토큰은 암호화 안해도 되는지? (비밀번호같은거는 노출되면 안되니까 암호화해야하는데)</p>
<p>  ⇒ <strong>액세스 토큰을 DB에 저장하는게 아님</strong></p>
</li>
</ul>
</li>
<li><p>카카오에서 발급하는 액세스 토큰(accessToken)은 DB에 저장하지 않음. 요청-응답에서 카카오 쪽에 해당 회원이 누군지? 인증용으로 일회성으로 묻는 용도.</p>
<ul>
<li>거기에 더해 전송 구간은 HTTPS(TLS)로 보호됨. 포스트맨/프론트 → 우리 서버, 우리 서버 → 카카오는 둘 다 HTTPS로 통신하니까 암호화되어서 전송됨. Payload 안의 문자열을 추가 암호화할 필요는 없음.</li>
</ul>
</li>
<li><p>저장하는 건 <strong>‘OauthID(kakaoId)’</strong>: 카카오가 각 사용자에게 부여하는 고유하고, 변하지 않는 회원 식별번호. ⇒ 이게 영구적인 회원 식별자로 저장. 암호화가 불필요함</p>
<ul>
<li>암호화 불필요한 이유: 회원 식별 용도이지, 이게 탈취당했다고해서 바로 계정이 탈취되지는 않음.</li>
</ul>
</li>
</ul>
</li>
<li><p>전체적으로 액세스 토큰 받아오는 부분은 프론트 쪽에서 카카오 개발자 콘솔 작업을 진행하고 백엔드에서는 프론트에서 액세스 토큰을 전달한다는 가정하에 진행하였다. 백엔드에서 처리하는 방법으로 진행하였다면 아마 Rediret URL 같은 작업도 했어야했을 것 같다.</p>
</li>
</ul>
<h3 id="2-폴더-구조">(2) 폴더 구조</h3>
<ul>
<li><p>폴더 구조는 전체적으로 팀에서 정한 것 내에서 조금씩 수정. 동아리 활동하면서 도메인(domain) 내에 controller, service, dto, repository 구조에는 되게 익숙해서 기본으로 추가함</p>
<ul>
<li>그런데 repository 같은 경우에는 auth 도메인 내에는 불필요했음! 비밀번호를 서버에서 직접 저장해서 관리하지 않으니까, 카카오의 응답 확인하고(auth) 그 회원의 oauthId(kakaoId)하고 정보를 DB에 저장하는데 이 부분은 member 도메인쪽이라 그쪽 repository 폴더에 넣어둠.</li>
</ul>
<p>서버 쪽에서 jwt(토큰)을 발급해줘야하니까 해당 내용 관련 폴더 하나 파고, 카카오 외부 API 로그인을 구현할 거라 해당 부분 관련 내용을 넣을 client 폴더를 추가함.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/32296d96-3811-4bd6-a538-96964c740824/image.png" alt=""></p>
<h3 id="3-client-폴더">(3) Client 폴더</h3>
<p>실질적으로? 확장성 생각하면 카카오 로그인 외에도 대부분의 서비스에 구글, 다른 SNS 로그인이 있을 수 있긴하니까, OauthApiClient, OauthUserInfo 같이 공통 인터페이스로 빼두었다. 공통 부분을 인터페이스로 빼는게 추후에 확장성에는 전체적으로 좋을 것 같긴하니까?</p>
<h4 id="oauthapiclient">OauthApiClient</h4>
<ul>
<li>제일 착잡했던 부분? 외부 API 가져다가 쓰는게 처음이라 어떤식으로 써야할지? 코드를 어떻게 작성해야할지도 모르겠었다.</li>
</ul>
<pre><code class="language-java">//OauthApiClient.java
public interface OauthApiClient {
    OauthUserInfo getUserInfo(String accessToken);
    OauthProvider supportedProvider();
}</code></pre>
<ul>
<li>일단은 인터페이스니까 여러 api에서 공통적으로 사용할 부분들 정의</li>
<li>OauthProvider의 경우에는 enum으로 만들었고, 카카오는 KAKAO로 두었다. 구글을 하게되면 GOOGLE 이렇게 될테니까?</li>
</ul>
<h4 id="oauthuserinfo">OauthUserInfo</h4>
<pre><code class="language-java">public interface OauthUserInfo {
    String getProviderId(); //각 소셜의 고유 ID(문자열로 통일)
    String getNickname();
    String getProfileImageUrl();
    OauthProvider getProvider();
}</code></pre>
<ul>
<li>사실 현재 구현하려는 서비스 정책 기준으로 필요한건 providerID 즉 kakaoID 하나가 전부였다. 닉네임이나 프로필 이미지는 필요한 부분은 아니었고, OauthProvider도 확장성 때문에 두긴했지만 실질적으로는 KAKAO 하나였으니까?</li>
</ul>
<h4 id="kakaoapiclient">KakaoApiClient</h4>
<ul>
<li>위에서 만든 인터페이스를 실질적으로 구현해야하는 부분 이 부분 하면서 그래도 어느정도 외부 API 가져와서 쓰는게 이런 느낌이구나 했던 것 같다.</li>
<li>일단 찾아봤을 때, WebClient가 필요하다고 해서 설정하고 필드로 두었다. <code>@RequiredArgsConstructor</code> 어노테이션 써서 스프링 컨테이너에서 주입하는 형식으로<ul>
<li>일단 WebClient는 WebClientConfig를 통해 Bean으로 등록했다.</li>
<li>위처럼 스프링 빈으로 등록하면, 다른 곳에서 주입받아서 쓸 수 있으니까. 이렇게 해두고 어노테이션 써서 주입하는 식?</li>
</ul>
</li>
<li>✔️ WebClient 의존성<ul>
<li>build.gradle에 spring-boot-starter-webflux. WebFlux에 webclient가 포함되어있음</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class KakaoApiClient implements OauthApiClient {

    private final WebClient webClient;

    @Override
    public OauthUserInfo getUserInfo(String accessToken) {
        try {
            return webClient.get()
                    .uri(&quot;https://kapi.kakao.com/v2/user/me&quot;)
                    .header(&quot;Authorization&quot;, &quot;Bearer &quot; + accessToken)
                    .retrieve()
                    .bodyToMono(KakaoUserInfoResponse.class)
                    .block();
        } catch (WebClientResponseException e) {
            throw new BusinessException(GlobalErrorCode.INVALID_KAKAO_TOKEN);
        }
    }

    @Override
    public OauthProvider supportedProvider() {
        return OauthProvider.KAKAO;
    }
}</code></pre>
<ul>
<li>주입받은 webClient를 통해서 카카오 사용자 정보 API를 호출 → access token을 Authorization: Bearer … 헤더로 보냄 → 이 응답을 다시 KaKaoUserInfo(OauthUserInfo 인터페이스 구현)로 받는 것.</li>
</ul>
<aside>

<h3 id="✅-webclient의-표준-api">✅ WebClient의 표준 API</h3>
<ul>
<li>WebClient: 스프링이 제공하는 HTTP 클라이언트.<ul>
<li>표준 API → 어떠한 외부 API를 호출하더라도 항상 이 패턴을 사용함</li>
</ul>
</li>
</ul>
<pre><code class="language-java">webClient.get()
                 .uri(&quot;https://kapi.kakao.com/v2/user/me&quot;)
                 .header(&quot;Authorization&quot;, &quot;Bearer &quot; + accessToken)
                 .retrieve()
                 .bodyToMono(KakaoUserInfoResponse.class)
                 .block();</code></pre>
<ul>
<li><p><code>get()</code>: HTTP 메소드 작성. GET/POST/PUT/DELETE 요청 가능</p>
</li>
<li><p><code>uri()</code>: 요청을 보낼 주소 URL. 위에서는 카카오 API 문서에 명시되어있는 엔드포인트를 작성</p>
</li>
<li><p><code>header()</code>: HTTP 헤더 추가 → 즉, HTTP 요청에 헤더를 실어서 보내는 것.</p>
<ul>
<li>“Authorization”, “Bearer ” 형식은 <strong>OAuth 2.0 표준 관례</strong>이며, WebClient 고유 문법이 아니라 HTTP 표준(RFC 6750)</li>
</ul>
</li>
<li><p><code>retrieve()</code>: 응답 받아오기 시작. 즉, 실제 요청을 보내고 응답을 받아오겠다는 선언부. WebClient 자체의 API 설계상 필요한 것.</p>
</li>
<li><p><code>bodyToMono()</code>: 요청의 응답으로 온 Body를 서버(우리 서버)에서 정의한 객체로 변환하는 부분.</p>
<ul>
<li>즉, 응답으로 온 JSON을 직접 선언/만든 <code>KakaoUserInfoResponse</code>라는 자바 객체로 자동 변환해달라는 의미.</li>
<li><code>Mono</code>: 리액티브 스트림에서 0개 또는 1개 결과가 나중에 온다는 것을 나타내는 타입.</li>
</ul>
</li>
<li><p><code>block()</code>: 비동기 결과를 동기적으로 기다려서 받기.</p>
<ul>
<li><p><code>Mono</code>-비동기 결과를 결과 완료까지 기다렸다가 실제 값으로 반환해달라는 의미. → 프로젝트는 서블릿(동기) 기반이기에, 리액티브 결과를 동기 코드 흐름에 맞추기 위해서 이렇게 처리</p>
<p>  ⇒ 아직 동기/비동기 개념이 막 잘 와닿지는 않는 느낌…? 이론적으로는 알지만 실제로..어디서 쓰이겠다 이런?</p>
</li>
</ul>
</li>
</ul>
</aside>

<h4 id="kakaouserinforesponse">KakaoUserInfoResponse</h4>
<ul>
<li><p><code>@JsonProperty</code>: JSON 필드명과 자바의 필드명을 연결(매핑)해주는 어노테이션</p>
<ul>
<li>Jackson: JSON ↔ 자바 객체 변환 라이브러리</li>
<li>Jackson에게 자바 필드명과 JSON의 필드명을 서로 매칭시키라는 의미.</li>
</ul>
</li>
<li><p><code>KakaoProfile</code>의 내용은 카카오 API 문서에 명시된 응답 스펙</p>
<p>  ⇒ 외부 API 어떻게 쓰는지 궁금했는데, 뭔가 프론트-백엔드 소통처럼 명시된 API 명세서/JSON에 따라서 응답 받고, 요청을 보내는 식이라 신기했음! (클래스명은 당연하지만, 자유) + 구글의 경우를 찾아봤는데, 구글은 또 다른 형식의 응답인데다가 중첩 구조가 아니라 flat한 구조였음!</p>
<ul>
<li><p>카카오의 실제 JSON 응답 형식</p>
<pre><code class="language-java">  {
    &quot;id&quot;: 123456789,
    &quot;kakao_account&quot;: {
      &quot;profile&quot;: {
        &quot;nickname&quot;: &quot;홍길동&quot;,
        &quot;profile_image_url&quot;: &quot;https://...&quot;
      }
    }
  }</code></pre>
</li>
</ul>
</li>
</ul>
<pre><code class="language-java">public record KakaoUserInfoResponse (
    Long id,
    @JsonProperty(&quot;kakao_account&quot;) KakaoAccount kakaoAccount
) implements OauthUserInfo {
    @Override
    public String getProviderId() {return String.valueOf(id);}

    @Override
    public String getNickname() {return kakaoAccount.profile().nickname();}

    @Override
    public String getProfileImageUrl() {return kakaoAccount.profile().profileImageUrl();}

    @Override
    public OauthProvider getProvider() {return OauthProvider.KAKAO;}

    public record KakaoAccount(KakaoProfile profile) {
        public record KakaoProfile(
                String nickname,
                @JsonProperty(&quot;profile_image_url&quot;) String profileImageUrl
        ) {}
    }
}
</code></pre>
<h3 id="4-jwt-폴더">(4) JWT 폴더</h3>
<h4 id="jwtutil">JwtUtil</h4>
<ul>
<li>JWT 토큰 관련해서 유튜브를 봤을 때는, 전체적인 흐름은 서버에서 자체적으로 토큰을 발급해서 클라이언트 쪽에서 요청을 보낼 때 해당 토큰을 헤더에 보내게되고, 이 토큰이 해당 서버에서 발급한 토큰이 맞는 확인하는게 JWT를 활용해서 인증을 진행하는 전체적인 흐름인 것 같았다.</li>
<li>JwtUtil은 그중에서 서버에서 자체적으로 토큰을 발급하는 부분을 담당.</li>
<li>액세스 토큰, 리프레시 토큰을 생성하는 로직 존재</li>
</ul>
<pre><code class="language-groovy">@Component
public class JwtUtil {

    private final SecretKey key;
    private final Long accessExpirationMs;
    private final Long refreshExpirationMs;

    public JwtUtil (@Value(&quot;${jwt.secret}&quot;)String secret,
                    @Value(&quot;${jwt.access-expiration}&quot;)Long accessExpirationMs,
                    @Value(&quot;${jwt.refresh-expiration}&quot;)Long refreshExpirationMs) {
        this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
        this.accessExpirationMs = accessExpirationMs;
        this.refreshExpirationMs = refreshExpirationMs;
    }

    public String createAccessToken(Long memberId) {
        return createToken(memberId, accessExpirationMs);
    }

    public String createRefreshToken(Long memberId) {
        return createToken(memberId, refreshExpirationMs);
    }

    private String createToken(Long memberId, Long expirationMs) {
        Date now = new Date();
        Date expiry = new Date(now.getTime()+expirationMs);

        return Jwts.builder()
                .claim(&quot;memberId&quot;, memberId)
                .issuedAt(now)
                .expiration(expiry)
                .signWith(key)
                .compact();
    }

    public Long getAccessExpirationMs() {return accessExpirationMs;}
}
</code></pre>
<ul>
<li><code>SecretKey</code>: jjwt 라이브러리에서 제공하는 타입으로 javax.crypto에 정의되어있는 인터페이스. 대칭키 암호화에 쓰이는 비밀키를 나타내는 타입.<ul>
<li><code>Keys.hmacShaKeyFor()</code>: application.yml에 문자열로 쓰여있는 jwt.secret 값을 받아와서 SecretKey 객체로 변환해주는 역할.<ul>
<li>필요한 이유: JWT 서명(<code>signWith(key)</code>)의 경우 HMAC이라는 암호화 알고리즘을 쓰는데 문자열이 아니라 정해진 형식의 키 객체를 요구. 따라서 문자열을 바이트로 바꾼 후에, 바이트를 알고리즘이 이해할 수 있는 키 형태(SecretKey)로 감싸는 것.</li>
</ul>
</li>
</ul>
</li>
<li>토큰 생성<ul>
<li>토큰을 생성할 때 현재 시각, 만료 시각을 포함. (<code>issuedAt</code>, <code>expiration</code>)</li>
<li><code>signWith</code>의 경우, 서명하는 것. → 변조되지 않았음을 증명.</li>
</ul>
</li>
<li>생성자<ul>
<li><code>@Value</code> 어노테이션: application.yml의 설정값을 자바 필드/파라미터에 자동으로 주입해주는 스프링 어노테이션<ul>
<li>이걸로 서명할 때 쓸 문자열, 액세스 토큰의 유효시간, 리프레시 토큰의 유효시간을 가져와서 클래스의 필드 설정해둠.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="5-config-폴더">(5) Config 폴더</h3>
<h4 id="webclientconfig">WebClientConfig</h4>
<ul>
<li>WebClient 객체를 딱 한 번 만들어서 스프링이 관리하는 공용 빈으로 등록하는 설정.<ul>
<li>현재는 KakaoApiClient에서만 사용하지만 추후 다른 API를 호출할 것이기에 빈으로 등록함.</li>
</ul>
</li>
<li><code>@Configuration</code>: 스프링에 해당 클래스가 Bean 설정을 담당하는 클래스임을 알림</li>
<li><code>@Bean</code>: 해당 메소드가 반환하는 객체를 스프링이 관리하는 빈으로 등록하라는 의미.</li>
<li>추후에 AI 스트리밍을 팀원분이 구현할 예정이라 WebFlux까지 넣음.</li>
</ul>
<pre><code class="language-groovy">@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient() {
        return WebClient.builder().build();
    }
}</code></pre>
<h4 id="securityconfig">SecurityConfig</h4>
<ul>
<li>이 부분은 사실 유튜브 내용을 그대로 따라 쓴 느낌..? 필터체인 부분이고, CSRF, FORM 로그인 부분의 체인은 JWT 사용할 때 사용하지 않을 거기에 꺼주었고, 마찬가지로 HTTP의 기본 인증 방식 역시 여기서는 사용하지 않을거라 꺼주었다.</li>
<li><code>authorizationHttpRequest</code> 부분을 통해 경로별 권한/인가를 관리할 수 있는데 우선은 모든 경로에서 인증 필요없이 통과되도록 설정. (임시로 현재 테스트 진행을 위해서.)</li>
<li>세션을 stateless로 유지하도록 설정.</li>
</ul>
<pre><code class="language-groovy">@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) {
        //CSRF disable
        http.csrf((auth) -&gt; auth.disable());
        //FORM login disable
        http.formLogin((auth) -&gt; auth.disable());
        //HTTP basic 인증방식 disable
        http.httpBasic((auth) -&gt; auth.disable());

        //경로별 권한(인가)관리: 임시로 전체 permit
        http.authorizeHttpRequests((auth) -&gt; auth
                .anyRequest().permitAll());

        //세션 stateless로 유지
        http.sessionManagement((session) -&gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }
}
</code></pre>
<h3 id="6-controller">(6) Controller</h3>
<ul>
<li>Jwt 설정이나 토큰 발급, 실제 카카오 API하고 연동하는 부분을 처리하고 나서 Controller나 Service 부분의 작업은 그렇게 애먹지는 않았던 것 같다.</li>
<li><code>@Slf4j</code>를 통해서 로깅하는 거는 팀 컨벤션? 규칙이었는데 확실히 좋은 것 같다. 에러 났을 때 원인 파악도 훨씬 수월했다.</li>
<li><code>ApiResponse</code>라는 공통 응답형식도 존재하여 사용해서 응답함.</li>
<li>Request만 실상 Service 쪽으로 전달하는 느낌.</li>
</ul>
<pre><code class="language-groovy">@RestController
@RequestMapping(&quot;/api/auth&quot;)
@RequiredArgsConstructor
@Slf4j
public class AuthController {

    private final AuthService authService;

    @PostMapping(&quot;/sign-in&quot;)
    public ResponseEntity&lt;ApiResponse&lt;SignInResponse&gt;&gt; signIn(@Valid @RequestBody SignInRequest request) {

        log.info(&quot;로그인 요청 - provider: {}&quot;, request.provider());
        SignInResponse response = authService.signIn(request);

        return ResponseEntity.ok(ApiResponse.success(&quot;로그인 성공&quot;, response));
    }
}</code></pre>
<h3 id="7-service">(7) Service</h3>
<ul>
<li>초반에 작성하였기도 하고, 이때는 로그인 구현을 끝내는게 우선이라 이미 존재하는 회원인지의 검증을 따로 헬퍼 메소드로 분리하지 않고 <code>signIn()</code> 메소드 안에 작성하였었다.</li>
<li><code>OauthApiClient</code>와 <code>OauthUserInfo</code> 를 통해서 요청자 정보를 request로부터 가져옴.<ul>
<li><code>provider</code>: 어떤 SNS인지. 카카오</li>
<li><code>oauthAccessToken</code>: SNS 액세스 토큰. 이 토큰을 가지고 카카오 서버에 사용자 정보를 재요청하는 것.</li>
</ul>
</li>
<li>이미 존재하지 않는 사용자와 존재하는 사용자를 구분해서 처리.<ul>
<li>존재하지 않는 사용자 → 새로 회원가입(DB 저장) 후 로그인</li>
</ul>
</li>
<li>로그인 시점에 액세스 토큰과 리프레시 토큰을 발급.</li>
<li>응답에 만료시점을 전달하는 쪽으로 API 명세서를 짰기에 <code>expiresIn</code>과 <code>expiresAt</code>을 계산해서 전달하는 방식을 취함.</li>
</ul>
<pre><code class="language-groovy">@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class AuthService {
    private final List&lt;OauthApiClient&gt; oauthApiClientList;
    private final MemberRepository memberRepository;
    private final JwtUtil jwtUtil;

    public SignInResponse signIn(SignInRequest request) {
        OauthApiClient client = findClient(request.provider());
        OauthUserInfo userInfo = client.getUserInfo(request.oauthAccessToken());

        Optional&lt;Member&gt; existingMember = memberRepository.findByOauthIdAndOauthProvider(userInfo.getProviderId(), request.provider());
        boolean isNewUser = existingMember.isEmpty();

        Member member = existingMember.orElseGet(() -&gt; memberRepository.save(
                        Member.builder()
                                .oauthProvider(request.provider())
                                .oauthId(userInfo.getProviderId())
                                .build()
                ));

        String accessToken = jwtUtil.createAccessToken(member.getId());
        String refreshToken = jwtUtil.createRefreshToken(member.getId());

        long expiresIn = jwtUtil.getAccessExpirationMs()/1000;
        LocalDateTime expiresAt = LocalDateTime.now().plusSeconds(expiresIn);

        log.info(&quot;로그인 성공 - memberId: {}, provider: {}, isNewUser: {}&quot;,
                member.getId(), request.provider(), isNewUser);

        return SignInResponse.builder()
                .memberId(member.getId())
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .expiresIn(expiresIn)
                .expiresAt(expiresAt)
                .isNewUser(isNewUser)
                .build();
    }

    private OauthApiClient findClient(OauthProvider provider) {
        return oauthApiClientList.stream()
                .filter(client -&gt; client.supportedProvider() == provider)
                .findFirst()
                .orElseThrow(() -&gt; new BusinessException(GlobalErrorCode.UNSUPPORTED_PROVIDER));
    }
}</code></pre>
<h3 id="8-dto">(8) DTO</h3>
<h4 id="request">Request</h4>
<pre><code class="language-groovy">public record SignInRequest(
        @NotNull (message = &quot;provider는 필수입니다.&quot;)
        OauthProvider provider,
        @NotBlank(message = &quot;oauthAccessToken은 필수입니다.&quot;)
        String oauthAccessToken) {}</code></pre>
<h4 id="response">Response</h4>
<pre><code class="language-groovy">@Builder
public record SignInResponse(
    Long memberId,
    String accessToken,
    String refreshToken,
    long expiresIn,
    LocalDateTime expiresAt,
    boolean isNewUser
) {}</code></pre>
<ul>
<li>API 명세서는 팀원 다같이 짜서 그걸 토대로 응답을 구성.</li>
<li>서버에서 발급한 액세스 토큰, 리프레시 토큰, 그리도 회원ID를 전달.</li>
<li>외에도 만료시간(액세스토큰의)과 시각, 새로운 회원인지의 여부를 판단하여 전달.</li>
</ul>
<h3 id="buildgradle-설정">build.gradle 설정</h3>
<pre><code class="language-groovy">implementation &#39;io.jsonwebtoken:jjwt-api:0.12.3&#39;
implementation &#39;org.springframework.boot:spring-boot-starter-webflux&#39;

runtimeOnly &#39;io.jsonwebtoken:jjwt-impl:0.12.3&#39;
runtimeOnly &#39;io.jsonwebtoken:jjwt-jackson:0.12.3&#39;</code></pre>
<hr>
<blockquote>
</blockquote>
<h4 id="✔️-프론트에서의-작업---백엔드에서의-작업">✔️ 프론트에서의 작업 - 백엔드에서의 작업</h4>
<ul>
<li>프론트에서 어디까지 처리해서 전달이 이루어지는 것인지 궁금해서 찾아보았을 때<h4 id="프론트">프론트</h4>
</li>
<li>카카오 SDK 로그인 버튼 → 클릭 시 카카오 로그인 화면 팝업<ul>
<li>카카오 SDK: 카카오가 배포하는 라이브러리? 카카오 로그인/공유하기/카카오맵을 앱/웹사이트에 붙일 수 있게 해주는 코드.</li>
<li>프론트에서 직접 카카오 인증 서버 URL로의 리다이렉트, 응답으로 온 인가코드 받고, 이걸 다시 보내서 accessToken으로 교환하고 에러 처리/팝업 관리하는 부분을 생략할 수 있음.</li>
</ul>
</li>
<li>사용자가 카카오 로그인을 하면, 카카오에서 프론트에게 accessToken을 발급</li>
<li>이 accessToken을 프론트 → 백엔드로 전달<h4 id="백엔드">백엔드</h4>
</li>
<li>전달 받은 accessToken을 사용해서 카카오 서버에 사용자 정보 요청</li>
<li>카카오 서버에서 해당 토큰을 검증한 후에 사용자 정보를 전달해줌</li>
<li>전달받은 사용자 정보를 변환해서 사용.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 11주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-11%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-11%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sat, 27 Jun 2026 14:35:26 GMT</pubDate>
            <description><![CDATA[<h1 id="11장-웹-애플리케이션-제작">11장. 웹 애플리케이션 제작</h1>
<blockquote>
</blockquote>
<p><strong>예제에서 사용하는 기술</strong></p>
<ul>
<li>뷰: JSP, JSTL</li>
<li>웹 계층: 스프링 MVC</li>
<li>데이터 저장 계층: JPA, 하이버네이트</li>
<li>기반 프레임워크: 스프링 프레임워크</li>
<li>빌드: 메이븐</li>
</ul>
<hr>
<h2 id="✔️-111-프로젝트-환경설정">✔️ 11.1 프로젝트 환경설정</h2>
<aside>

<p><strong>프로젝트 환경설정 진행순서</strong></p>
<ol>
<li>프로젝트 구조 분석</li>
<li>메이븐(Maven)과 라이브러리 설정</li>
<li>스프링 프레임워크 설정</aside>
</li>
</ol>
<ul>
<li><p>프로젝트 폴더 구조를 분석 → 메이븐에 사용할 라이브러리 지정 → 스프링 프레임워크 환경 설정</p>
</li>
<li><p>웹 서버 실행: 톰캣 플러그인 사용</p>
<ul>
<li><p>메뉴의 [Run] &gt; [Run As] &gt; [Maven Build…] 선택</p>
</li>
<li><p>Goals에 tomcat7:run을 입력하고 Run 버튼 선택</p>
<p>⇒ <a href="http://localhost:8080%EC%9C%BC%EB%A1%9C">http://localhost:8080으로</a> 서버가 실행됨</p>
</li>
</ul>
</li>
</ul>
<h3 id="1-프로젝트-구조">(1) 프로젝트 구조</h3>
<p>메이븐이 제공하는 표준 프로젝트 구조</p>
<pre><code class="language-markdown">jpashop (프로젝트 루트)
├── src (소스 폴더)
|    ├── main(실행 코드)
|    |     ├── java (자바 소스코드)
|    |     ├── resources (리소스)
|    |     └── webapp (웹 폴더)
|    └── test (테스트 코드)
├── target (빌드 결과)
└── pom.xml (메이븐 설정 파일)</code></pre>
<h3 id="2-메이븐과-사용-라이브러리-관리">(2) 메이븐과 사용 라이브러리 관리</h3>
<h4 id="pomxml-파일">pom.xml 파일</h4>
<ul>
<li><modelVersion>: POM 모델 버전.</li>
<li><groupId>: 프로젝트 그룹명 ex. org.springframework</li>
<li><artifactId>: 프로젝트를 식별하는 아이디 ex. spring-mvc</li>
<li><version>: 프로젝트 버전</li>
<li><name>: 프로젝트 이름</li>
<li><packaging>: 빌드 패키징 방법을 지정함. 웹 애플리케이션은 war, 자바 라이브러리는 jar로 설정</li>
<li><dependencies>: 사용할 라이브러리를 지정</li>
<li><build>: 빌드 관련 정보를 설정</li>
</ul>
<h4 id="dependencies-파일">dependencies 파일</h4>
<ul>
<li>핵심 라이브러리<ul>
<li><strong>스프링 MVC</strong>: 스프링 MVC 라이브러리</li>
<li><strong>스프링 ORM</strong>: 스프링 프레임워크와 JPA를 연동하기 위한 라이브러리</li>
<li><strong>JPA, 하이버네이트</strong>: JPA 표준과 하이버네이트를 포함하는 라이브러리. hibernate-entitymanager를 라이브러리로 지정하면 다음 중요 라이브러리도 함께 내려받음<ul>
<li>hibernate-core-(버전정보).jar: 하이버네이트 라이브러리</li>
<li>hibernate-jpa-2.1-api-(버전정보).jar: JPA 2.1 표준 인터페이스가 있는 라이브러리</li>
</ul>
</li>
</ul>
</li>
<li>기타 라이브러리<ul>
<li><strong>H2 데이터베이스</strong>: H2 데이터베이스는 아주 작은 데이터베이스로 별도 설치 없이 JVM 메모리 안에서 동작하는 기능도 있어 실습으로 적당함.</li>
<li><strong>WEB</strong>: 서블릿, JSP 관련 라이브러리</li>
<li><strong>로깅 SLF4J &amp; LogBack</strong>: 과거 Log4j가 많이 사용됐지만, 최근 SLF4J &amp; LogBack이 많이 사용함.</li>
<li><strong>테스트</strong>: 테스트용 라이브러리. spring-test는 스프링 프레임워크와 통합 테스트를 지원</li>
</ul>
</li>
</ul>
<h4 id="테스트">테스트</h4>
<ul>
<li>target 폴더에 빌드 결과로 <code>jpashop.war</code> 파일이 생성됨</li>
</ul>
<h3 id="3-스프링-프레임워크-설정">(3) 스프링 프레임워크 설정</h3>
<h4 id="webxml-웹-애플리케이션-환경설정-파일">web.xml: 웹 애플리케이션 환경설정 파일</h4>
<ul>
<li>스프링 프레임워크를 구동하기 위한 설정<ul>
<li>webAppConfig.xml: 스프링 MVC 설정을 포함해서 웹 계층을 담당</li>
<li>appConfig.xml: 비즈니스 로직, 도메인 계층, 서비스 계층, 데이터 저장 계층을 담당</li>
</ul>
</li>
</ul>
<h4 id="webappconfigxml-스프링-웹-관련-환경설정-파일">webAppConfig.xml: 스프링 웹 관련 환경설정 파일</h4>
<ul>
<li><a href="mvc:annotation-driven">mvc:annotation-driven</a>: 스프링 MVC 기능을 활성화</li>
<li>&lt;context: component-scan&gt;: basePackages를 포함한 하위 패키지를 검색해 <code>@Component</code>, <code>@Service</code>, <code>@Repository</code>, <code>@Controller</code> 어노테이션이 붙어있는 클래스들을 스프링 빈으로 자동 등록함.</li>
<li><bean>: 스프링 빈을 등록</li>
</ul>
<h4 id="appconfigxml-스프링-애플리케이션-관련-환경설정-파일">appConfig.xml: 스프링 애플리케이션 관련 환경설정 파일</h4>
<ul>
<li>&lt;tx: annotation-driven/&gt;: 스프링 프레임워크가 제공하는 어노테이션 기반의 트랜잭션 관리자를 활성화함. 이 기능은 <code>@Transactional</code>이 붙은 곳에 트랜잭션을 적용함</li>
</ul>
<pre><code class="language-markdown">jpashop (프로젝트 루트)
├── src (소스 폴더)
|    ├── main(실행 코드)
|    |     ├── java (자바 소스코드)
|         |     |     └── jpabook
|    |     |             └── jpashop
|    |     |                     ├── domain //도메인 계층
|    |     |                     ├── repository //데이터 저장 계층
|    |     |                     ├── service //서비스 계층
|    |     |                     └── web //웹 계층
|    |     |
|    |     ├── resources (리소스)
|    |     |       └── **appConfig.xml** //스프링 애플리케이션 관련 설정
|    |     |       └── **webAppConfig.xml** //스프링 웹 관련 설정
|    |     └── webapp (웹 폴더)
|    |            └── WEB-INF
|    |                   └── **web.xml** //웹 애플리케이션 환경설정 파일
|    └── test (테스트 코드)
├── target (빌드 결과)
└── pom.xml (메이븐 설정 파일)</code></pre>
<h4 id="데이터소스-설정">데이터소스 설정</h4>
<pre><code class="language-xml">&lt;bean id=&quot;dataSource&quot; class=&quot;org.apache.tomcat.jdbc.pool.DataSource&quot;&gt;
    &lt;property name=&quot;driverClassName&quot; value=&quot;org.h2.Driver&quot;/&gt;
    &lt;property name=&quot;url&quot; value=&quot;jdbc:h2:mem:jpashop&quot;/&gt;
    &lt;property name=&quot;username&quot; value=&quot;sa&quot;/&gt;
    &lt;property name=&quot;password&quot; value=&quot;&quot;/&gt;
&lt;/bean&gt;</code></pre>
<ul>
<li>데이터베이스에 접근할 데이터소스를 등록</li>
<li>H2 데이터베이스의 접속 URL을 <code>jdbc:h2:mem:...</code>으로 설정해 JVM 안에서 동작하는 인 메모리 데이터베이스로 사용함<ul>
<li>인 메모리 데이터베이스 → 별도의 데이터베이스 서버를 실행하지 않아도 됨</li>
</ul>
</li>
<li>애플리케이션을 시작할 때 데이터베이스도 애플리케이션 안에서 함께 실행되고 애플리케이션을 종료할 때 함께 사라짐</li>
</ul>
<h4 id="트랜잭션-관리자-설정">트랜잭션 관리자 설정</h4>
<pre><code class="language-xml">&lt;bean id=&quot;transactionalManager&quot; class=&quot;org.springframework.orm.jpa.JpatransactionManager&quot;&gt;
    &lt;property name=&quot;dataSource&quot; ref=&quot;dataSource&quot;&gt;
&lt;/bean&gt;</code></pre>
<ul>
<li>트랜잭션 관리자를 등록</li>
<li>JPA 사용을 위해서는 <code>org.springframework.orm.jpa.JpatransactionManager</code>를 트랜잭션 관리자로 등록해양함</li>
</ul>
<h4 id="jpa-예외-변환-aop-설정">JPA 예외 변환 AOP 설정</h4>
<pre><code class="language-xml">&lt;bean class=&quot;org.springframework.dao.annoation.PersistenceExceptionTranslationPostProcessor&quot;/&gt;</code></pre>
<ul>
<li><code>@Repository</code> 어노테이션이 붙어있는 스프링 빈에 예외 변환 AOP를 적용함</li>
</ul>
<h4 id="jpa-설정-엔티티-매니저-팩토리-등록">JPA 설정-엔티티 매니저 팩토리 등록</h4>
<pre><code class="language-xml">&lt;bean id=&quot;entityManagerFactory&quot; class=&quot;org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean&quot;&gt;
    &lt;property name=&quot;dataSource&quot; ref=&quot;dataSource&quot;&gt;
    &lt;!--@Entity 탐색 시작 위치--&gt;
    &lt;property name=&quot;packagesToScan&quot; value=&quot;jpabook.jpashop.domain&quot;/&gt;
    &lt;property name=&quot;jpaVendorAdapter&quot;&gt;
        &lt;!--하이버네이트 구현체 사용--&gt;
        &lt;bean class=&quot;org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter&quot;/&gt;
    &lt;/property&gt;
    &lt;property name=&quot;jpaProperties&quot;&gt; &lt;!--하이버네이트 상세 설정--&gt;
        &lt;props&gt;
            &lt;prop key=&quot;hibernate.dialect&quot;&gt;
                org.hibernate.dialect.H2Dialect &lt;/prop&gt; &lt;!--방언--&gt;
            &lt;prop key=&quot;hibernate.show_sql&quot;&gt;true&lt;/prop&gt; &lt;!--SQL 보기--&gt;
            &lt;prop key=&quot;hibernate.format_sql&quot;&gt;true&lt;/prop&gt; &lt;!--SQL 정렬해서 보기--&gt;
            ...
</code></pre>
<ul>
<li>스프링 프레임워크에서 JPA를 사용하기 위해서는 <code>LocalContainerEntityManagerFactoryBean</code>을 스프링 빈으로 등록해야함</li>
<li>J2SE 환경(순수 자바만 사용)에서는 persistence.xml에 엔티티 매니저 팩토리 정보를 설정<ul>
<li>LocalContainerEntityManagerFactoryBean: JPA 스프링 컨테이너에서 사용할 수 있도록 스프링 프레임워크가 제공하는 기능. 해당 클래스는 spring-orm 라이브러리가 제공</li>
<li>dataSource: 사용할 데이터소스를 등록</li>
<li>packagesToScan: <code>@Entity</code>가 붙은 클래스를 자동으로 검색하기 위한 시작점을 지정</li>
<li>persistenceUnitName: 영속성 유닛 이름을 지정. 지정하지 않을 시, default라는 이름의 영속성 유닛을 생성</li>
<li>jpaVendorAdapter: 사용할 JPA 벤더를 지정.</li>
</ul>
</li>
</ul>
<h4 id="하이버네이트-속성-설정">하이버네이트 속성 설정</h4>
<ul>
<li>hibernate.dialect: 사용할 데이터베이스 방언을 설정</li>
<li>hibernate.show_sql: 실행하는 SQL을 콘솔에 출력</li>
<li>hibernate.format_sql: SQL을 보기 좋게 정리해서 출력</li>
<li>hibernate.use_sql_comments: SQL 출력 시 어떻게 실행된 SQL인지 또는 사용자가 설정한 코멘트를 남김</li>
<li>hibernate.id.new_generator_mapping: JPA에 맞춘 새로운 ID 생성 방법을 사용.</li>
<li>hibernate.hbm2ddl.auto: 애플리케이션 시작 시, 테이블과 기타 DDL을 자동으로 생성 (4가지 옵션 존재)<ul>
<li>create: 기존 DDL을 제거하고 새로 생성</li>
<li>create-drop: create와 같지만, 애플리케이션 종료 시 생성했던 DDL을 삭제함</li>
<li>update: 현재 데이터베이스 DDL과 비교해 변경사항만 수정</li>
<li>validate: 현재 엔티티 매핑 정보와 데이터베이스 스키마가 같은지 비교. 다를 경우, 경고를 남기고 애플리케이션 실행하지 않음. DDL을 변경하지 않는 옵션.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="✔️-112-도메인-모델과-테이블-설계">✔️ 11.2 도메인 모델과 테이블 설계</h2>
<h3 id="1-요구사항-분석">(1) 요구사항 분석</h3>
<aside>

<ul>
<li>회원기능: 회원 등록, 회원 조회</li>
<li>상품기능: 상품 등록, 상품 수정, 상품 조회</li>
<li>주문기능:  상품 주문, 주문 내역 조회, 주문 취소</li>
<li>기타 요구사항: 상품 종류에는 ‘도서, 음반, 영화’ 존재. 상품을 카테고리로 구분 가능, 상품 주문 시 배송 정보 입력 가능</aside>

</li>
</ul>
<h3 id="2-도메인-모델-설계">(2) 도메인 모델 설계</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/8ccda877-ce79-4f87-bedb-8eb641934814/image.png" alt=""></p>
<ul>
<li>회원-주문-상품의 관계:<ul>
<li>회원은 여러 상품 주문 가능</li>
<li>한 번의 주문에 여러 상품을 선택 가능 ⇒ 주문-상품은 다대다 관계<ul>
<li>이런 다대다 관계는 관계형 데이터베이스 및 엔티티에서도 거의 사용하지 않기 때문에, <strong>주문 상품이라는 엔티티를 추가해 다대다 관계를 일대다, 다대일 관계로 품.</strong></li>
</ul>
</li>
</ul>
</li>
<li>상품 분류: 상품은 도서, 음반, 영화로 구분되는데 이대 상품의 공통 속성을 사용하기에 상속 구조로 표현함</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/77ecd9b6-9e87-4cca-abb0-47879b3518d7/image.png" alt=""></p>
<ul>
<li>회원:<ul>
<li>이름, 주문한 상품들, 주소(임베디드 타입)</li>
</ul>
</li>
<li>주문: 주문-주문상품은 일대다 관계.<ul>
<li>주문한 회원, 배송정보, 주문날짜, 주문 상태(열거형)</li>
</ul>
</li>
<li>주문상품<ul>
<li>주문한 상품 정보, 주문 금액, 주문 수량 정보</li>
</ul>
</li>
<li>상품<ul>
<li>이름, 가격, 재고수량</li>
</ul>
</li>
<li>배송: 주문 시 하나의 배송 정보를 생성. 주문-배송은 일대일 관계</li>
<li>카테고리: 카테고리-상품 다대다 관계</li>
<li>주소: 값 타입(임베디드 타입). 회원과 배송에서 사용</li>
</ul>
<h3 id="3-테이블-설계">(3) 테이블 설계</h3>
<ul>
<li>MEMBER: 회원 엔티티의 주소 임베디드 타입 정보가 회원 테이블에 그대로 들어감</li>
<li>ITEM: 앨범, 도서, 영화 타입을 통해해 하나의 테이블로 만들고 DTYPE 컬럼을 통해 구분.</li>
</ul>
<h3 id="4-연관관계-정리">(4) 연관관계 정리</h3>
<ul>
<li>회원과 주문: <strong>일대다 양방향 관계</strong>.<ul>
<li>외래키가 있는 주문이 연관관계의 주인</li>
<li>Order.member를 ORDERS.MEMBER_ID 외캐리와 매핑함</li>
</ul>
</li>
<li>주문상품과 주문: <strong>다대일 양방향 관계</strong><ul>
<li>주문상품이 연관관계의 주인.</li>
<li>OrderItem.order를 ORDER_ITEM.ORDER_ID 외래키와 매핑함</li>
</ul>
</li>
<li>주문과 배송: <strong>일대일 양방향 관계</strong><ul>
<li>Order.delivery를 ORDERS.DELIVERY_ID 외래키와 매핑함</li>
</ul>
</li>
<li>카테고리와 상품: <code>@ManyToMany</code>를 사용해 매핑함</li>
</ul>
<h3 id="5-엔티티-클래스">(5) 엔티티 클래스</h3>
<ul>
<li>앞의 설계를 기반으로 필요한 엔티티 클래스의 코드를 작성함</li>
<li>아래는 Member의 클래스</li>
</ul>
<pre><code class="language-java">...
@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = &quot;MEMBER_ID&quot;)
    private Long id;

    private String name;

    @Embedded
    private Address address;

    @OneToMany(mappedBy = &quot;member&quot;)
    private List&lt;Order&gt; orders = new ArrayList&lt;Order&gt;();

    public Long getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public List&lt;Order&gt; getOrders() {
        return orders;
    }

    public void setOrders(List&lt;Order&gt; orders) {
        this.orders = orders;
    }

    @Override
    public String toString() {
        return &quot;Member{&quot; +
                &quot;id=&quot; + id +
                &quot;, name=&#39;&quot; + name + &#39;\&#39;&#39; +
                &quot;, address=&quot; + address +
                &#39;}&#39;;
    }
}</code></pre>
<hr>
<h2 id="113-애플리케이션-구현">11.3 애플리케이션 구현</h2>
<h3 id="1-개발-방법">(1) 개발 방법</h3>
<p>!image.png</p>
<ul>
<li><strong>Controller</strong>: MVC의 컨트롤러가 모여있는 곳. <strong>서비스 계층을 호출하고 결과를 뷰(JSP)에 전달</strong>함</li>
<li><strong>Service</strong>: 서비스계층에는 <strong>비즈니스 로직</strong>이 있고 <strong>트랜잭션</strong>을 시작함. 서비스 계층은 데이터 접근 계층인 <strong>레포지토리를 호출</strong>함.</li>
<li><strong>Repository</strong>: JPA를 직접 사용하는 곳. 엔티티를 매니저를 사용해 엔티티를 저장하고 조회.</li>
<li><strong>Domain</strong>: 엔티티가 모여있는 계층으로 모든 계층에서 사용.</li>
</ul>
<p>⇒ 개발순서: 비즈니스 로직을 수행하는 서비스(Service)와 레포지토리(Repository) 계층을 먼저 개발 → 테스트 케이스를 작성해 검증 → 검증 완료 후 컨트롤러와 뷰를 개발</p>
<h3 id="2-회원-기능">(2) 회원 기능</h3>
<ul>
<li>구현기능: 회원 등록, 회원 목록 조회</li>
</ul>
<h4 id="도메인domain">도메인(Domain)</h4>
<pre><code class="language-java">@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = &quot;MEMBER_ID&quot;)
    private Long id;

    private String name;

    @Embedded
    private Address address;

    @OneToMany(mappedBy = &quot;member&quot;)
    private List&lt;Order&gt; orders = new ArrayList&lt;Order&gt;();

    ...
}</code></pre>
<h4 id="레포지토리repository">레포지토리(Repository)</h4>
<ul>
<li><code>@Repository</code>:<ul>
<li><a href="context:component-scan">context:component-scan</a>에 의해 스프링 빈으로 자동 등록됨</li>
<li>JPA 전용 예외 발생 시, 스프링이 추상화한 예외로 변환해줌</li>
</ul>
</li>
<li><code>@PersistenceContext</code>:<ul>
<li><strong>컨테이너가 관리하는 엔티티 매니저를 주입하는 어노테이션</strong></li>
<li>순수 자바 환경에서 엔티티 매니저 팩토리에서 엔티티 매니저를 직접 생성해 사용하지만, 스프링/J2EE 컨테이너 사용 시, 컨테이너가 엔티티 매니저를 관리하고 제공함. 따라서, 엔티티 매니저를 직접 생성해 사용하는 것이 아니라, 컨테이너가 제공하는 엔티티 매니저를 사용해야함</li>
</ul>
</li>
<li><code>@PersistenceUnit</code><ul>
<li>엔티티 매니저 팩토리를 주입받기 위해서 사용</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Repository
public class MemberRepository {

    @PersistenceContext
    EntityManager em;

    public void save(Member member) {
        em.persist(member);
    }

    public Member findOne(Long id) {
        return em.find(Member.class, id);
    }

    public List&lt;Member&gt; findAll() {
        return em.createQuery(&quot;select m from Member m&quot;, Member.class).getResultList();
    }

    public List&lt;Member&gt; findByName(String name) {
        return em.createQuery(&quot;select m from Member m where m.name = :name&quot;, Member.class)
        .setParameter(&quot;name&quot;, name)
        .getResultList();
    }
}</code></pre>
<h4 id="서비스service">서비스(Service)</h4>
<ul>
<li><code>@Service</code>: <a href="context:component-scan">context:component-scan</a>에 의해 스프링 빈으로 등록됨</li>
<li><code>@Transactional</code>: 트랜잭션을 적용해줌<ul>
<li>외부에서 해당 클래스의 메소드를 호출하면 <strong>트랜잭션을 시작하고, 메소드가 종료될 때, 트랜잭션을 커밋함</strong>. 예외 발생 시, 트랜잭션을 롤백함</li>
</ul>
</li>
<li><code>@Autowired</code>: 스프링 컨테이너가 적절한 스프링 빈을 주입해줌.</li>
</ul>
<pre><code class="language-java">@Service
@Transactional
public class MemberService {

    @Autowired
    MemberRepository memberRepository;

    public Long join(Member member) {

        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        List&lt;Member&gt; findMembers = memberRepository.findByName(member.getName());
        if (!findMembers.isEmpty()) {
            throw new IllegalStateException(&quot;이미 존재하는 회원입니다.&quot;);
        }
    }

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

    public Member findOne(Long memberId) {
        return memberRepository.findOne(memberId);
    }
}</code></pre>
<ul>
<li><p>회원가입 (<code>join()</code>)</p>
<ul>
<li><p><code>validateDuplicateMember()</code>를 통해 중복 회원가입을 방지함</p>
<p>  동일한 이름을 가진 회원이 존재한다면, 메시지와 함께 예외를 발생시키고 성공했을 경우에는 회원 식별자를 반환해줌</p>
</li>
</ul>
</li>
</ul>
<h4 id="테스트-1">테스트</h4>
<ul>
<li><code>@RunWith(SpringJUnit4ClassRunner.class)</code><ul>
<li>JUnit으로 작성한 테스트 케이스를 스프링 프레임워크와 통합하기 위한 것</li>
<li>테스트가 스프링 컨테이너에서 실행되어 제공되는 <code>@Autowired</code> 같은 기능을 사용할 수 있음</li>
</ul>
</li>
<li><code>@ContextConfiguration(locations = &quot;classpath:appConfig.xml&quot;)</code><ul>
<li>테스트 케이스 실행 시 사용할 스프링 정보 지정. (여기서는 설정 정보로 appConfig.xml을 설정함)</li>
</ul>
</li>
<li>트랜잭션<ul>
<li>테스트에서는 실행 마다 트랜잭션을 시작하고, 테스트 종료 후에는 트랜잭션을 강제로 롤백하기에, 데이터베이스에 저장한 내용이 초기화되어 반복해 테스트를 진행할 수 있음</li>
</ul>
</li>
</ul>
<pre><code class="language-java">
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = &quot;classpath:appConfig.xml&quot;)
@Transactional
public class MemberServiceTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    public void 회원가입() throws Exception {

        //Given
        Member member = new Member();
        member.setName(&quot;kim&quot;);

        //When
        Long saveId = memberService.join(member);

        //Then
        assertEquals(member, memberRepository.findOne(saveId));
    }

    @Test(expected = IllegalStateException.class)
    public void 중복_회원_예외() throws Exception {

        //Given
        Member member1 = new Member();
        member1.setName(&quot;kim&quot;);

        Member member2 = new Member();
        member2.setName(&quot;kim&quot;);

        //When
        memberService.join(member1);
        memberService.join(member2); //예외가 발생해야 한다.

        //Then
        fail(&quot;예외가 발생해야 한다.&quot;);
    }

}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 팀프로젝트 4주차 작업로그]]></title>
            <link>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-4%EC%A3%BC%EC%B0%A8-%EC%9E%91%EC%97%85%EB%A1%9C%EA%B7%B8</link>
            <guid>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-4%EC%A3%BC%EC%B0%A8-%EC%9E%91%EC%97%85%EB%A1%9C%EA%B7%B8</guid>
            <pubDate>Tue, 09 Jun 2026 04:46:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h3 id="4주차-작업">4주차 작업</h3>
<p>1-3주차에 작업한 데이터베이스 쿼리 및 API 명세서를 기반으로 스프링의 CRUD 기능을 사용해 필요한 백엔드 부분을 구현한 뒤에 AI를 사용해 구현한 백엔드의 DTO 및 API를 활용해 프론트엔드를 구현. 이후 Vercel과 AWS를 활용해 배포를 진행하였습니다.</p>
</blockquote>
<hr>
<h1 id="spring-crud-작성">Spring CRUD 작성</h1>
<h2 id="상품">상품</h2>
<h3 id="1-폴더-분리">1. 폴더 분리</h3>
<ul>
<li>폴더는 controller, domain(entity), dto(response/request), service, repository로 분리하여 진행하였습니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/e5754414-f0a5-4eb7-b6a4-8c0240406d73/image.png" alt=""></p>
<ul>
<li>SQL 쿼리 공유</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/c299fe1e-8e3e-432e-b1cb-b3cd589152bb/image.png" alt=""></p>
<h3 id="2-controller">2. Controller</h3>
<ul>
<li>상품 생성 (POST)</li>
<li>상품 조회 (GET)<ul>
<li>상품 목록 조회 (일반)</li>
<li>상품 카테고리별 조회</li>
<li>상품 가격순 조회</li>
<li>상품 개별 조회</li>
</ul>
</li>
<li>상품 가격 수정 (PATCH)</li>
<li>상품 삭제 (DELETE)</li>
</ul>
<h4 id="코드">코드</h4>
<pre><code class="language-java">@Controller
@RequiredArgsConstructor
@RequestMapping(&quot;/products&quot;)
public class ProductController {
    private final ProductService productService;

    //TODO: 상품 생성
    @PostMapping
    public ResponseEntity&lt;ProductResponse&gt; createProduct(@RequestBody CreateProductRequest request) {
        ProductResponse response = productService.createProduct(request);

        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }

    //TODO: 상품 목록 조회 (가격순 정렬)
    @GetMapping
    public ResponseEntity&lt;ProductListResponse&gt; getProductList(@RequestParam(defaultValue = &quot;productName,desc&quot;) String sortBy) {
        ProductListResponse response = productService.getProductList(sortBy);

        return ResponseEntity.ok(response);
    }

    //TODO: 카테고리별 상품 목록 조회 (가격순 정렬)
    @GetMapping(&quot;/category/{categoryId}&quot;)
    public ResponseEntity&lt;ProductListResponse&gt; getProductListByCategory(@PathVariable(&quot;categoryId&quot;) Long categoryId,
                                                                        @RequestParam(defaultValue = &quot;productName,desc&quot;) String sortBy) {
        ProductListResponse response = productService.getProductListByCategory(categoryId, sortBy);

        return ResponseEntity.ok(response);
    }

    //TODO: 상품 개별 조회
    @GetMapping(&quot;/{productId}&quot;)
    public ResponseEntity&lt;ProductResponse&gt; getProductDetail(@PathVariable(&quot;productId&quot;) Long productId) {
        ProductResponse response = productService.getProductDetail(productId);

        return ResponseEntity.ok(response);
    }

    //TODO: 상품 검색
    @GetMapping(&quot;/search&quot;)
    public ResponseEntity&lt;ProductListResponse&gt; searchProduct(@RequestBody SearchProduct request,
                                                         @RequestParam(defaultValue = &quot;productName,desc&quot;) String sortBy) {
        ProductListResponse response = productService.searchProduct(request, sortBy);

        return ResponseEntity.ok(response);

    }

    //TODO: 상품 가격 업데이트
    @PatchMapping(&quot;/{productId}&quot;)
    public ResponseEntity&lt;Void&gt; updateProductPrice(@PathVariable(&quot;productId&quot;) Long productId,
                                                   @RequestBody UpdateProductPrice request) {
        productService.updateProductPrice(productId, request);

        return ResponseEntity.ok().build();
    }

    //TODO: 상품 삭제
    @DeleteMapping(&quot;/{productId}&quot;)
    public ResponseEntity&lt;Void&gt; deleteProduct(@PathVariable(&quot;productId&quot;) Long productId) {
        productService.deleteProduct(productId);

        return ResponseEntity.ok().build();
    }
}</code></pre>
<h3 id="3-domainentity">3. Domain(Entity)</h3>
<h4 id="category">Category</h4>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;category_id&quot;)
    private Long id;

    @Column(name = &quot;category_name&quot;)
    private String category;

    @Builder
    public Category(String category) {
        this.category = category;
    }
}</code></pre>
<h4 id="manufacturer">Manufacturer</h4>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor
public class Manufacturer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;manufacturer_id&quot;)
    private Long id;

    @Column (name = &quot;company_name&quot;)
    private String company;

    @Column (name = &quot;owner&quot;)
    private String owner;

    @Builder
    public Manufacturer(String company, String owner) {
        this.company = company;
        this.owner = owner;
    }
}
</code></pre>
<h4 id="product">Product</h4>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor
public class Product {
    @Id
    @GeneratedValue (strategy = GenerationType.IDENTITY)
    @Column(name = &quot;product_id&quot;)
    private Long id;

    @ManyToOne (fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;manufacturer_id&quot;)
    private Manufacturer manufacturer;

    @Column(name = &quot;product_name&quot;)
    private String productName;

    @Column(name = &quot;price&quot;)
    private int price;

    @OneToOne
    @JoinColumn(name = &quot;category_id&quot;)
    private Category category;

    @Column (name = &quot;image_url&quot;)
    private String imageUrl;

    @Builder
    public Product(Manufacturer manufacturer, String name, int price, Category category, String imageUrl) {
        this.manufacturer = manufacturer;
        this.productName = name;
        this.category = category;
        this.price = price;
        this.imageUrl = imageUrl;
    }

    public void updatePrice(int price) {
        this.price = price;
    }

}</code></pre>
<h3 id="4-dto">4. Dto</h3>
<h4 id="createproductrequest">CreateProductRequest</h4>
<pre><code class="language-java">@Getter
@AllArgsConstructor
public class CreateProductRequest {
    private Long manufacturerId;
    private String productName;
    private int price;
    private Long categoryId;
    private String imageUrl;
}</code></pre>
<h4 id="searchproduct">SearchProduct</h4>
<pre><code class="language-java">@Getter
@AllArgsConstructor
public class SearchProduct {
    private String keyword;
}</code></pre>
<h4 id="updateproductprice">UpdateProductPrice</h4>
<pre><code class="language-java">@Getter
@AllArgsConstructor
public class UpdateProductPrice {
    private int price;
}</code></pre>
<h4 id="productlistresponse">ProductListResponse</h4>
<pre><code class="language-java">@Getter
@Builder
@AllArgsConstructor
public class ProductListResponse {
    private List&lt;ProductResponse&gt; productResponseList;
    private Long productCount;

    public static ProductListResponse of (List&lt;Product&gt; products) {
        return ProductListResponse.builder()
                .productResponseList(products.stream().map(ProductResponse::of).toList())
                .productCount((long) products.size())
                .build();
    }
}</code></pre>
<h4 id="productresponse">ProductResponse</h4>
<pre><code class="language-java">@Getter
@Builder
@AllArgsConstructor
public class ProductResponse {
    private Long productId;
    private String productName;
    private int price;
    private String manufacturer;
    private String category;
    private String imageUrl;

    public static ProductResponse of (Product product) {
        return ProductResponse.builder()
                .productId(product.getId())
                .productName(product.getProductName())
                .price(product.getPrice())
                .category(product.getCategory().getCategory())
                .manufacturer(product.getManufacturer().getCompany())
                .imageUrl(product.getImageUrl())
                .build();
    }
}
</code></pre>
<h3 id="5-repository">5. Repository</h3>
<ul>
<li>검색 관련 쿼리만 JPQL을 사용하여 작성하였습니다.</li>
</ul>
<pre><code class="language-java">public interface ProductRepository extends JpaRepository&lt;Product, Long&gt; {
    List&lt;Product&gt; findAllByCategory(Category category, Sort sort);
    @Query(&quot;select p from Product p where p.productName LIKE %:keyword%&quot; )
    List&lt;Product&gt; searchByName(@Param(&quot;keyword&quot;)String keyword, Sort sort);
}</code></pre>
<h3 id="6-service">6. Service</h3>
<h4 id="코드-1">코드</h4>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class ProductService {

    private final ManufacturerRepository manufacturerRepository;
    private final CategoryRepository categoryRepository;
    private final ProductRepository productRepository;

    //TODO: 상품 생성
    @Transactional
    public ProductResponse createProduct(CreateProductRequest request) {
        Manufacturer manufacturer = getManufacturer(request.getManufacturerId());
        Category category = getCategory(request.getCategoryId());

        Product newProduct = Product.builder()
                .manufacturer(manufacturer)
                .name(request.getProductName())
                .price(request.getPrice())
                .category(category)
                .imageUrl(request.getImageUrl())
                .build();

        productRepository.save(newProduct);

        return ProductResponse.of(newProduct);
    }

    //TODO: 상품 목록조회 (일반)
    @Transactional(readOnly = true)
    public ProductListResponse getProductList(String sortBy) {
        Sort sort = createSort(sortBy);
        List&lt;Product&gt; productList = productRepository.findAll(sort);

        return ProductListResponse.of(productList);
    }

    //TODO: 상품 목록조회 (카테고리)
    @Transactional(readOnly = true)
    public ProductListResponse getProductListByCategory(Long categoryId, String sortBy) {
        Category category = getCategory(categoryId);
        Sort sort = createSort(sortBy);
        List&lt;Product&gt; productList = productRepository.findAllByCategory(category, sort);

        return ProductListResponse.of(productList);
    }

    //TODO: 상품 개별조회
    @Transactional(readOnly = true)
    public ProductResponse getProductDetail(Long productId) {
        Product product = getProduct(productId);

        return ProductResponse.of(product);
    }

    //TODO: 상품 가격 수정
    @Transactional
    public void updateProductPrice(Long productId, UpdateProductPrice request) {
        Product product = getProduct(productId);
        product.updatePrice(request.getPrice());
    }

    //TODO: 상품 삭제
    @Transactional
    public void deleteProduct(Long productId) {
        Product product = getProduct(productId);
        productRepository.delete(product);
    }

    //TODO: 상품 검색
    @Transactional(readOnly = true)
    public ProductListResponse searchProduct(SearchProduct request, String sortBy) {
        Sort sort = createSort(sortBy);
        List&lt;Product&gt; productList = productRepository.searchByName(request.getKeyword(),sort);

        return ProductListResponse.of(productList);
    }

    private Product getProduct(Long productId) {
        return productRepository.findById(productId)
                .orElseThrow(() -&gt; new CustomException(ErrorCode.NOT_FOUND));
    }
    private Manufacturer getManufacturer(Long manufacturerId) {
        return manufacturerRepository.findById(manufacturerId)
                .orElseThrow(() -&gt; new CustomException(ErrorCode.NOT_FOUND));
    }

    private Category getCategory (Long categoryId) {
        return categoryRepository.findById(categoryId)
                .orElseThrow(() -&gt; new CustomException(ErrorCode.NOT_FOUND));
    }

    //AI 도움.
    private Sort createSort (String sortBy) {
        try {
            String[] sortParams = sortBy.split(&quot;,&quot;);
            String property = sortParams[0];
            String direction = sortParams[1];

            if (direction.equalsIgnoreCase(&quot;asc&quot;)) {
                return Sort.by(Sort.Direction.ASC, property);
            } else {
                return Sort.by(Sort.Direction.DESC, property);
            }
        } catch (Exception e) {
            return Sort.by(Sort.Direction.DESC, &quot;productName&quot;);
        }
    }

}</code></pre>
<h3 id="7-테스트-화면">7. 테스트 화면</h3>
<h4 id="1-상품-목록-조회-일반-get">(1) 상품 목록 조회: 일반 (GET)</h4>
<h4 id="url-products">URL: /products</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/e8350cad-0b3c-4e37-980d-348a2ffe7b63/image.png" alt=""></p>
<hr>
<h4 id="2-상품-목록-조회-카테고리별-get">(2) 상품 목록 조회: 카테고리별 (GET)</h4>
<h4 id="url-productcategorycategoryid">URL: /product/category/{categoryId}</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/3629f0db-4223-4b20-9268-48b384f7f211/image.png" alt=""></p>
<hr>
<h4 id="3-상품-목록-조회-가격순-내림차순-get">(3) 상품 목록 조회 (가격순: 내림차순) (GET)</h4>
<h4 id="url-productssortbypricedesc">URL: /products?sortBy=price,desc</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/10d798cd-2726-40b2-95cb-053692ace4e7/image.png" alt=""></p>
<hr>
<h4 id="4-상품-목록-조회-가격순-오름차순-get">(4) 상품 목록 조회 (가격순: 오름차순) (GET)</h4>
<h4 id="url-productssortbypriceasc">URL: /products?sortBy=price,asc</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/fda866b6-ed0e-4499-97d7-466a727d5988/image.png" alt=""></p>
<hr>
<h4 id="5-상품-개별-조회-get">(5) 상품 개별 조회 (GET)</h4>
<h4 id="url-productsproductid">URL: /products/{productId}</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/d4f564c4-4b7b-4572-96fa-566a991355de/image.png" alt=""></p>
<hr>
<h4 id="6-상품-생성-post">(6) 상품 생성 (POST)</h4>
<h4 id="url-products-1">URL: /products</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/0cd7a360-b8ca-40f6-aa0a-8ff970c0eb20/image.png" alt=""></p>
<hr>
<h4 id="7-상품-가격-수정">(7) 상품 가격 수정</h4>
<h4 id="url-productsproductid-1">URL: /products/{productId}</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/da5fd3f3-9244-44ec-8272-2c0041286c1d/image.png" alt=""></p>
<hr>
<h4 id="8-상품-삭제">(8) 상품 삭제</h4>
<h4 id="url-productsproductid-2">URL: /products/{productId}</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/ea0072cf-97ce-4e38-ab42-bc8a788448b9/image.png" alt=""></p>
<hr>
<h4 id="9-상품-키워드-검색">(9) 상품 키워드 검색</h4>
<h4 id="url-productssearch">URL: /products/search</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/8ddfb17f-8a51-4c3a-91dd-c2cbcf313b54/image.png" alt=""></p>
<hr>
<h2 id="상품상세sku">상품상세(SKU)</h2>
<h3 id="상품상세-생성">상품상세 생성</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/f1854b5c-3521-4692-b15b-ddd6b8272395/image.png" alt=""></p>
<h3 id="상품상세-조회">상품상세 조회</h3>
<p>상품마다 어떤 옵션이 존재하고 있는지를 반환받도록 구성</p>
<p>보면 ProductId는 모두 동일하고 SKU(판매단위)가 되는 ProductDetailId 및 관련 내용들만 상이함.
<img src="https://velog.velcdn.com/images/summeryoung_/post/d61671ac-5816-4bd8-b2e4-482a3cc865a4/image.png" alt=""></p>
<h3 id="상품옵션-조회">상품옵션 조회</h3>
<p>상품상세 하나를 조회하는 것 (즉, 사이즈, 색상까지 모두 선택한 상품상세 SKU를 하나 조회)
<img src="https://velog.velcdn.com/images/summeryoung_/post/0bc3322d-4478-46a4-b55f-1fc45cbe08ff/image.png" alt=""></p>
<h3 id="상품옵션-수정">상품옵션 수정</h3>
<ul>
<li>남은 재고 수량, 판매량, 추가금, 이미지 링크 등을 수정할 수 있으면 변화가 없을 때는 기존의 값이 유지됨.
<img src="https://velog.velcdn.com/images/summeryoung_/post/a4e6645a-a164-4884-a935-3a25ab2f07f1/image.png" alt=""></li>
</ul>
<hr>
<h2 id="리뷰">리뷰</h2>
<h3 id="리뷰-생성">리뷰 생성</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/5294aad3-fe1b-49f0-8da1-0ddee7f4611d/image.png" alt=""></p>
<h3 id="상품별리뷰-조회">(상품별)리뷰 조회</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/c0227a84-b940-40f4-8034-691056094c3c/image.png" alt=""></p>
<h3 id="리뷰-수정">리뷰 수정</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/2fe473bc-8242-4420-902a-02efa5a32a40/image.png" alt=""></p>
<h3 id="리뷰-삭제">리뷰 삭제</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/145b59a3-e146-4b53-8f3c-e7f86a8c07bc/image.png" alt=""></p>
<hr>
<h2 id="통계">통계</h2>
<h3 id="1-연월일별-매출-조회">(1) 연/월/일별 매출 조회</h3>
<h4 id="연매출-조회">연매출 조회</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/edf0355d-566c-4bb3-9016-cfba91109fdf/image.png" alt=""></p>
<h4 id="월별-매출-조회">월별 매출 조회</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/03f9d28e-54c6-4960-80a7-666310c72475/image.png" alt=""></p>
<h4 id="일별-매출-조회">일별 매출 조회</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/2dea1448-878d-404c-8448-eb7bfb65573d/image.png" alt=""></p>
<h3 id="2-상품별-판매량-조회">(2) 상품별 판매량 조회</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/00af6aec-31c6-4497-a263-a77f1d1178fa/image.png" alt=""></p>
<h3 id="3-카테고리별-조회">(3) 카테고리별 조회</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/4390f6ae-10e2-4754-99b3-e0c8302b0ed2/image.png" alt=""></p>
<hr>
<blockquote>
<h4 id="소감">소감</h4>
<p>뭔가 처음에는 빨리해서 끝내려고 했는데 생각보다 이렇게 쌓아보니까 팀원 각자가 한 일이 너무 많았다. 그리고 뭔가 부피도 엄청 컸다. 스프링으로 구현하니까 더 컸다.  <br>
왠지 ERD 작성하고 개념적 설계, 논리적 설계 이런 부분을 진행할 때는 목표에 엔티티도 많고 해서 팀원 각자가 설계해볼 수 있어서 괜찮은 것 같았는데 이걸 또 다 구현하고 정규화도 하고, 쿼리도 작성하다보니까 정말 힘들었다. <br>
약간 규모를 좀 더 잘 잡을 수 있지 않았을까 싶기도 하다. <br>
힘들었다는 내용은 일단 제쳐두고 소감을 생각해보면, 이게 상품, 장바구니 류를 주로 맡아서 계속 진행을 했는데 상품 부분 데이터베이스를 짜는게 생각보다 까다로웠다. 옵션을 만들까말까하다가 사실 처음에 빼자고 했는데 ERD 그리면서 만들어버려서 그냥 끝까지 만들게되었다. <br>
상품마다 옵션이 여러 개가 있고? 또 종류도 여러개가 있으니까 이게 구현하기가 진짜 어려웠다. 다대다 관계를 테이블로 빼는건 둘째치고, 이걸 계속 조인해서 쓰고 이런게 진짜 좋은건지는? 일단 그런식으로 구현을 하긴했는데 잘 모르겠다. 원래는 EXPLAIN 이런걸 써서 인덱스 설정 효과를 보거나 이것저것 더 테스트를 해보려고 했는데 여력이 안돼서 포기해야해서 아쉬웠다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 10주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-10%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-10%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sun, 31 May 2026 04:05:49 GMT</pubDate>
            <description><![CDATA[<h1 id="10장-객체지향-쿼리-언어">10장. 객체지향 쿼리 언어</h1>
<h2 id="104-querydsl">10.4 QueryDSL</h2>
<p>JPA Criteria는 문자가 아닌 코드로 JPQL을 작성하기에 문법 오류를 컴파일 단계에서 잡을 수 있고, IDE 자동완성 기능의 도움을 받을 수 있다는 장점이 존재하지만, 단점은 너무 복잡하고 어렵다는 점이다.</p>
<p><strong>쿼리를 문자가 아닌 코드로 작성해도 쉽고, 간결하며 그 모양도 쿼리와 비슷하게 개발할 수 있는</strong> 프로젝트가 <strong>QueryDSL</strong>. </p>
<h3 id="1-querydsl-설정">(1) QueryDSL 설정</h3>
<h4 id="필요-라이브러리">필요 라이브러리</h4>
<ul>
<li>querydsl-jpa: QueryDSL JPA 라이브러리</li>
<li>querydsl-apt: 쿼리 타입 생성 시 필요한 라이브러리</li>
</ul>
<h4 id="환경설정">환경설정</h4>
<p>QueryDSL을 사용하기 위해서는 Criteria 메타 모델처럼 엔티티 기반의 <strong>쿼리 타입</strong>이라는 쿼리용 클래스를 생성해야함. </p>
<p>쿼리 타입 생성용 플러글인은 <code>pom.xml</code>에 추가해야함.</p>
<h3 id="2-시작">(2) 시작</h3>
<pre><code class="language-java">public void queryDSL() {
    EntityManager em = emf.createEntityManager();

    JPAQuery query = new JPAQuery(em);
    QMember qMember = new QMember(&quot;m&quot;);
    List&lt;Member&gt; members = query.from(qMember)
        .where(qMember.name.eq(&quot;회원1&quot;))
        .orderBy(qMember.name.desc())
        .list(qMember);

}</code></pre>
<p>QueryDSL 사용을 위해서는 JPAQuery 객체를 생성해야하는데, 이때 <strong>엔티티매니저를 생성자에게 넘겨</strong>준다.</p>
<h4 id="기본-q-생성">기본 Q 생성</h4>
<p>쿼리타입은 사용이 편리하도록 기본 인스턴스를 보관하고 있음. 다만, 같은 엔티티를 조인하거나 같은 엔티티를 서브쿼리에 사용하면 같은 별칭이 사용되기에 이때는 별칭을 직접 지정해서 사용해야함</p>
<pre><code class="language-java">public class QMember extends EntityPathBase&lt;Member&gt; {
    public static final QMember member = new QMember(&quot;member1&quot;);
}

QMember qMember = new QMember(&quot;m&quot;); //직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용</code></pre>
<p>쿼리 타입의 기본 인스턴스를 사용하면 <code>import static</code>을 활용해 코드를 더 간결하게 작성할 수 있음.</p>
<pre><code class="language-java">import static jpabook.jpashop.domain.QMember.member; //기본 인스턴스

public void basic() {
    EntityManager em = emf.createEntityManager();
    ...
}</code></pre>
<h3 id="3-검색-조건-쿼리">(3) 검색 조건 쿼리</h3>
<p>QueryDSL의 기본 쿼리기능</p>
<pre><code class="language-java">JPAQuery query = new JPAQuery(em);
QItem item = QItem.item;
List&lt;Item&gt; list = query.from(item)
    .where(item.name.eq(&quot;좋은상품&quot;).and(item.price.gt(20000)))
    .list(item); //조회할 프로젝션 지정</code></pre>
<p>QueryDSL의 where절에는 and나 or을 사용할 수 있음. 또한 다음처럼 여러 검색 조건을 사용해도 되며 이때에는 and 연산이 가능하다.</p>
<p>쿼리 타입의 필드는 필요한 대부분의 메소드를 명시적으로 제공한다.</p>
<h3 id="4-결과-조회">(4) 결과 조회</h3>
<p>쿼리 작성 이후 결과 조회 메소드를 호출하면 실제 데이터베이스를 조회한다. 보통 <code>uniqueResult()</code>나 <code>list()</code>를 사용하고 파라미터로 프로젝션 대상을 넘겨줌.</p>
<ul>
<li><code>uniqueResult()</code>: 조회 결과가 한 건일 때 사용. 조회 결과가 없으면 null을 반환하고 결과가 하나 이상이면 예외 발생</li>
<li><code>singleResult()</code>: <code>uniqueResult()</code>와 같지만 결과가 하나 이상이면 처음 데이터를 반환</li>
<li><code>list()</code>: 결과가 하나 이상일 때 사용하면, 없을 때는 빈 컬렉션을 반환.</li>
</ul>
<h3 id="5-페이징과-정렬">(5) 페이징과 정렬</h3>
<pre><code class="language-java">QItem item = QItem.item;

query.from(item)
    .where(item.price.gt(20000))
    .orderBy(item.price.desc(), item.stockQuantity.asc())
    .list(item);</code></pre>
<p>정렬은 <code>orderBy</code>를 사용하는데 쿼리 타입이 제공하는 <code>asc()</code>, <code>desc()</code>를 사용. <strong>페이징의 경우는 <code>offset</code>과 <code>limit</code>을 적절히 조합해서 사용하면 됨</strong>.</p>
<p>페이징은 <code>restrict()</code> 메소드에 QueryModifiers를 파라미터로 사용해도 됨.</p>
<pre><code class="language-java">QueryModifiers queryModifiers = new QueryModifiers(20L, 10L); //limit, offset
List&lt;Item&gt; list =
    query.from(item)
    .restrict(queryModifiers)
    .list(item);</code></pre>
<p>실제 페이징 처리를 위해서는 검색된 전체 데이터의 수를 알아야함. 이때는 <code>list()</code> 대신 <code>listResults()</code>를 사용함.</p>
<pre><code class="language-java">SearchResults&lt;Item&gt; result = 
    query.from(item)
    .where(item.price.gt(10000))
    .offset(10).limit(20)
    .listResults(item);

long total = result.getTotal(); //검색된 전체 데이터 수
long limit = result.getLimit();
long offset = result.getOffset();
List&lt;Item&gt; results = result.getResults(); //조회된 데이터</code></pre>
<p><code>listResults()</code>를 사용하면 전체 데이터 조회를 위한 <code>count</code> 쿼리를 한 번 더 실행함. 그리고 <code>SearchResults</code>를 반환하는데 이 객체에서 전체 데이터 수를 조회할 수 있음.</p>
<h3 id="6-그룹">(6) 그룹</h3>
<p>그룹은 <code>groupBy</code>를 사용하고, 그룹화된 결과를 제한하기 위해서는 <code>having</code>을 사용하면 됨.</p>
<pre><code class="language-java">query.from(item)
    .groupBy(item.price)
    .having(item.price.get(1000))
    .list(item);</code></pre>
<h3 id="7-조인">(7) 조인</h3>
<p>조인은 <code>innerJoin(join)</code>, <code>leftJoin</code>, <code>rightJoin</code>, <code>fullJoin</code>을 사용할 수 있고 추가로 JPQL의 on과 성능 최적화를 위한 <code>fetch</code> 조인 역시 사용할 수 있음.</p>
<pre><code class="language-java">QOrder order = QOrder.order;
QMember member =QMember.member;
QOrderItem orderItem = QOrderItem.orderItem;

query.from(order)
    .join(order.member, member)
    .leftJoin(order.orderItems, orderItem)
    .list(order);

//조인 join에 사용
query.from(order)
    .leftJoin(order.orderItems, orderItem)
    .on(orderItem.count.gt(2))
    .list(order);
//페치 조인 사용
query.from(order)
    .innerJoin(order.member, member).fetch()
    .leftJoin(order.orderItems, orderItem).fetch()
    .list(order);</code></pre>
<h3 id="8-서브쿼리">(8) 서브쿼리</h3>
<p>서브쿼리는 JPASubQuery를 생성해서 사용함. 서브 쿼리의 결과가 하나이면 <code>unique()</code>, 여러 건이면 <code>list()</code>를 사용.</p>
<pre><code class="language-java">QItem item = QItem.item;
QItem itemSub = new QItem(&quot;itemSub&quot;);

query.from(item)
    .where(item.price.eq(
            new JPAQuery().from(itemSub).unique(itemSub.price.max())
            ))
    .list(item);</code></pre>
<h3 id="9-프로젝션과-결과반환">(9) 프로젝션과 결과반환</h3>
<p><code>select</code>절에 조회 대상을 지정하는 것을 <strong>프로젝션</strong>이라함.</p>
<h4 id="프로젝션-대상이-하나일-때">프로젝션 대상이 하나일 때</h4>
<pre><code class="language-java">QItem item = QItem.item;
List&lt;String&gt; result = query.from(item).list(item.name);

for (String name : result) {
    System.out.println(&quot;name = &quot;+name);
}</code></pre>
<h4 id="여러-컬럼-반환과-튜플">여러 컬럼 반환과 튜플</h4>
<p>프로젝션 대상으로 여러 필드를 선택하면 QueryDSL은 기본으로 Tuple이라는 Map과 비슷한 내부 타입을 사용함. 조회 결과는 <code>tuple.get()</code> 메소드에 조회한 쿼리 타입을 지정하면 됨.</p>
<pre><code class="language-java">QItem item = QItem.item;

List&lt;Tuple&gt; result = query.from(item).list(item.name, item.price);

for (Tuple tuple : result) {
    System.out.println(&quot;name = &quot;+ tuple.get(item.name));
    System.out.println(&quot;price = &quot;+tuple.get(item.price));
}</code></pre>
<h4 id="빈-생성">빈 생성</h4>
<p>쿼리 결과를 엔티티가 아닌 <strong>특정 객체로</strong> 받고 싶으면 <strong>빈 생성(bean population)</strong> 기능을 사용. QueryDSL은 아래와 같은 방법들을 제공함.</p>
<ul>
<li>프로퍼티 접근</li>
<li>필드 접근</li>
<li>생성자 사용</li>
</ul>
<p>원하는 방법의 지정을 위해서는 Projections를 사용하면 됨.</p>
<h3 id="10-수정-삭제-배치-쿼리">(10) 수정, 삭제 배치 쿼리</h3>
<p>QueryDSL도 수정, 삭제 같은 배치 쿼리를 지원함. JPQL 배치 쿼리 같이 영속성 컨텍스트를 무시하고 데이터베이스를 직접 쿼리하는 부분에 유의해야함.</p>
<p>수정 배치 쿼리</p>
<pre><code class="language-java">QItem item = QItem.item;
JPAUpdateClause updateClause = new JPAUpdateClause(em, item);
long count = updateClause.where(item.name.eq(&quot;책&quot;))
    .set(item.price, item.price.add(100))
    .execute();</code></pre>
<p>삭제 배치 쿼리</p>
<pre><code class="language-java">QItem item = QItem.item;
JPADeleteClause deleteClause = new JPADeleteCluase(em, item);
long count = deleteClause.where(item.name.eq(&quot;책&quot;))
    .execute();</code></pre>
<h3 id="11-동적-쿼리">(11) 동적 쿼리</h3>
<p><code>BooleanBuilder</code>를 사용하면 특정 조건에 따라 동적 쿼리를 편하게 생성할 수 있음.</p>
<pre><code class="language-java">SearchParam param = new SearchParam();
param.setName(&quot;개발자&quot;);
param.setPrice(10000);

QItem item = QItem.item;

BooleanBuilder builder = new BooleanBuilder();
if (StringUtils.hasText(param.getName())) {
    builder.and(item.name.contains(param.getName())));
...
}
</code></pre>
<h3 id="12-메소드-위임">(12) 메소드 위임</h3>
<p>메소드 위임 기능 사용 시 쿼리 타입에 검색 조건을 직접 정의할 수 있음</p>
<pre><code class="language-java">public class ItemExpression {
    @QueryDelegate(Item.class)
    public static BooleanExpression isExpensive(QItem item, Integer price) {
        return item.price.gt(price);
    }
}</code></pre>
<p>메소드 위임 기능을 사용하기 위해서는 우선 <strong>정적 메소드</strong>를 만들고 <code>@QueryDelegate</code> 어노테이션에 속성으로 이 기능을 적용할 엔티티를 지정해야함.</p>
<p>정적 메소드의 첫 번째 파라미터에는 대상 엔티티의 쿼리 타입을 지정하고, 나머지는 필요한 파라미터를 정의함.</p>
<h2 id="105-네이티브-sql">10.5 네이티브 SQL</h2>
<p>JPQL은 표준 SQL이 지원하는 대부분의 문법과 SQL 함수들을 지원하지만 특정 데이터베이스 종속적인 기능은 지원하지 않음.</p>
<ul>
<li>특정 데이터베이스만 지원하는 함수, 문법, SQL 쿼리 힌트</li>
<li>인라인 뷰, UNION, INTERSECT</li>
<li>스토어드 프로시저</li>
</ul>
<p>때로는 특정 데이터베이스에 종속적인 기능이 필요하기에 JPA는 특정 데이터베이스 종속적인 기능ㅇ르 사용할 수 있는 다양한 방법을 열어둠.</p>
<h3 id="1-네이티브-sql-사용">(1) 네이티브 SQL 사용</h3>
<p>네이티브 쿼리의 API는 다음 3가지가 존재</p>
<pre><code class="language-java">//결과 타입 정의
public Query createNativeQuery (String sqlString, Class resultClass);

//결과 타입을 정의할 수 없을 때
public Query createNativeQuery(String sqlString);

//결과 매핑 사용
public Query createNativeQuery(String sqlString, String resultSetMapping); </code></pre>
<h4 id="엔티티-조회">엔티티 조회</h4>
<p>네이티브 SQL은 <code>em.createNativeQuery(SQL, 결과 클래스)</code>를 사용함. 첫 번째 파라미터에 네이티브 SQL을 입력하고, 두 번째는 조회할 엔티티 클래스의 타입을 입력함.</p>
<p><strong>네이티브 SQL로 SQL만 직접 사용할 뿐, 나머지는 JPQL을 사용할 때와 같음. 조회한 엔티티 역시 영속성 컨텍스트에서 관리</strong>됨.</p>
<h4 id="값-조회">값 조회</h4>
<pre><code class="language-java">String sql = &quot;SELECT ID, AGE, NAME, TEAM_ID &quot;+
            &quot;FROM MEMBER WHERE AGE &gt; ?&quot;;
Query nativeQuery = em.createNativeQuery(sql).setParameter(1,10);

List&lt;Object[]&gt; resultList = nativeQuery.getResultList();
for (Object[] row : resultList) {
    ...
}</code></pre>
<p>위에서는 엔티티로 조회하지 않고 단순히 값으로 조회함. 이렇게 여러 값으로 조회하기 위해서는 <code>em.createNativeQuery(SQL)</code>의 두 번째 파라미터를 사용하지 않으면 됨. JPA는 조회한 값들을 Object[]에 담아서 반환함. 여기서는 스칼라 값들을 조회했을 뿐이기에 영속성 컨텍스트가 관리하지 않음. JDBC로 데이터를 조회한 것과 같음.</p>
<h4 id="결과-매핑-사용">결과 매핑 사용</h4>
<p>엔티티와 스칼라 값을 함께 조회하는 것처럼 매핑이 복잡해지면 <code>@SqlResultSetMapping</code>을 정의해서 결과 매핑을 사용해야함.</p>
<pre><code class="language-java">String sql = &quot;SELECT M.ID , AGE, NAME, TEAM_ID, I.ORDER_COUNT &quot;+
            &quot;FROM MEMBER M&quot; +
            &quot;LEFT JOIN &quot; +
            &quot;         (SELECT IM.ID, COUNT(*) AS ORDER_COUNT &quot;+
            &quot;         FROM ORDERS O, MEMBER IM &quot;+
            &quot;       WHERE O.MEMBER_ID = IM.ID) I &quot; +
            &quot;ON M.ID = I.ID&quot;;

Query nativeQuery = em.createNativeQuery(sql, &quot;memberWithOrderCount&quot;);</code></pre>
<p><code>em.createNativeQuery(sql, &quot;memberWithOrderCount&quot;);</code>의 두 번째 파라미터에 결과 매핑 정보의 이름이 사용됨.</p>
<p>결과 매핑 정의</p>
<pre><code class="language-java">@Entity
@SqlResultSetMapping(name = &quot;memberWithOrderCount&quot;,
    entities = {@EntityResult (entityClass = Member.class)},
    columns = {@ColumnResult (name = &quot;ORDER_COUNT&quot;)})
public class Member {...}</code></pre>
<p><code>memberWithOrderCount</code>의 결과 매핑을 보면 회원 엔티티와 ORDER_COUNT 컬럼을 매핑함. <code>entities</code>, <code>columns</code>라는 이름에서 알 수 있듯, 여러 엔티티와 여러 컬럼을 매핑할 수 있음.</p>
<p><code>@FieldResult</code>를 사용해 컬럼명과 필드명을 직접 매핑하며, 해당 설정은 엔티티 필드에 정의한 <code>@Column</code>보다 앞섬. 불편한 점은 해당 어노테이션을 한 번이라도 사용하면, 전체 필드를 해당 어노테이션을 사용해 매핑해야한다.</p>
<p>두 엔티티 조회의 컬럼명이 중복될 때에도 역시 <code>@FieldResult</code>를 사용해야함.</p>
<h3 id="2-named-네이티브-sql">(2) Named 네이티브 SQL</h3>
<p>JPQL처럼 네이티브 SQL도 Named 네이티브 SQL을 사용해 정적 SQL을 작성할 수 있음.</p>
<pre><code class="language-java">@Entity
@NamedNativeQuery (
    name = &quot;Member.memberSQL&quot;,
    query = &quot;SELECT ID, AGE, NAME, TEAM_ID&quot; +
            &quot;FROM MEMBER WHERE AGE &gt; ?&quot;,
            resultClass = Member.class
)
public class Member {...}
)</code></pre>
<p><code>@NamedNativeQuery</code>로 Named 네이티브 SQL을 등록.</p>
<pre><code class="language-java">TypedQuery&lt;Member&gt; nativeQuery = 
    em.createNamedQuery(&quot;Member.memberSQL&quot;, Member.class)
    .setParameter(1, 20L);</code></pre>
<h4 id="namednativequery"><code>@NamedNativeQuery</code></h4>
<ul>
<li>name: 네임드 쿼리 이름 (필수)</li>
<li>query: SQL 쿼리(필수)</li>
<li>hints: 벤더 종속적인 힌트</li>
<li>resultClass: 결과 클래스</li>
<li>resultSetMapping: 결과 매핑 사용</li>
</ul>
<h3 id="3-네이티브-sql-xml에-정의">(3) 네이티브 SQL XML에 정의</h3>
<p>XML에 정의할 때는 순서를 지켜야하는데 <code>&lt;named-native-query&gt;</code>를 먼저 정의하고 <code>&lt;sql-result-set-mapping&gt;</code>를 정의해야함.</p>
<h3 id="5-스토어드-프로시저">(5) 스토어드 프로시저</h3>
<p>JPA 2.1부터 스토어드 프로시저를 지원함.</p>
<h4 id="스토어드-프로시저-사용">스토어드 프로시저 사용</h4>
<p>: 단순히 입력값을 두 배로 증가시켜주는 <code>proc_multiply</code>라는 스토어드 프로시저가 있을 때, 해당 프로시저는 첫 번째 파라미터로 값을 입력받고, 두 번째 파라미터로 결과를 반환함.</p>
<p>JPA로 해당 프로시저를 호출하기 위해서는 아래와 같다.</p>
<pre><code class="language-java">StoredProcedureQuery spq = em.createStoredProcedureQuery(&quot;proc_multiply&quot;);

spq.registerStoredProcedureParameter(1, Integer.class, ParameterMode.IN);
spq.registerStoredProcedureParameter(2, Integer.class, ParameterMode.OUT);

spq.setParameter(1, 100);
spq.execute();

Integer out = (Integer)spq.getOutputParameterValue(2);</code></pre>
<p>스토어드 프로시저를 사용하기 위해서는 <code>em.createStoredProcedureQuery()</code> 메소드에 사용할 스토어드 프로시저 이름을 입력하면 됨. 그리고 <code>registerStoredProcedureParameter()</code> 메소드를 사용해 프로시저에서 사용할 파라미터를 순서, 타입, 파라미터 모드 순으로 정의함.</p>
<p>사용 가능한 파라미터 모드는 아래와 같음.</p>
<pre><code class="language-java">public enum ParameterMode {
    IN, //INPUT 파라미터
    INOUT, //INPUT, OUTPUT 파라미터
    OUT, //OUTPUT 파라미터
    REF_CURSOR //CURSOR 파라미터
}</code></pre>
<p>파라미터 순서 대신 이름을 사용하는 방법 역시 존재함.</p>
<h4 id="named-스토어드-프로시저-사용">Named 스토어드 프로시저 사용</h4>
<p>스토어드 프로시저 쿼리에 이름을 부여해 사용하는것을 Named 스토어드 프로시저라함.</p>
<pre><code class="language-java">@NamedStoredProcedureQuery (
    name = &quot;multiply&quot;,
    procedureName = &quot;proc_multiply&quot;,
    parameters = {
        @StoredProcedureParameter(name = &quot;inParam&quot;, mode = ParameterMode.IN, type = Integer.class),
        @StoredProcedureParameter(naem = &quot;outParam&quot;, mode = ParameterMode.OUT, type = Integer.class)
    }
)
@Entity
public class Member {...}</code></pre>
<p>XML에 정의한 Named 스토어드 프로시저는 <code>em.createNamedStoredProcedureQuery()</code> 메소드에 등록한 Named 스토어드 프로시저 이름을 파라미터로 사용해 찾아올 수 있음.</p>
<hr>
<h2 id="106-객체지향-쿼리-심화">10.6 객체지향 쿼리 심화</h2>
<h3 id="1-벌크-연산">(1) 벌크 연산</h3>
<p>엔티티 수정을 위해서는 영속성 컨텍스트의 변경 감지 기능이나 병합을 사용하며, 삭제를 위해서는 <code>EntityManager.remove()</code> 메소드를 사용함. 다만, 이 방법으로 수 백개 이상의 엔티티를 하나씩 처리하기에는 시간이 너무 오래 걸림.</p>
<p>이럴 때에 <strong>여러 건을 한 번에 수정하거나 삭제하는 벌크 연산</strong>을 사용하면 됨.</p>
<pre><code class="language-java">String qlString = 
    &quot;update Product p &quot;+
    &quot;set p.price = p.price * 1.1 &quot;+
    &quot;where p.stockAmount &lt; :stockAmount&quot;;

int resultCount = em.createQuery(qlString)
                    .setParameter(&quot;stockAmount&quot;, 10)
                    .executeUpdate();</code></pre>
<p>벌크연산은 <code>executeUpdate()</code> 메소드를 사용함. 해당 메소드는 벌크 연산으로 영향을 받은 엔티티 건수를 반환한다. 삭제 연산 역시 같은 메소드를 사용한다.</p>
<h4 id="벌크-연산의-주의점">벌크 연산의 주의점</h4>
<p>벌크 연산 사용 시, 해당 연산이 <strong>영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다는 점에 주의해야함</strong>.</p>
<p>이런 문제를 해결하기 위한 방법으로는 아래의 방법들이 존재한다.</p>
<ul>
<li><p><code>em.refresh()</code> 사용:
  벌크 연산을 수행한 직후 정확한 엔티티를 사용하기 위해서는 <code>em.refresh()</code>를 사용해 데이터베이스에서 해당 엔티티를 다시 조회한다.</p>
</li>
<li><p>벌크 연산 먼저 실행
  가장 실용적인 해결책은 벌크 연산을 <strong>가장 먼저 실행</strong>하는 것. 이 경우에는 벌크 연산 실행 후, 엔티티를 조회하기에 이미 벌크 연산으로 변경된 엔티티를 조회하게 되기에 괜찮다.</p>
</li>
<li><p>벌크 연산 수행 후 영속성 컨텍스트 초기화
  <strong>영속성 컨텍스트 초기화</strong>를 통해 남아있는 엔티티를 제거하는 것 역시 좋은 방법이다. 초기화 이후에는 데이터베이스에서 엔티티를 조회해 온다.</p>
</li>
</ul>
<h3 id="2-영속성-컨텍스트와-jpql">(2) 영속성 컨텍스트와 JPQL</h3>
<h4 id="쿼리-후-영속-상태인-것과-아닌-것">쿼리 후 영속 상태인 것과 아닌 것</h4>
<p>JPQL의 조회 대상으로는 엔티티, 임베디드 타입, 값 타입 등 다양한 종류가 있다. JPQL로 엔티티 조회 시, 영속성 컨텍스트에서 관리되지만, 만약 <strong>엔티티가 아니라면 영속성 컨텍스트에서 관리되지 않는다</strong>.</p>
<p>예로는 임베디드 타입의 경우 조회해 값을 변경해도 영속성 컨텍스트가 관리하지 않기에 변경 감지에 의한 수정이 발생하지 않는다. 만약 엔티티를 조회하게 되면, 엔티티가 가지고 있는 임베디드 타입은 함께 수정되기는 한다.</p>
<h4 id="jpql로-조회한-엔티티와-영속성-컨텍스트">JPQL로 조회한 엔티티와 영속성 컨텍스트</h4>
<ul>
<li>JPQL로 조회한 엔티티는 영속 상태</li>
<li>영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 기존 엔티티를 반환함.</li>
</ul>
<h4 id="find-vs-jpql">find() vs JPQL</h4>
<p><code>em.find()</code> 메소드는 엔티티를 영속성 컨텍스트에서 먼저 찾고 없으면 데이터베이스에서 찾음. 따라서 해당 엔티티가 영속성 컨텍스트에 있으면 메모리에서 바로 찾기에 성능상 이점이 존재한다.</p>
<p>반면 JPQL은 <strong>항상 데이터베이스에 SQL을 실행해 결과를 조회한다.</strong></p>
<blockquote>
<p>JPQL의 특징</p>
</blockquote>
<ul>
<li>JPQL은 항상 데이터베이스를 조회</li>
<li>JPQL로 조회한 엔티티는 영속 상태</li>
<li>영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 기존 엔티티를 반환함.</li>
</ul>
<h3 id="3-jpql과-플러시-모드">(3) JPQL과 플러시 모드</h3>
<p>플러시: 영속성 컨텍스트의 변경 내역을 데이터베이스에 동기화하는 것. JPA는 플러시가 일어날 때 영속성 컨텍스트에 등록, 수정, 삭제한 엔티티를 찾아 INSERT, UPDATE, DELETE SQL을 만들어 데이터베이스에 반영함.</p>
<p>플러시 호출을 위해서는 <code>em.flush()</code> 메소드를 직접 사용해도 되지만 보통 플러시 모드에 따라 커밋하기 직전이나 쿼리 실행 직전에 자동으로 플러시를 호출됨.</p>
<h4 id="쿼리와-플러시-모드">쿼리와 플러시 모드</h4>
<p>JPQL은 영속성 컨텍스트에 있는 데이터를 고려하지 않고 데이터베이스에서 데이터를 조회함. 따라서 JPQL을 실행하기 전에 영속성 컨텍스트의 내용을 데이터베이스에 반영해야함. 그렇지 않을 경우 의도치 않은 결과 발생 가능.</p>
<p><code>setFlushMode()</code>를 통해 플러시 모드를 설정할 수 있음.</p>
<h4 id="플러시-모드-최적화">플러시 모드 최적화</h4>
<p><code>FlushModeType.COMMIT</code> 모드는 트랜잭션을 커밋할 때만 플러시하고 쿼리를 실행할 때는 플러시하지 않음. 따라서 JPA 쿼리를 사용할 때 영속성 컨텍스트에는 있지만, 아직 데이터베이스에 반영하지 않은 데이터를 조회할 수 없다. 이런 상황은 잘못하면 데이터 무결성에 심각한 피해를 줄 수 있음.</p>
<p>JPA를 사용하지 않고 JDBC를 직접 사용해 SQL을 실행할 때에도 플러시 모드를 고민해야함. 즉, 쿼리 실행 직전에 <code>em.flush()</code>를 통해 영속성 컨텍스트의 내용을 데이터베이스에 동기화하는 것이 안전함.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 팀프로젝트 3주차 작업로그]]></title>
            <link>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-3%EC%A3%BC%EC%B0%A8-%EC%9E%91%EC%97%85%EB%A1%9C%EA%B7%B8</link>
            <guid>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-3%EC%A3%BC%EC%B0%A8-%EC%9E%91%EC%97%85%EB%A1%9C%EA%B7%B8</guid>
            <pubDate>Tue, 26 May 2026 10:01:03 GMT</pubDate>
            <description><![CDATA[<h1 id="3주차-작업로그">3주차 작업로그</h1>
<h1 id="1-주요-기능-sql-쿼리-작성하기">1. 주요 기능 SQL 쿼리 작성하기</h1>
<aside>

<h4 id="동작-시나리오-상품">동작 시나리오: 상품</h4>
<ul>
<li>회원은 상품을 전체 조회할 수 있다. ✔️</li>
<li>회원은 키워드 검색을 통해 상품을 전체 조회할 수 있다. ✔️<ul>
<li>카테고리를 설정하여 상품을 조회할 수 있다. ✔️</li>
<li>가격순(오름/내림차순)을 설정하여 상품을 조회할 수 있다. ✔️</li>
</ul>
</li>
<li>회원은 상품 내에서 옵션별 선택지를 조회하고 선택할 수 있다. ✔️</aside>

</li>
</ul>
<h4 id="상품-목록-전체-조회">상품 목록 전체 조회</h4>
<pre><code class="language-sql">SELECT *
FROM product
ORDER BY created_at DESC;</code></pre>
<h4 id="상품-키워드-조회">상품 키워드 조회</h4>
<pre><code class="language-sql">SELECT *
FROM product 
WHERE product_name LIKE CONCAT(&#39;%&#39;, **{검색어}**, &#39;%&#39;)
ORDER BY product_name;</code></pre>
<h4 id="상품-카테고리별-조회">상품 카테고리별 조회</h4>
<pre><code class="language-sql">SELECT *
FROM product
WHERE category_id = **{카테고리 ID}**
ORDER BY product_name;</code></pre>
<h4 id="상품-가격순별오름차순내림차순-조회">상품 가격순별(오름차순/내림차순) 조회</h4>
<pre><code class="language-sql">SELECT *
FROM product
ORDER BY price ASC;</code></pre>
<pre><code class="language-sql">SELECT *
FROM product
ORDER BY price DESC;</code></pre>
<h4 id="개별-상품별--상세보기-존재하는-옵션-확인하기">개별 상품별  상세보기 (존재하는 옵션 확인하기)</h4>
<pre><code class="language-sql">SELECT pd.product_detail_id, od.option_type_id, od.option_detail_id
FROM product AS p JOIN 
         product_detail pd ON p.product_id = pd.product_id JOIN 
         product_option op ON pd.product_detail_id = op.product_detail_id JOIN 
         option_detail od ON op.option_detail_id = od.option_detail_id
WHERE p.product_id = **{클릭한 상품ID}**;</code></pre>
<h4 id="개별-상품별-옵션-선택하기">개별 상품별 옵션 선택하기</h4>
<pre><code class="language-sql">SELECT pd.product_detail_id
FROM product_detail pd 
JOIN product_option op ON pd.product_detail_id = op.product_detail_id
WHERE pd.product_id = **{상품ID}**
          AND op.option_detail_id IN (**{선택한 옵션상세ID(1)}**, **{선택한 옵션상세ID(2)}**)
GROUP BY pd.product_detail_id
HAVING COUNT(DISTINCT op.option_detail_id) = **{선택한 옵션 개수}**;</code></pre>
<aside>

<h4 id="동작-시나리오-장바구니">동작 시나리오: 장바구니</h4>
<ul>
<li>회원은 물건(옵션 상세까지 지정 후) 장바구니에 물건을 담을 수 있다.<ul>
<li>옵션 지정하지 않을 수 못 담도록 설정해야함</li>
</ul>
</li>
<li>회원은 장바구니의 물건의 수량을 증가 및 감소시킬 수 있다.</li>
<li>회원은 장바구니의 물건을 삭제할 수 있다.</aside>

</li>
</ul>
<h4 id="장바구니에-물건-담기">장바구니에 물건 담기</h4>
<pre><code class="language-sql">INSERT INTO cart_item(&quot;member_id&quot;, &quot;product_detail_id&quot;, &quot;quantity&quot;)
VALUES (?, ?, ?);</code></pre>
<h4 id="장바구니-물건-수량-증가감소">장바구니 물건 수량 증가/감소</h4>
<pre><code class="language-sql">UPDATE cart_item
SET quantity = quantity + 1
WHERE member_id = ? AND product_detail_id = ?;</code></pre>
<pre><code class="language-sql">UPDATE cart_item
SET quantity = quantity - 1
WHERE member_id = ? AND product_detail_id = ?;</code></pre>
<h4 id="장바구니-물건-삭제">장바구니 물건 삭제</h4>
<pre><code class="language-sql">DELETE FROM cart_item
WHERE member_id = ? AND product_detail_id = ?;</code></pre>
<hr>
<h1 id="2-통계-관련-sql-쿼리-작성하기">2. 통계 관련 SQL 쿼리 작성하기</h1>
<h4 id="상품별-판매량-조회">상품별 판매량 조회</h4>
<pre><code class="language-sql">SELECT p.product_id, SUM(pd.sales) AS total_sales
FROM product p JOIN
     product_detail pd ON p.product_id = pd.product_id
GROUP BY p.product_id;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/835b4c1e-a5b9-4059-933f-4b366d1bb6ee/image.png" alt=""></p>
<h4 id="상품상세별-판매량-조회">상품상세별 판매량 조회</h4>
<pre><code class="language-sql">SELECT pd.product_detail_id,
        MAX(CASE WHEN od.option_type_id=1 THEN od.option_value END) AS size_option,
        MAX(CASE WHEN od.option_type_id=2 THEN od.option_value END) AS color_option,
        SUM(pd.sales) AS total_sales
FROM product_detail pd JOIN
     product_option op ON pd.product_detail_id = op.product_detail_id JOIN
     option_detail od ON op.option_detail_id = od.option_detail_id
GROUP BY pd.product_detail_id;</code></pre>
<ul>
<li><p>작성시 AI 도움 받음</p>
<p>  질문: product_detail_id마다의 옵션이 행으로 분리되어 여러 개의 옵션이 붙으면 2-3행이 나오는 것을 어떻게 처리할지?</p>
<ul>
<li>행으로 분리되어있는것을 → 열로 피벗 이동</li>
<li>CASE WHEN을 사용해서 옵션 타입별로 우선 새로운 새로 열로 만듦.<ul>
<li>이때 앞에 MAX()를 사용해서 GROUP BY를 사용할 때, 널과 값이 존재하는 경우에는 존재하는 값을 선택하도록 설정.</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/b2fe6aa9-621d-4c45-b4da-61d084d0a0c8/image.png" alt=""></p>
<ul>
<li>product 테이블까지 조인해주면 어떤 상품의 어떤 옵션들이 얼마나 팔렸는지 조회 가능</li>
</ul>
<pre><code class="language-sql">SELECT p.product_id, pd.product_detail_id,
            MAX(CASE WHEN od.option_type_id=1 THEN od.option_value END) AS size_option,
            MAX(CASE WHEN od.option_type_id=2 THEN od.option_value END) AS color_option,
      SUM(pd.sales) AS total_sales
FROM product p JOIN
         product_detail pd ON p.product_id = pd.product_id JOIN
         product_option op ON pd.product_detail_id = op.product_detail_id JOIN
     option_detail od ON op.option_detail_id = od.option_detail_id
GROUP BY pd.product_detail_id;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/642b8df3-a781-4e08-9f9b-ddb7f9bed64e/image.png" alt=""></p>
<h4 id="월별-매출-조회">월별 매출 조회</h4>
<pre><code class="language-sql">SELECT MONTH(order_date) AS month, SUM(total_price) AS total_income
FROM orders
WHERE YEAR(order_date) = 2026
GROUP BY MONTH(order_date)
ORDER BY month;</code></pre>
<h4 id="전월-대비-매출">전월 대비 매출</h4>
<pre><code class="language-sql">WITH month_income AS (
    SELECT MONTH(order_date) AS month, SUM(total_price) AS total_income
    FROM orders
    WHERE YEAR(order_date) = 2026
    GROUP BY MONTH(order_date)
    ORDER BY month
)

SELECT month, total_income AS &quot;월 매출&quot;,
       LAG(total_income, 1, 0) OVER (ORDER BY month) AS &quot;전월매출&quot;,
       total_income - LAG(total_income, 1, 0) OVER (ORDER BY month) AS &quot;전월 대비 증감&quot;
FROM month_income;</code></pre>
<ul>
<li>CTE를 통해 우선 위에서 사용한 월별 매출을 임시 테이블처럼 사용할 수 있도록 구성</li>
<li>윈도우 함수 LAG를 통해 직전 행을 가져올 수 있도록 구성</li>
</ul>
<hr>
<h1 id="3-더미-데이터-넣어서-테스트하기">3. 더미 데이터 넣어서 테스트하기</h1>
<ul>
<li><input checked="" disabled="" type="checkbox"> 더미 데이터 생성</li>
<li><input checked="" disabled="" type="checkbox"> INSERT (삽입)</li>
<li><input checked="" disabled="" type="checkbox"> SELECT (조회)</li>
<li><input checked="" disabled="" type="checkbox"> UPDATE (수정)</li>
<li><input checked="" disabled="" type="checkbox"> DELETE (삭제)</li>
</ul>
<hr>
<h2 id="0-더미-데이터-datasql">0. 더미 데이터 (data.sql)</h2>
<pre><code class="language-sql">-- [제조업체]
INSERT INTO manufacturer (manufacturer_id, company_name, owner)
VALUES (1, &#39;ZARA&#39;, &#39;자라 소유주&#39;);
INSERT INTO manufacturer (manufacturer_id, company_name, owner)
VALUES (2, &#39;H&amp;M&#39;, &#39;H&amp;M 소유주&#39;);
INSERT INTO manufacturer (manufacturer_id, company_name, owner)
VALUES (3, &#39;UNIQLO&#39;, &#39;UNIQLO 소유주&#39;);
INSERT INTO manufacturer (manufacturer_id, company_name, owner)
VALUES (4, &#39;Nike&#39;, &#39;Nike 소유주&#39;);
INSERT INTO manufacturer (manufacturer_id, company_name, owner)
VALUES (5, &#39;Adidas&#39;, &#39;Adidas 소유주&#39;);

-- [카테고리]
INSERT INTO category (category_id, category_name)
VALUES (1, &#39;자켓&#39;);
INSERT INTO category (category_id, category_name)
VALUES (2, &#39;티셔츠&#39;);
INSERT INTO category (category_id, category_name)
VALUES (3, &#39;바지&#39;);
INSERT INTO category (category_id, category_name)
VALUES (4, &#39;스커트&#39;);
INSERT INTO category (category_id, category_name)
VALUES (5, &#39;아우터&#39;);
INSERT INTO category (category_id, category_name)
VALUES (6, &#39;니트&#39;);

-- [상품]
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (1, 1, &#39;트위드 자켓&#39;, 150000, 1, &#39;http://img/zara-tweed-jacket&#39;);
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (2, 1, &#39;플리츠 미디 스커트&#39;, 89000, 4, &#39;http://img/zara-pleats-skirt&#39;);
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (3, 1, &#39;리넨 블라우스&#39;, 65000, 2, &#39;http://img/zara-linen-blouse&#39;);
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (4, 2, &#39;오버사이즈 티셔츠&#39;, 29900, 2, &#39;http://img/hm-oversized-tee&#39;);
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (5, 2, &#39;슬림핏 청바지&#39;, 49900, 3, &#39;http://img/hm-slim-jeans&#39;);
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (6, 3, &#39;히트텍 롱슬리브&#39;, 19900, 2, &#39;http://img/uniqlo-heattech&#39;);
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (7, 3, &#39;후리스 집업&#39;, 59900, 5, &#39;http://img/uniqlo-fleece-zip&#39;);
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (8, 4, &#39;드라이핏 반팔&#39;, 45000, 2, &#39;http://img/nike-dri-fit&#39;);
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (9, 5, &#39;트랙 재킷&#39;, 89000, 1, &#39;http://img/adidas-track-jacket&#39;);
INSERT INTO product (product_id, manufacturer_id, product_name, price, category_id, image_url)
VALUES (10, 5, &#39;조거 팬츠&#39;, 69000, 3, &#39;http://img/adidas-jogger-pants&#39;);
</code></pre>
<pre><code class="language-sql">-- [상품상세]
-- 트위드 자켓 (product_id=1)
INSERT INTO product_detail (product_detail_id, product_id, stock_quantity, surcharge, sales, image_url)
VALUES (1, 1, 30, 0, 10, &#39;http://img/zara-tweed-jacket-white-s&#39;);
INSERT INTO product_detail (product_detail_id, product_id, stock_quantity, surcharge, sales, image_url)
VALUES (2, 1, 25, 0, 20, &#39;http://img/zara-tweed-jacket-white-m&#39;);
INSERT INTO product_detail (product_detail_id, product_id, stock_quantity, surcharge, sales, image_url)
VALUES (3, 1, 20, 0, 15, &#39;http://img/zara-tweed-jacket-black-m&#39;);
INSERT INTO product_detail (product_detail_id, product_id, stock_quantity, surcharge, sales, image_url)
VALUES (4, 1, 15, 0, 5, &#39;http://img/zara-tweed-jacket-black-l&#39;);
-- 플리츠 미디 스커트 (product_id=2)
INSERT INTO product_detail (product_detail_id, product_id, stock_quantity, surcharge, sales, image_url)
VALUES (5, 2, 40, 0, 30, &#39;http://img/zara-pleats-skirt-beige-s&#39;);
INSERT INTO product_detail (product_detail_id, product_id, stock_quantity, surcharge, sales, image_url)
VALUES (6, 2, 35, 0, 25, &#39;http://img/zara-pleats-skirt-beige-m&#39;);
INSERT INTO product_detail (product_detail_id, product_id, stock_quantity, surcharge, sales, image_url)
VALUES (7, 2, 30, 0, 20, &#39;http://img/zara-pleats-skirt-black-m&#39;);
INSERT INTO product_detail (product_detail_id, product_id, stock_quantity, surcharge, sales, image_url)
VALUES (8, 2, 20, 0, 10, &#39;http://img/zara-pleats-skirt-navy-l&#39;);
-- 등...</code></pre>
<h2 id="1-조회-select">1. 조회 (SELECT)</h2>
<h3 id="1-상품별-옵션-조회하기">(1) 상품별 옵션 조회하기</h3>
<p>각 상품별로 어떤 옵션들이 있는지 조회</p>
<ul>
<li>상품상세(product_detai)과 옵션 상세(option_detail)을 둘을 연결하는 테이블인 상품옵션 조합(product_option) 테이블과 함께 조인해서 연관 있는 상품상세-옵션상세가 연결되도록 우선 설정.</li>
<li>그 후에 WHERE에 조건을 설정하여 찾으려는 상품의 id를 조회</li>
<li>해당 상품(id)와 연관된 옵션들의 목록을 볼 수 있음.</li>
<li>재고가 남아있는 상품(상세)들만 조회되도록 WHERE 절에 조건 추가.</li>
</ul>
<pre><code class="language-sql">SELECT *
FROM product_detail as pd JOIN
         product_option as pop ON pd.product_detail_id = pop.product_detail_id JOIN
     option_detail as od ON pop.option_detail_id = od.option_detail_id
WHERE pd.product_id = **{선택한 상품ID}**
            AND **pd.stock &gt; 0**;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/c8ce71ab-97a6-4d05-a927-cc12e5f4bd60/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/b2f5b5e9-dbc4-420c-8eb0-725ec279fd64/image.png" alt=""></p>
<h3 id="2-상품-옵션-타입별사이즈색상-조회하기">(2) 상품 옵션 타입별(사이즈/색상) 조회하기</h3>
<p>상품 안에서 색상/사이즈 내역을 조회해서 출력하기 위한 쿼리문</p>
<pre><code class="language-sql">SELECT *
FROM product_detail as pd JOIN
         product_option as pop ON pd.product_detail_id = pop.product_detail_id JOIN
     option_detail as od ON pop.option_detail_id = od.option_detail_id
WHERE pd.product_id = **{선택한 상품ID}** 
            AND option_type_id = **{선택한 옵션종류ID}**;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/4dd3072d-e6cd-4219-90da-624d8c669d9f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/a6d2ba05-32b3-49d2-821e-fb789ce44cf3/image.png" alt=""></p>
<h3 id="3-카테고리별-상품-조회">(3) 카테고리별 상품 조회</h3>
<pre><code class="language-sql">SELECT *
FROM product as p JOIN
     category as c ON p.category_id = c.category_id
WHERE c.category_name = &#39;자켓&#39;;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/8eea0d06-56f5-4d75-b5d7-9ac51cdf2c1a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/6bef26b8-fe46-4ce4-9538-280d7554678a/image.png" alt=""></p>
<h2 id="3-업데이트-update">3. 업데이트 (UPDATE)</h2>
<h3 id="1-상품-변경">(1) 상품 변경</h3>
<h4 id="상품가격-업데이트">상품가격 업데이트</h4>
<pre><code class="language-sql">UPDATE product
SET price = **{새로운 가격}**
WHERE product_id = **{변경하려는 상품}**;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/95c6cd6e-59d8-470e-9986-d8a720a83b67/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/09b2e335-be0b-49f2-852c-0b4c20429090/image.png" alt=""></p>
<h4 id="2-상품상세-판매수량-업데이트">(2) 상품상세 판매수량 업데이트</h4>
<pre><code class="language-sql">UPDATE product_detail
SET sales = {업데이트되는 판매량}
WHERE product_detail_id = {업데이트되는 상품상세ID};</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/cb3bb36b-8420-4cb8-b4b5-db10de45e2ea/image.png" alt=""></p>
<h4 id="3-상품-옵션-변경">(3) 상품 옵션 변경</h4>
<pre><code class="language-sql">UPDATE product
SET category_id = **{새로운 옵션ID}**
WHERE product_id = **{바꾸려는 상품ID}**;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/3007d228-553f-4235-bd97-bcf5b20948d3/image.png" alt="">
<img src="https://velog.velcdn.com/images/summeryoung_/post/85d558c3-344a-48fb-896f-13e7fcf5af3a/image.png" alt=""></p>
<ul>
<li>존재하지 않는 카테고리의 아이디로 설정해 업데이트하려고 하면 불가능. 에러를 일으킴.</li>
</ul>
<h4 id="4-상품-옵션-테이블에서의-행-삭제">(4) 상품-옵션 테이블에서의 행 삭제</h4>
<p>상품-옵션 테이블의 행을 삭제하는 것은 “해당 상품의 옵션 종류 중 하나를 삭제하는 것을 의미”</p>
<pre><code class="language-sql">DELETE FROM product_option
WHERE product_detail_id = **{상품 상세ID}** 
            AND option_detail_id = **{삭제하려는 옵션상세ID}**;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/08dee03d-85f0-48a3-ae1f-b156baaabee8/image.png" alt=""></p>
<h2 id="4-삭제-delete">4. 삭제 (DELETE)</h2>
<h4 id="1-상품의-삭제--상품-삭제의-삭제">(1) 상품의 삭제 &amp; 상품 삭제의 삭제</h4>
<p>상품을 삭제했을 때, 상품을 외래키로 갖는 상품 상세 등이 어떻게 동작하는지를 확인.</p>
<ul>
<li>product_detail 테이블의 외래키에 ON DELTE CASCADE를 설정해놨기 때문에 상품이 삭제되면 관련된 옵션 등의 데이터를 가지고 있던 상품 상세 데이터들도 삭제</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/32fd3628-6248-4769-9499-6e6c704e9302/image.png" alt=""></p>
<ul>
<li>뿐만 아니라 이렇게 product_detail의 데이터가 삭제될 경우 해당 데이터를 참조하던 상품-옵션 테이블에서 해당 데이터를 참조하던 행들 역시 ON DELETE CASCADE를 적용해 삭제되도록 구성.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/d3c95041-2b95-4ea0-9dae-0b88d3033c42/image.png" alt=""></p>
<h2 id="5-삽입-insert">5. 삽입 (INSERT)</h2>
<h4 id="상품-테이블-삽입">상품 테이블 삽입</h4>
<ul>
<li>정상 삽입
<img src="https://velog.velcdn.com/images/summeryoung_/post/83cdf8aa-99e7-4e7c-8a0d-2d7719949636/image.png" alt=""></li>
</ul>
<ul>
<li>기본키(PK) 중복 불가: 중복된 기본키로 새로운행 삽입 시 에러
<img src="https://velog.velcdn.com/images/summeryoung_/post/4d4af06d-ee70-43bf-8087-9b39940075de/image.png" alt=""></li>
</ul>
<ul>
<li>가격 널값일 때: 삽입 에러
<img src="https://velog.velcdn.com/images/summeryoung_/post/694950ed-ab98-4d33-8cc8-1c451c35898a/image.png" alt=""></li>
</ul>
<h4 id="상품-상세-테이블">상품 상세 테이블</h4>
<ul>
<li>정상 삽입
<img src="https://velog.velcdn.com/images/summeryoung_/post/51888d61-8856-45c3-bcca-23be1ee2ceab/image.png" alt=""></li>
</ul>
<ul>
<li>product_id(외래키) 지정하지 않을 시 삽입 불가 (에러)
<img src="https://velog.velcdn.com/images/summeryoung_/post/3b143540-9b10-4daf-a0bc-b8bba11f26be/image.png" alt=""></li>
</ul>
<ul>
<li>stock_quantity, surcharge, sales 등 0 이상이어야하는 값에 음수 대입시 삽입 불가(에러)
<img src="https://velog.velcdn.com/images/summeryoung_/post/df5d7485-d4a7-424f-9bb2-696408dc670f/image.png" alt="">
<img src="https://velog.velcdn.com/images/summeryoung_/post/cc4a7bd5-7cb6-4fcd-a196-84aa67db6a31/image.png" alt="">
<img src="https://velog.velcdn.com/images/summeryoung_/post/0cde2b51-fa66-4618-a0a5-a56250906805/image.png" alt=""></li>
</ul>
<ul>
<li>이미지 URL 생략하여 삽입: 이미지URL은 NULL값을 허용하므로 생략해서 삽입 가능
<img src="https://velog.velcdn.com/images/summeryoung_/post/5b909408-5b37-423a-b6df-c38d5abab038/image.png" alt=""></li>
</ul>
<h4 id="상품-옵션-테이블">상품-옵션 테이블</h4>
<ul>
<li>정상 삽입
<img src="https://velog.velcdn.com/images/summeryoung_/post/5cd29687-5cf5-4251-8b3f-4721e3c3b415/image.png" alt=""></li>
</ul>
<ul>
<li>옵션 상세ID/상품상세ID 생략: 두 외래키의 널값을 허용하지 않으므로 삽입 불가 (에러)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/9769d12c-f5b3-4fdd-a3aa-f66aa8d6e61d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/e75900ea-ac76-4deb-be57-302ad8e53e75/image.png" alt=""></p>
<ul>
<li>외래키 무결성: 존재하지 않는 product_detail_id 또는 option_detail_id를 삽입하는 것은 불가 (에러)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/acccc852-cbf8-4126-a0ef-3929ca470636/image.png" alt=""></p>
<ul>
<li><input checked="" disabled="" type="checkbox"> 더미 데이터 생성</li>
<li><input checked="" disabled="" type="checkbox"> INSERT (삽입)</li>
<li><input checked="" disabled="" type="checkbox"> SELECT (조회)</li>
<li><input checked="" disabled="" type="checkbox"> UPDATE (수정)</li>
<li><input checked="" disabled="" type="checkbox"> DELETE (삭제)</li>
</ul>
<hr>
<h3 id="0-더미-데이터-datasql-1">0. 더미 데이터 (data.sql)</h3>
<pre><code class="language-sql">-- [옵션 종류]
INSERT INTO option_type (option_type_id, option_type_name)
VALUES (1, &#39;사이즈&#39;);
INSERT INTO option_type (option_type_id, option_type_name)
VALUES (2, &#39;색상&#39;);

-- [옵션 상세]
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (1, 1, &#39;XS&#39;);
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (2, 1, &#39;S&#39;);
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (3, 1, &#39;M&#39;);
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (4, 1, &#39;L&#39;);
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (5, 1, &#39;XL&#39;);
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (6, 2, &#39;화이트&#39;);
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (7, 2, &#39;블랙&#39;);
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (8, 2, &#39;네이비&#39;);
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (9, 2, &#39;베이지&#39;);
INSERT INTO option_detail (option_detail_id, option_type_id, option_value)
VALUES (10, 2, &#39;그레이&#39;);</code></pre>
<h3 id="1-삽입-insert">1. 삽입 (INSERT)</h3>
<h4 id="1-옵션-종류-테이블">(1) 옵션 종류 테이블</h4>
<ul>
<li>정상 삽입
<img src="https://velog.velcdn.com/images/summeryoung_/post/c0f4182b-6723-4938-958f-f89d3ced4e61/image.png" alt=""></li>
</ul>
<ul>
<li>‼️ 문제점: 옵션 종류의 이름(분류) 역시 중복이면 안되는데 UNIQUE 설정이 되어있지 않아 중복으로 들어가는 상황</li>
</ul>
<p>⇒ option_type_name을 UNIQUE가 되도록 (option_type_id, option_type_name)을 복합키로 구성하도록 변경</p>
<pre><code class="language-sql">CREATE TABLE option_type
(
    option_type_id   BIGINT      NOT NULL AUTO_INCREMENT,
    option_type_name VARCHAR(30) NOT NULL,

    PRIMARY KEY (option_type_name, option_type_id)
);</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/5faa585c-3046-4b54-993f-b13d92f57e52/image.png" alt="">
<img src="https://velog.velcdn.com/images/summeryoung_/post/c7d8dcd9-0dfb-4b5a-abf2-afc4a9ca3c8b/image.png" alt=""></p>
<h4 id="2-옵션-상세-테이블">(2) 옵션 상세 테이블</h4>
<ul>
<li>정상삽입</li>
<li>‼️ 문제:  (option_type, option_value)를 UNIQUE로 지정해야, 중복된 옵션 상세 내용이 들어가지 않음</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/e0aef000-d3f3-4f3d-829f-de984c5ec184/image.png" alt="">
<img src="https://velog.velcdn.com/images/summeryoung_/post/489804a8-b707-40cb-bec9-b2a74b152aee/image.png" alt=""></p>
<ul>
<li>‼️ 문제: option_type_id에 널값을 허용하여 종류를 구분하지 않은 채로 옵션 상세 값이 들어감 ⇒ 외래키 널값 허용 금지하기</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/494d9380-a253-47c5-b070-2c636af0c3ea/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/647b37b9-24a1-423d-802d-ab728b56c323/image.png" alt=""></p>
<p>⇒ ❓ 의문: PK(대리키)를 제외하고 두 속성을 합쳐서 UNIQUE인 제약이 있어도 괜찮을지?</p>
<h3 id="2-조회-select">2. 조회 (SELECT)</h3>
<h4 id="1-옵션-종류별-존재하는-옵션-조회">(1) 옵션 종류별 존재하는 옵션 조회</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/f9d02ac5-8068-4b6f-9ab6-89515eb43921/image.png" alt=""></p>
<h3 id="3-수정-update">3. 수정 (UPDATE)</h3>
<h4 id="1-옵션-종류option-type-테이블-업데이트">(1) 옵션 종류(option type) 테이블 업데이트</h4>
<ul>
<li>정상 업데이트</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/93015187-f18f-4ede-9c2e-c72de9f72d6d/image.png" alt=""></p>
<ul>
<li>‼️ 문제: option_type_name을 널값을 금지하긴했지만, 이게 문자열이 공백인거랑은 또 달라서 공백으로도 업데이트가 되는 부분 ⇒ check constraint로 추가하기 (또는 spring에서 @Not Blank 어노테이션 사용하기)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/11e4de3e-3cd0-4346-a9b2-085da8f5d093/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/6064f8d9-624d-4188-aafd-c43af2938eb9/image.png" alt=""></p>
<h4 id="2-옵션-상세option-detail-테이블-업데이트">(2) 옵션 상세(option detail) 테이블 업데이트</h4>
<ul>
<li>정상 업데이트</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/68a698b3-fb9d-42ff-a51a-c1fd8796bc41/image.png" alt=""></p>
<ul>
<li>‼️문제: 옵션 종류와 마찬가지로 공백 문자열 허용하는 경우가 존재 ⇒ CHECK + CONSTRAINT 사용해서 수정</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/a44e0ad1-3ae5-4cad-adc5-b916f4b1d875/image.png" alt=""></p>
<h3 id="4-삭제-delete-1">4. 삭제 (DELETE)</h3>
<h4 id="옵션-종류-테이블">옵션 종류 테이블</h4>
<ul>
<li>옵션 상세에서 참조하는 행을 삭제 ⇒ 삭제 불가 (금지 RESTRICTED)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/00270c05-ddc3-494c-adc4-7761b76c33db/image.png" alt=""></p>
<h4 id="옵션-상세-테이블">옵션 상세 테이블</h4>
<ul>
<li>정상 삭제: 자식 테이블이라 추가적인 제약은 없음.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/938c57d2-0f38-4a2f-a8ac-2ce6e231749e/image.png" alt=""></p>
<hr>
<h1 id="4-트리거프로시저함수">4. 트리거/프로시저/함수</h1>
<h3 id="리뷰-신고-트랜잭션--프로시저">리뷰 신고 (트랜잭션 + 프로시저)</h3>
<ul>
<li>리뷰 신고 시, review_report 테이블에 추가</li>
<li>review 테이블에 신고 횟수를 기록하기 위한 컬럼 추가 (수정사항)</li>
<li>리뷰 신고 테이블에 추가 → 리뷰에 신고 횟수 증가 이 부분을 하나의 트랜잭션으로 생성</li>
<li>트랜잭션 중간에 에러가 생겼을 때, 롤백 시키기 위한 방법을 고민하다 AI한테 물어봤는데 SQLEXCEPTION  발생시 빠져나가 롤백하도록 BEGIN 바로 아래에 구성.</li>
</ul>
<pre><code class="language-sql">DELIMITER $$

CREATE PROCEDURE report_review (
    IN r_user_id BIGINT,
    IN r_review_id BIGINT,
    IN r_reason VARCHAR(255)
) 
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
    END;

    START TRANSACTION;

    INSERT INTO review_report (review_id, reporter_member_id, report_reason, report_status)
    VALUES(r_review_id, r_user_id, r_reason, &#39;WAITING&#39;);

    UPDATE review
    SET report_count = report_count+1
    WHERE review_id = r_review_id;

    COMMIT;
END $$
DELIMITER ;</code></pre>
<h3 id="장바구니-아이템-추가-프로시저">장바구니 아이템 추가 (프로시저)</h3>
<pre><code class="language-sql">DELIMITER $$

CREATE PROCEDURE add_cart (
    IN p_member_id BIGINT,
    IN p_product_id BIGINT,
    IN p_quantity INT
) 
BEGIN
    DECLARE exist INT;

    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
    END;

    START TRANSACTION;

    SELECT COUNT(*) INTO exist
    FROM cart_item
    WHERE member_id = p_member_id AND product_detail_id = p_product_id;

    IF exist &gt; 0 THEN
        UPDATE cart_item
        SET quantity = quantity + p_quantity
        WHERE member_id = p_member_id AND product_detail_id = p_product_id;
    ELSE
        INSERT INTO cart_item (member_id, product_detail_id, quantity)
        VALUES (p_member_id, p_product_id, p_quantity);
    END IF;

    COMMIT;
END $$
DELIMITER ;</code></pre>
<h3 id="장바구니-재고-확인-트리거">장바구니 재고 확인 (트리거)</h3>
<pre><code class="language-sql">DELIMITER $$

CREATE TRIGGER check_stock_insert
BEFORE INSERT ON cart_item
FOR EACH ROW
BEGIN
    DECLARE stock INT;

    SELECT stock_quantity INTO stock
    FROM product_detail
    WHERE product_detail_id = NEW.product_detail_id;

    IF NEW.quantity &gt; stock
    THEN SIGNAL SQLSTATE &#39;45000&#39;
         SET MESSAGE_TEXT = &#39;상품 재고가 부족합니다.&#39;;
    END IF;
END $$

CREATE TRIGGER check_stock_update
BEFORE UPDATE ON cart_item
FOR EACH ROW
BEGIN
    DECLARE stock INT;

    SELECT stock_quantity INTO stock
    FROM product_detail
    WHERE product_detail_id = NEW.product_detail_id;

    IF NEW.quantity &gt; stock
    THEN SIGNAL SQLSTATE &#39;45000&#39;
         SET MESSAGE_TEXT = &#39;상품 재고가 부족합니다.&#39;;
    END IF;
END $$
DELIMITER ;</code></pre>
<h3 id="장바구니-총-결제-금액-계산-함수">장바구니 총 결제 금액 계산 (함수)</h3>
<pre><code class="language-sql">DELIMITER $$

CREATE FUNCTION get_cart_price (
    p_member_id BIGINT
) 
RETURNS INT
DETERMINISTIC
BEGIN
    DECLARE total_price INT;

    SELECT IFNULL(SUM(price*quantity),0) INTO total_price
    FROM cart_item
    WHERE member_id = p_member_id;

    RETURN total_price;

END $$
DELIMITER ;

SELECT get_cart_price(5) AS total_price;</code></pre>
<hr>
<h1 id="5-상품-crud-작성하기">5. 상품 CRUD 작성하기</h1>
<h4 id="코드">코드</h4>
<p><a href="https://github.com/26-1-db-course-project/db-project-e-commerce/tree/main/backend/src/main/java/db/project/ecommerce/product">db-project-e-commerce/backend/src/main/java/db/project/ecommerce/product at main · 26-1-db-course-project/db-project-e-commerce</a></p>
<h2 id="1-폴더-분리">1. 폴더 분리</h2>
<ul>
<li>폴더는 controller, domain(entity), dto(response/request), service, repository로 분리하여 진행하였습니다.
<img src="https://velog.velcdn.com/images/summeryoung_/post/212df928-536e-476b-ac98-736f861810f1/image.png" alt=""></li>
</ul>
<ul>
<li>SQL 쿼리 공유
<img src="https://velog.velcdn.com/images/summeryoung_/post/041a5a84-5ebe-451c-b5f0-deef2db8555a/image.png" alt=""></li>
</ul>
<h2 id="2-controller">2. Controller</h2>
<ul>
<li>상품 생성 (POST)</li>
<li>상품 조회 (GET)<ul>
<li>상품 목록 조회 (일반)</li>
<li>상품 카테고리별 조회</li>
<li>상품 가격순 조회</li>
<li>상품 개별 조회</li>
</ul>
</li>
<li>상품 가격 수정 (PATCH)</li>
<li>상품 삭제 (DELETE)</li>
</ul>
<h4 id="코드-1">코드</h4>
<pre><code class="language-java">@Controller
@RequiredArgsConstructor
@RequestMapping(&quot;/products&quot;)
public class ProductController {
    private final ProductService productService;

    //TODO: 상품 생성
    @PostMapping
    public ResponseEntity&lt;ProductResponse&gt; createProduct(@RequestBody CreateProductRequest request) {
        ProductResponse response = productService.createProduct(request);

        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }

    //TODO: 상품 목록 조회 (가격순 정렬)
    @GetMapping
    public ResponseEntity&lt;ProductListResponse&gt; getProductList(@RequestParam(defaultValue = &quot;productName,desc&quot;) String sortBy) {
        ProductListResponse response = productService.getProductList(sortBy);

        return ResponseEntity.ok(response);
    }

    //TODO: 카테고리별 상품 목록 조회 (가격순 정렬)
    @GetMapping(&quot;/category/{categoryId}&quot;)
    public ResponseEntity&lt;ProductListResponse&gt; getProductListByCategory(@PathVariable(&quot;categoryId&quot;) Long categoryId,
                                                                        @RequestParam(defaultValue = &quot;productName,desc&quot;) String sortBy) {
        ProductListResponse response = productService.getProductListByCategory(categoryId, sortBy);

        return ResponseEntity.ok(response);
    }

    //TODO: 상품 개별 조회
    @GetMapping(&quot;/{productId}&quot;)
    public ResponseEntity&lt;ProductResponse&gt; getProductDetail(@PathVariable(&quot;productId&quot;) Long productId) {
        ProductResponse response = productService.getProductDetail(productId);

        return ResponseEntity.ok(response);
    }

    //TODO: 상품 검색
    @GetMapping(&quot;/search&quot;)
    public ResponseEntity&lt;ProductListResponse&gt; searchProduct(@RequestBody SearchProduct request,
                                                         @RequestParam(defaultValue = &quot;productName,desc&quot;) String sortBy) {
        ProductListResponse response = productService.searchProduct(request, sortBy);

        return ResponseEntity.ok(response);

    }

    //TODO: 상품 가격 업데이트
    @PatchMapping(&quot;/{productId}&quot;)
    public ResponseEntity&lt;Void&gt; updateProductPrice(@PathVariable(&quot;productId&quot;) Long productId,
                                                   @RequestBody UpdateProductPrice request) {
        productService.updateProductPrice(productId, request);

        return ResponseEntity.ok().build();
    }

    //TODO: 상품 삭제
    @DeleteMapping(&quot;/{productId}&quot;)
    public ResponseEntity&lt;Void&gt; deleteProduct(@PathVariable(&quot;productId&quot;) Long productId) {
        productService.deleteProduct(productId);

        return ResponseEntity.ok().build();
    }
}</code></pre>
<h2 id="3-domainentity">3. Domain(Entity)</h2>
<h3 id="category">Category</h3>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;category_id&quot;)
    private Long id;

    @Column(name = &quot;category_name&quot;)
    private String category;

    @Builder
    public Category(String category) {
        this.category = category;
    }
}</code></pre>
<h3 id="manufacturer">Manufacturer</h3>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor
public class Manufacturer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;manufacturer_id&quot;)
    private Long id;

    @Column (name = &quot;company_name&quot;)
    private String company;

    @Column (name = &quot;owner&quot;)
    private String owner;

    @Builder
    public Manufacturer(String company, String owner) {
        this.company = company;
        this.owner = owner;
    }
}
</code></pre>
<h3 id="product">Product</h3>
<pre><code class="language-java">@Entity
@Getter
@NoArgsConstructor
public class Product {
    @Id
    @GeneratedValue (strategy = GenerationType.IDENTITY)
    @Column(name = &quot;product_id&quot;)
    private Long id;

    @ManyToOne (fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;manufacturer_id&quot;)
    private Manufacturer manufacturer;

    @Column(name = &quot;product_name&quot;)
    private String productName;

    @Column(name = &quot;price&quot;)
    private int price;

    @OneToOne
    @JoinColumn(name = &quot;category_id&quot;)
    private Category category;

    @Column (name = &quot;image_url&quot;)
    private String imageUrl;

    @Builder
    public Product(Manufacturer manufacturer, String name, int price, Category category, String imageUrl) {
        this.manufacturer = manufacturer;
        this.productName = name;
        this.category = category;
        this.price = price;
        this.imageUrl = imageUrl;
    }

    public void updatePrice(int price) {
        this.price = price;
    }

}</code></pre>
<h2 id="4-dto">4. Dto</h2>
<h3 id="createproductrequest">CreateProductRequest</h3>
<pre><code class="language-java">@Getter
@AllArgsConstructor
public class CreateProductRequest {
    private Long manufacturerId;
    private String productName;
    private int price;
    private Long categoryId;
    private String imageUrl;
}</code></pre>
<h3 id="searchproduct">SearchProduct</h3>
<pre><code class="language-java">@Getter
@AllArgsConstructor
public class SearchProduct {
    private String keyword;
}</code></pre>
<h3 id="updateproductprice">UpdateProductPrice</h3>
<pre><code class="language-java">@Getter
@AllArgsConstructor
public class UpdateProductPrice {
    private int price;
}</code></pre>
<h3 id="productlistresponse">ProductListResponse</h3>
<pre><code class="language-java">@Getter
@Builder
@AllArgsConstructor
public class ProductListResponse {
    private List&lt;ProductResponse&gt; productResponseList;
    private Long productCount;

    public static ProductListResponse of (List&lt;Product&gt; products) {
        return ProductListResponse.builder()
                .productResponseList(products.stream().map(ProductResponse::of).toList())
                .productCount((long) products.size())
                .build();
    }
}</code></pre>
<h3 id="productresponse">ProductResponse</h3>
<pre><code class="language-java">@Getter
@Builder
@AllArgsConstructor
public class ProductResponse {
    private Long productId;
    private String productName;
    private int price;
    private String manufacturer;
    private String category;
    private String imageUrl;

    public static ProductResponse of (Product product) {
        return ProductResponse.builder()
                .productId(product.getId())
                .productName(product.getProductName())
                .price(product.getPrice())
                .category(product.getCategory().getCategory())
                .manufacturer(product.getManufacturer().getCompany())
                .imageUrl(product.getImageUrl())
                .build();
    }
}
</code></pre>
<h2 id="5-repository">5. Repository</h2>
<ul>
<li>검색 관련 쿼리만 JPQL을 사용하여 작성하였습니다.</li>
</ul>
<pre><code class="language-java">public interface ProductRepository extends JpaRepository&lt;Product, Long&gt; {
    List&lt;Product&gt; findAllByCategory(Category category, Sort sort);
    @Query(&quot;select p from Product p where p.productName LIKE %:keyword%&quot; )
    List&lt;Product&gt; searchByName(@Param(&quot;keyword&quot;)String keyword, Sort sort);
}</code></pre>
<h2 id="6-service">6. Service</h2>
<h4 id="코드-2">코드</h4>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class ProductService {

    private final ManufacturerRepository manufacturerRepository;
    private final CategoryRepository categoryRepository;
    private final ProductRepository productRepository;

    //TODO: 상품 생성
    @Transactional
    public ProductResponse createProduct(CreateProductRequest request) {
        Manufacturer manufacturer = getManufacturer(request.getManufacturerId());
        Category category = getCategory(request.getCategoryId());

        Product newProduct = Product.builder()
                .manufacturer(manufacturer)
                .name(request.getProductName())
                .price(request.getPrice())
                .category(category)
                .imageUrl(request.getImageUrl())
                .build();

        productRepository.save(newProduct);

        return ProductResponse.of(newProduct);
    }

    //TODO: 상품 목록조회 (일반)
    @Transactional(readOnly = true)
    public ProductListResponse getProductList(String sortBy) {
        Sort sort = createSort(sortBy);
        List&lt;Product&gt; productList = productRepository.findAll(sort);

        return ProductListResponse.of(productList);
    }

    //TODO: 상품 목록조회 (카테고리)
    @Transactional(readOnly = true)
    public ProductListResponse getProductListByCategory(Long categoryId, String sortBy) {
        Category category = getCategory(categoryId);
        Sort sort = createSort(sortBy);
        List&lt;Product&gt; productList = productRepository.findAllByCategory(category, sort);

        return ProductListResponse.of(productList);
    }

    //TODO: 상품 개별조회
    @Transactional(readOnly = true)
    public ProductResponse getProductDetail(Long productId) {
        Product product = getProduct(productId);

        return ProductResponse.of(product);
    }

    //TODO: 상품 가격 수정
    @Transactional
    public void updateProductPrice(Long productId, UpdateProductPrice request) {
        Product product = getProduct(productId);
        product.updatePrice(request.getPrice());
    }

    //TODO: 상품 삭제
    @Transactional
    public void deleteProduct(Long productId) {
        Product product = getProduct(productId);
        productRepository.delete(product);
    }

    //TODO: 상품 검색
    @Transactional(readOnly = true)
    public ProductListResponse searchProduct(SearchProduct request, String sortBy) {
        Sort sort = createSort(sortBy);
        List&lt;Product&gt; productList = productRepository.searchByName(request.getKeyword(),sort);

        return ProductListResponse.of(productList);
    }

    private Product getProduct(Long productId) {
        return productRepository.findById(productId)
                .orElseThrow(() -&gt; new CustomException(ErrorCode.NOT_FOUND));
    }
    private Manufacturer getManufacturer(Long manufacturerId) {
        return manufacturerRepository.findById(manufacturerId)
                .orElseThrow(() -&gt; new CustomException(ErrorCode.NOT_FOUND));
    }

    private Category getCategory (Long categoryId) {
        return categoryRepository.findById(categoryId)
                .orElseThrow(() -&gt; new CustomException(ErrorCode.NOT_FOUND));
    }

    //AI 도움.
    private Sort createSort (String sortBy) {
        try {
            String[] sortParams = sortBy.split(&quot;,&quot;);
            String property = sortParams[0];
            String direction = sortParams[1];

            if (direction.equalsIgnoreCase(&quot;asc&quot;)) {
                return Sort.by(Sort.Direction.ASC, property);
            } else {
                return Sort.by(Sort.Direction.DESC, property);
            }
        } catch (Exception e) {
            return Sort.by(Sort.Direction.DESC, &quot;productName&quot;);
        }
    }

}</code></pre>
<h2 id="7-테스트-화면">7. 테스트 화면</h2>
<h3 id="1-상품-목록-조회-일반-get">(1) 상품 목록 조회: 일반 (GET)</h3>
<h4 id="url-products">URL: /products</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/58b6da70-90dc-45a0-9e0d-900d2a3896e6/image.png" alt=""></p>
<hr>
<h3 id="2-상품-목록-조회-카테고리별-get">(2) 상품 목록 조회: 카테고리별 (GET)</h3>
<h4 id="url-productcategorycategoryid">URL: /product/category/{categoryId}</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/4b2f0ded-7a5c-43e6-b574-a3af96c35506/image.png" alt=""></p>
<hr>
<h3 id="3-상품-목록-조회-가격순-내림차순-get">(3) 상품 목록 조회 (가격순: 내림차순) (GET)</h3>
<h4 id="url-productssortbypricedesc">URL: /products?sortBy=price,desc</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/cdf451f5-bff6-48c3-9924-f1ea319b6e38/image.png" alt=""></p>
<hr>
<h3 id="4-상품-목록-조회-가격순-오름차순-get">(4) 상품 목록 조회 (가격순: 오름차순) (GET)</h3>
<h4 id="url-productssortbypriceasc">URL: /products?sortBy=price,asc</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/fd019b6a-c7cb-46b7-ab6d-51b6ae23ba27/image.png" alt=""></p>
<hr>
<h3 id="5-상품-개별-조회-get">(5) 상품 개별 조회 (GET)</h3>
<h4 id="url-productsproductid">URL: /products/{productId}</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/180be5ff-b62b-4c6a-8ab4-c27166e0fd10/image.png" alt=""></p>
<hr>
<h3 id="6-상품-생성-post">(6) 상품 생성 (POST)</h3>
<h4 id="url-products-1">URL: /products</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/b3ce1614-5987-4d1f-9bd0-b675b771751e/image.png" alt=""></p>
<hr>
<h3 id="7-상품-가격-수정">(7) 상품 가격 수정</h3>
<h4 id="url-productsproductid-1">URL: /products/{productId}</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/56fec434-c8e5-4e32-b007-eeddc2995302/image.png" alt=""></p>
<hr>
<h3 id="8-상품-삭제">(8) 상품 삭제</h3>
<h4 id="url-productsproductid-2">URL: /products/{productId}</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/525263c4-667c-4961-85f9-7f3acb0427f5/image.png" alt=""></p>
<hr>
<h3 id="9-상품-키워드-검색">(9) 상품 키워드 검색</h3>
<h4 id="url-productssearch">URL: /products/search</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/d89d1901-59fb-4385-9059-009a361bf9a7/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 9주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-9%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-9%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sun, 24 May 2026 06:01:33 GMT</pubDate>
            <description><![CDATA[<h1 id="10장-객체지향-쿼리-언어-1">10장. 객체지향 쿼리 언어 (1)</h1>
<p>JPA는 복잡한 검색 조건을 사용해 엔티티 객체를 조회할 수 있는 다양한 쿼리 기술을 지원한다.</p>
<p>JPQL은 가장 중요한 객체지향 쿼리 언어이다. Criteria나 QueryDSL은 결국 JPQL을 편리하게 사용하도록 도와주는 기술이다.</p>
<h2 id="101-객체지향-쿼리-소개">10.1 객체지향 쿼리 소개</h2>
<p><code>EntityManager.find()</code> 메소드를 사용하면 식별자로 엔티티 하나를 조회할 수 있다. 이렇게 조회한 엔티티에 객체 그래프 탐색을 사용하면 연관된 엔티티들을 찾을 수 있다. </p>
<ul>
<li>식별자로 조회: <code>EntityManager.find()</code></li>
<li>객체 그래프 탐색: 예) <code>a.getB().getC()</code></li>
</ul>
<p>하지만 위의 방법으로는 현실적이고 복잡한 검색이 어렵다.</p>
<h4 id="jpql의-특징">JPQL의 특징</h4>
<p>데이터베이스 테이블이 아닌 <strong>엔티티 객체를 대상으로 개발</strong>하기에 검색 역시 테이블이 아닌 엔티티 객체를 대상으로 하는 방법.</p>
<ul>
<li>테이블이 아닌 <strong>객체를 대상으로 검색하는 객체지향 쿼리</strong></li>
<li>SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않음.</li>
</ul>
<p>SQL: 데이터베이스 테이블을 대상으로 하는 데이터 중심의 쿼리
JPQL: 엔티티 객체를 대상으로하는 객체지향 쿼리. JPA는 JPQL을 분석해 적절한 SQL을 만들어 데이터베이스를 조회 -&gt; 그 결과로 엔티티 객체를 생성해 반환한다.</p>
<p>+) JPA가 공식 지원하는 기능</p>
<ul>
<li>JPQL</li>
<li>Criteria 쿼리: JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음</li>
<li>네이티브 SQL: JPA에서 JPQL 대신 직접 SQL 사용 가능</li>
</ul>
<p>+) JPA가 공식 지원은 하지 않는 기능</p>
<ul>
<li>QueryDSL: Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음. 비표준 오픈소스 프레임워크.</li>
<li>JDBC 직접 사용/MyBatis 같은 SQL 매퍼 프레임워크.</li>
</ul>
<h3 id="1-jpql-소개">(1) JPQL 소개</h3>
<p>JPQL은 <strong>엔티티 객체를 조회하는 객체지향 쿼리</strong>로 문법은 SQL과 비슷하고 ANSI 표준 SQL이 제공하는 기능을 유사하게 지원.</p>
<p>JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다. 그리고 데이터베이스 방언만 변경하면 JPQL을 수정하지 않아도 자연스럽게 데이터베이스를 변경할 수 있다.</p>
<p>또한 JPQL은 SQL보다 간결하다. 엔티티 직접 조회, 묵시적 조인, 다형성 지원으로인해 SQL보다 코드가 간결하다.</p>
<p>예) 회원 엔티티 (회원 이름이 kim인 엔티티를 조인)</p>
<pre><code class="language-java">@Entity (name = &quot;Member&quot;)
public class Member {
    @Column (name = &quot;name&quot;)
    private String username;
}

String jpql = &quot;select m from Member as m where m.username = &#39;kim&#39;&quot;;
List&lt;Member&gt; resultList = em.createQuery(jpql, Member.class).getReulstList();</code></pre>
<h3 id="2-criteria-쿼리-소개">(2) Criteria 쿼리 소개</h3>
<p><strong>Criteria</strong>: JPQL을 생성하는 빌더 클래스. 장점은 문자가 아닌 <code>query.select(m).where(...)</code> 처럼 프로그래밍 코드로 JPQL을 작성할 수 있다는 점.</p>
<p>문자열 기반 쿼리와는 다르게 <strong>컴파일 시점에 오류를 발견 가능</strong>.</p>
<blockquote>
<p><strong>Criteria로 작성한 JPQL의 장점</strong></p>
</blockquote>
<ul>
<li>컴파일 시점에 오류를 발견할 수 있음</li>
<li>IDE를 사용하면 코드 자동완성을 지원함</li>
<li>동적 쿼리를 작성하기 편함</li>
</ul>
<p>JPA의 경우 2.0부터 Criteria를 지원함.
예)</p>
<pre><code class="language-java">CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery&lt;Member&gt; query = cb.createQuery(Member.class);

//루트 클래스 (조회를 시작할 클래스)
Root&lt;Member&gt; m = query.from(Member.class);

//쿼리 생성
CriteriaQuery&lt;Member&gt; cq = query.select(m).where(cb.equal(m.get(&quot;username&quot;), &quot;kim&quot;));
List&lt;Member&gt; resultList = em.createQuery(cq).getResultList();</code></pre>
<p>위를 참고하면 쿼리를 문자가 아닌 코드로 작성한 것을 알 수 있다. 위에서는 <code>m.get(&quot;username&quot;)</code>을 문자로 작성했지만, <strong>메타 모델</strong>을 사용하면 문자가 아니라 코드로 해당 부분을 작성할 수 있다.</p>
<p><strong>메타 모델 API</strong>: 자바가 제공하는 어노테이션 프로세서 기능 사용 시, 어노테이션을 분석하여 클래스를 생성 가능함. JPA의 경우 이 기능을 사용해 Member 엔티티 클래스로 부터 Member_라는 Criteria 전용 클래스를 생성하는데 이것을 이르는 말.</p>
<p>메타 모델을 사용하면 온전히 코드만 사용해서 쿼리를 작성할 수 있음.</p>
<pre><code class="language-java">m.get(&quot;username&quot;) -&gt; m.get(Member_.username)</code></pre>
<ul>
<li>Criteria가 가진 장점은 많지만, 이를 모두 상쇄할 만큼 복잡하고 장황함. 사용이 불편한 것과 동시에 Criteria로 작성한 코드는 한눈에 잘 들어오지 않는다는 단점 역시 존재한다.</li>
</ul>
<h3 id="3-querydsl-소개">(3) QueryDSL 소개</h3>
<p><strong>QueryDSL</strong>: Criteria처럼 JPQL 빌더의 역할을 함. 장점은 코드 기반이면서도 단순하고 사용이 쉽다. 또한 작성한 코드 역시 JPQL과 비슷해 한눈에 들어온다.  다만 JPA 표준이 아니고 오픈소스 프로젝트이다.</p>
<pre><code class="language-java">JPAQuery query = new JPAQuery(em);
QMember member = QMember.member;

List&lt;Member&gt; members = 
    query.from(member)
    .where(member.username.eq(&quot;kim&quot;))
    .list(member);</code></pre>
<p>QueryDSL 역시 어노테이션 프로세서를 통해 쿼리 전용 클래스를 만들어야함. QMember는 여기서 Member 엔티티 클래스를 기반으로 생성한 QueryDSL 쿼리 전용 클래스.</p>
<h3 id="4-네이티브-sql-소개">(4) 네이티브 SQL 소개</h3>
<p>JPA는 SQL을 직접 사용할 수 있는 기능을 제공하는데 이것을 &quot;네이티브 SQL&quot;이라고 함.</p>
<p>JPQL을 사용한다해도 가끔 특정 데이터베이스에 의존하는 기능을 사용하게 될 때가 존재한다.
예) 오라클 데이터베이스에서만 쓰는 CONNECT BY 등의 사용</p>
<p>다만 이런 특정 데이터베이스의 기능은 표준화되어있지 않기에 JPQL에서 사용할 수 없다. 이때 네이티브 SQL을 사용하면 된다.</p>
<pre><code class="language-java">String sql = &quot;SELECT id, age, team_id, name FROM member WHERE name = &quot;kim&quot;;
List&lt;Member&gt; resultList = em.createNativeQuery(sql, Member.class).getResultList();</code></pre>
<h3 id="5-jdbc의-직접-사용--마이바티스-같은-sql-매퍼-프레임워크-사용">(5) JDBC의 직접 사용 / 마이바티스 같은 SQL 매퍼 프레임워크 사용</h3>
<p>JDBC 커넥션에 직접 접근하려할 때 JPA는 JDBC 커텍션을 획득하는 API를 제공하지 않기에, JPA 구현체가 제공하는 방법을 사용해야함.</p>
<pre><code class="language-java">Session session = entityManager.unwrap(Session.class);
session.doWork(new Work () {
    @Override
    public void execute(Connection connection) throws SQLException {
        //work...
    }
});</code></pre>
<ul>
<li>JPA의 EntityManager에서 하이버네이트 Session을 우선 구현해야함</li>
<li>이후 Session의 doWork() 메소드를 호출해야함.</li>
</ul>
<p><strong>JDBC나 마이바티스를 JPA와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 플러시(flush)해야함.</strong> 둘 모두 JPA를 우회해서 데이터베이스에 접근하기에 JPA는 인식할 수 없음. 따라서 영속성 컨텍스트와 데이터베이스를 불일치 상태로 만들어 데이터 무결성을 훼손하게 될 수 있으니 주의해야함.</p>
<ul>
<li>참고: 스프링프레임워크 사용시 JPA와 마이바티스를 쉽게 통합 가능함. 또한 스프링프레임워크의 AOP를 적절히 활용해 JPA를 우회해 데이터베이스에 접근하는 메소드를 호출할 때마다 영속성 컨텍스트를 플러시하면 위에서 언급한 문제도 해결 가능.</li>
</ul>
<hr>
<h2 id="102-jpql">10.2 JPQL</h2>
<blockquote>
<h4 id="jpql의-특징-1">JPQL의 특징</h4>
</blockquote>
<ul>
<li>객체지향 쿼리 언어. 즉, 테이블 대상으로 쿼리하는 것이 아니라 <strong>엔티티 객체</strong>를 대상으로 쿼리</li>
<li>JPQL은 SQL을 추상화하여 특정 데이터베이스 SQL에 의존하지 않음.</li>
<li>JPQL은 결국 SQL로 변환됨.</li>
</ul>
<h3 id="1-기본-문법과-쿼리-api">(1) 기본 문법과 쿼리 API</h3>
<p>JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있음. 엔티티 저장 시에는 EntityManager.persist() 메소드를 사용하면 되기에 INSERT문은 없음.</p>
<pre><code>select_문 ::=
    select_절
    from_절
    [where_절]
    [groupby_절]
    [having_절]
    [orderby_절]
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]</code></pre><ul>
<li>JPQL에서 UPDATE, DELETE문은 벌크 연산이라고 함.</li>
</ul>
<h4 id="select문">SELECT문</h4>
<pre><code class="language-java">SELECT m FROM Meber AS m WHERE m.username = &quot;Hello&quot;</code></pre>
<ul>
<li>대소문자 구분:
엔티티 속성은 대소문자를 구분. 반면 JPQL 키워드(SELECT, FROM, AS)는 대소문자를 구분하지 않음.</li>
<li>엔티티 이름: 
JPQL에서 사용한 Member는 클래스명이 아닌 <strong>엔티티명</strong>이다. 엔티티명의 경우 <code>@Entity(name = &quot;XXX&quot;);</code>를 통해 설정할 수 있음. 그러지 않을 경우 클래스명을 기본값으로 사용. 기본값을 사용하는 것이 추천됨.</li>
<li><strong>별칭은 필수</strong>
<code>Member AS m</code>을 살펴보면 Member에 m이라는 별칭을 부여했는데 JPQL의 경우에는 별칭이 필수적임. 별칭이 없으면 오류 발생.</li>
</ul>
<h4 id="typequery-query">TypeQuery, Query</h4>
<p>작성한 JPQL의 실행을 위해서는 쿼리 객체가 필요하다. 이때 쿼리 객체로는 <strong><code>TypeQuery</code></strong>와 <strong><code>Query</code></strong>가 존재함. 반환 타입을 명확히 지정할 수 있을 때, TypeQuery 객체를 사용하고 그렇지 않을 때는 Query 객체를 사용하면 된다.</p>
<p>예)</p>
<pre><code class="language-java">TypedQuery&lt;Member&gt; query = em.createQuery(&quot;SELECT m FROM Member m&quot;, Member.class);

List&lt;Member&gt; resultList = query.getResultList();
for (Member member: resultList) {
    System.out.println(&quot;member = &quot; + member);
}</code></pre>
<p><code>em.createQuery()</code>의 두 번째 파라미터에 반환할 타입을 지정할 때는 TypeQuery를, 그렇지 않을 때에는 Query를 반환함. 조회 대상이 Member 엔티티이기에 대상 타입이 명확한데 이럴 때에는 TypeQuery를 사용할 수 있는 것이다.</p>
<pre><code class="language-java">Query query = em.createQuery(&quot;SELECT m.username, m.age FROM Member m&quot;);
List resultList = query.getResultList();

for (Object o: resultList) {
    Object [] result = (Object[]) o;
    System.out.println(&quot;username = &quot;+result[0]);
    System.out.println(&quot;age = &quot; + result[1]);
}</code></pre>
<p>위의 경우에는 조회대상이 String 타입과 Integer 타입으로 조회 대상 타입이 명확하지 않다. SELECT 절에서 여러 엔티티나 컬럼을 선택하는 경우에는 <strong>Query 객체</strong>를 사용해야함.</p>
<p>Query 객체는 SELECT절의 조회 대상이 예제처럼 둘 이상이면 Object[]를 반환, 하나면 Object를 반환함.</p>
<h4 id="결과-조회">결과 조회</h4>
<p>아래 메소드를 호출해 실제 쿼리를 실행 및 데이터베이스를 조회하게된다.</p>
<ul>
<li><p><code>query.getResultList()</code>: 결과를 예제로 반환. 결과가  없으면 빈 컬렉션을 반환.</p>
</li>
<li><p><code>query.getSingleResult()</code>: 결과가 정확히 하나일 때 사용.</p>
<ul>
<li>결과가 없으면 <code>..NoResultException</code>예외 발생</li>
<li>결과가 1개보다 많으면 <code>..NonUniqueResultException</code>예외 발생</li>
</ul>
</li>
</ul>
<p>이때 <code>getSingleResult()</code>는 결과가 정확히 1개가 아니면 예외를 발생함.</p>
<h3 id="2-파라미터-바인딩">(2) 파라미터 바인딩</h3>
<p>JDBC는 위치 기준 파라미터 바인딩만 지원하지만 JPQL은 이름 기준 파라미터 바인딩 역시 지원함.</p>
<h4 id="이름-기준-파라미터">이름 기준 파라미터</h4>
<p>이름 기준 파라미터: 파라미터를 <strong>이름으로 구분</strong>하는 방법. 이름 기준 파라미터는 <strong>앞에 :를 사용</strong>.</p>
<p><code>:username</code>이라는 이름 기준 파라미터를 정의하고 <code>query.setParameter()</code>에서 <code>username</code>이라는 이름으로 파라미터를 바인딩.</p>
<p>JPQL API는 대부분 메소드 체인 방식으로 설계되어 있어 연속해서 작성할 수 있음.</p>
<pre><code class="language-java">List&lt;Member&gt; members = 
em. createQuery(&quot;SELECT m FROM Member m where m.username = :username, Member.class)
    .setParameter(&quot;username&quot;, usernameParam)
    .getResult();</code></pre>
<h4 id="위치-기준-파라미터">위치 기준 파라미터</h4>
<p>위치 기준 파라미터를 사용하기 위해서는 <code>?</code> 다음에 위치값을 주면 된다. 위치 값은 단 1부터 시작한다.</p>
<pre><code class="language-java">List&lt;Member&gt; members =
    em.createQuery(&quot;SELECT m FROM Member m where m.username = ?1&quot;, Member.class)
        .setParameter(1, usernameParam)
        .getResultList();</code></pre>
<p>위치 기준 파라미터는 <strong>방식보다는 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확하다</strong>.</p>
<h3 id="3-프로젝션">(3) 프로젝션</h3>
<p><strong>프로젝션</strong>: SELECT절에 조회할 대상을 지정하는 것.</p>
<blockquote>
<p><strong>SELECT</strong> {프로젝션 대상} <strong>FROM</strong></p>
</blockquote>
<h4 id="엔티티-프로젝션">엔티티 프로젝션</h4>
<pre><code class="language-java">SELECT m FROM Member m //회원
SELECT m.team FROM Member m //팀</code></pre>
<p>처음은 회원을 조회, 두 번째에는 회원과 연관된 팀을 조회하는데 둘 다 엔티티를 프로젝션 대상으로 사용함. 쉽게 생각하면 원하는 객체를 바로 조회한 것인데 컬럼을 하나하나 나열해서 조회해야하는 SQL과는 차이가 존재한다.</p>
<p>이렇게 <strong>조회한 엔티티는 영속성 컨텍스트에서 관리</strong>된다.</p>
<h4 id="임베디드-타입-프로젝션">임베디드 타입 프로젝션</h4>
<p>JPQL에서 임베디드 타입은 엔티티와 거의 비슷하게 사용됨. 임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 존재.</p>
<pre><code class="language-java">String query = &quot;SELECT a FROM Address a&quot;;</code></pre>
<p>따라서 위의 코드는 잘못된 쿼리이며 아래처럼 사용해야함.</p>
<pre><code class="language-java">String query = &quot;SELECT o.address FROM Order o&quot;;
List&lt;Address&gt; address = em.createQuery(query, Address.class).getResultList();</code></pre>
<p><strong>임베디드 타입은 엔티티 타입이 아닌 값 타입. 이렇게 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않음.</strong></p>
<h4 id="스칼라-타입-프로젝션">스칼라 타입 프로젝션</h4>
<p>스칼라 타입: 숫자, 문자, 날짜와 같은 기본 데이터 타입.</p>
<pre><code class="language-java">List&lt;String&gt; username = 
    em.createQuery(&quot;SELECT username FROM Member m&quot;, String.class)
    .getResultList();</code></pre>
<p>중복 데이터 제거 시에는 DISTINCT를 사용.</p>
<pre><code class="language-java">SELECT DISTINCT username FROM Member m</code></pre>
<p>다음과 같은 통계 쿼리도 주로 스칼라  타입으로 조회한다.</p>
<pre><code class="language-java">Double orderAmountAvg = em.createQuery(&quot;SELECT AVG(o.orderAmount) FROM Order o&quot;, Double.class)
    . getSingleResult();</code></pre>
<h4 id="여러-값-조회">여러 값 조회</h4>
<p>엔티티를 대상으로 조회하면 편리하겠지만, 꼭 필요한 데이터만 선택해 조회해야할 때가 존재함. 이럴 때에는 TypeQuery는 사용할 수 없기에 Query를 사용해야함.</p>
<pre><code class="language-java">Query query =
    em.createQuery(&quot;SELECT m.username, m.age FROM Member m&quot;);
List resultList = query.getResultList();

Iterator iterator = resultList.iterator();
while (iterator.hasNext()) {
    Object[] row = (Object[]) iterator.next();
    String username = (String) row[0];
    Integer age = (Integer) row[1];
}</code></pre>
<p>제너릭에 Obejct[]를 사용하면 아래처럼 더 간결하게 개발할 수 있다.</p>
<pre><code class="language-java">List&lt;Object[]&gt; resultList =
    em.createQuery(&quot;SELECT m.username, m.age FROM Member m&quot;)
    .getResult();

for (Object[] row : resultList) {
    String username = (String) row[0]
}</code></pre>
<p>스칼라 뿐만 아니라 엔티티 타입 역시 여러 값을 함께 조회할 수 있다.</p>
<h4 id="new-명령어">NEW 명령어</h4>
<p>앞에서는 여러 필드를 프로젝션할 경우 타입 지정이 불가해 TypeQuery를 사용하지 못해 Object[]를 반환받았지만, 실제 애플리케이션에서는 Object[]를 직접 사용하지 않고 UserDTO와 같은 객체로 변환해 사용하게 된다.</p>
<pre><code class="language-java">List&lt;UserDTO&gt; userDTOs = new ArrayList&lt;&gt;();
for (Object[] row:resultList) {
    UserDTO userDTO = new UserDTO((String) row[0], (Integer) row[1]);
    userDTOs.add(userDTO);
}

return userDTOs;

public class UserDTO {
    private String username;
    private int age;

    public UserDTO (String username, int age) {
        this.username = username;
        this.age = age;
    }
}</code></pre>
<p>NEW 명령어를 사용하는 예)</p>
<pre><code class="language-java">TypedQuery&lt;UserDTO&gt; query = 
    em.createQuery(&quot;SELECT new jpabook.jpql.UserDTO(m.username, m.age)
                    FROM Member m&quot;, UserDTO.class);
    List&lt;UserDTO&gt; resultList = query.getResultList();</code></pre>
<p>SELECT 이후에 NEW 명령어를 사용 시 반환받을 클래스를 지정할 수 있는데, 이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있다. 또한 NEW 명령어를 사용해 클래스로 TypeQuery를 사용할 수 있어 지루한 객체 변환 작업을 줄일 수 있음.</p>
<p><strong>주의사항</strong></p>
<ul>
<li>패키지명을 포함한 전체 클래스명을 입력해야함</li>
<li>순서와 타입이 일치하는 생성자가 필요.</li>
</ul>
<h3 id="4-페이징-api">(4) 페이징 API</h3>
<p>페이지 처리용 SQL을 작성하는 것은 귀찮기도 하고 데이터베이스마다 이 페이징 처리 문법이 다르다는 문제가 존재함.</p>
<ul>
<li><code>setFirstResult(int startPosition)</code>: 조회 시작 위치(0부터 시작)</li>
<li><code>setMaxResult(int maxResult)</code>: 조회할 데이터 수</li>
</ul>
<pre><code class="language-java">TypedQuery&lt;Member&gt; query = 
    em.createQuery(&quot;SELECT m FROM Member m ORDER BY m.username DESC&quot;, Member.class);

query.setFirstResult(10);
query.setMaxResults(20);
query.getResultList();</code></pre>
<p>데이터베이스마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은 데이터베이스 방언 덕분이다. </p>
<h4 id="mysql의-페이징">MySQL의 페이징</h4>
<pre><code class="language-sql">SELECT
    M.ID AS ID,
    M.AGE AS AGE,
    M.TEAM_ID AS TEAM_ID, 
    M.NAME AS NAME
FROM MEMBER M
ORDER BY M.NAME DESC LIMIT ?,?</code></pre>
<p>데이터베이스마다 SQL이 다르고, 오라클과 SQLServer의 경우에는 페이징 쿼리를 따로 공부해야 작성할 수 있을 정도로 복잡함. ?에 바인딩하는 값 역시도 데이터베이스마다 다르며 적절한 값을 입력해야함.</p>
<h3 id="5-집합과-정렬">(5) 집합과 정렬</h3>
<p>집합 =&gt; 집합함수와 함께 통계 정보를 구할 때 사용.</p>
<h4 id="집계함수">집계함수</h4>
<ul>
<li>COUNT: 결과의 수를 구함. 반환은 Long 타입.</li>
<li>MAX, MIN: 최대, 최소 값을 구함. 문자, 숫자, 날짜 등에 사용.</li>
<li>AVG: 평균값을 구함. 숫자타입만 사용할 수 있음. 반환은 Double 타입.</li>
<li>SUM: 합을 구함. 숫자타입만 사용 가능하면 반환은 정수합, 소수합.. 등.</li>
</ul>
<h4 id="집계함수-사용-시-참고사항">집계함수 사용 시 참고사항</h4>
<ul>
<li>널(NULL)값은 무시하기에 통계에 잡히지 않음. (DISTINCT 정의가 되어있어도)</li>
<li>값이 없는데 SUM, AVG, MAX, MIN을 사용하면 널값을 반환. COUNT의 경우에는 0.</li>
<li>DISTINCT를 집계함수 안에 사용하면 중복값을 제거하고 집합을 구할 수 있음.</li>
<li>DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원x</li>
</ul>
<h4 id="group-by-having">GROUP BY, HAVING</h4>
<ul>
<li>GROUP BY: 통계 데이터를 구할 때 특정 그룹끼리 묶어줌.</li>
<li>HAVING: GROUP BY로 그룹화한 통계 데이터를 기준으로 필터링.</li>
</ul>
<p>이런 쿼리들을 리포팅 쿼리/통계 쿼리라함. 결과가 많을 때에는 통계 결과만 저장하는 테이블을 별도로 만들어 두고 사용자가 적은 새벽에 통계 쿼리를 실행해 결과를 보관하는 것이 좋음.</p>
<h4 id="정렬-order-by">정렬 (ORDER BY)</h4>
<p>ORDER BY: 결과를 정렬할 때 사용. 내림차순(DESC) 또는 오름차순(ASC) 선택.</p>
<h3 id="6-jpql-조인">(6) JPQL 조인</h3>
<p>JPQL의 조인은 SQL과 기능은 같고 문법만 약간 다르다.</p>
<h4 id="내부조인">내부조인</h4>
<p>내부조인은 <code>INNER JOIN</code>을 사용. INNER는 생략이 가능하다.</p>
<pre><code class="language-java">String teamName = &quot;팀A&quot;;
String query = &quot;SELECT m FROM Member m INNER JOIN m.team t &quot; + &quot;WHERE t.name = :teamName&quot;;
List&lt;Member&gt; members = em.createQuery(query, Member.class)
    .setParameter(&quot;teamName&quot;. teamName);
    .getResultList();</code></pre>
<p>회원과 팀 내부 조인을 통해 팀A에 소속된 회원을 조회하는 JPQL 예)</p>
<pre><code class="language-java">SELECT m
FROM Member m INNER JOIN m.team t
where t.name = :teamName</code></pre>
<p>JPQL 조인의 특징</p>
<ul>
<li>연관 필드를 사용함: <code>m.team</code>이 예로, 연관 필드는 <strong>다른 엔티티와 연관관계를 가지기 위해 사용하는 필드를 말함.</strong></li>
<li>FROM Member m: 회원을 선택하고 m이라는 별칭 부여</li>
<li>Member m JOIN m.team t: 회원이 가지고 있는 연관 필드로 팀과 조인. 조인한 팀에 t라는 별칭 부여.</li>
</ul>
<p>JPQL 조인을 SQL 조인처럼 사용하면 문법 오류 발생. <strong>JPQL은 JOIN 명령어 다음 조인할 객체의 연관 필드를 사용</strong>.</p>
<h4 id="외부조인">외부조인</h4>
<pre><code class="language-java">SELECT m
FROM Member m LEFT [OUTER] JOIN m.team t</code></pre>
<h4 id="컬렉션-조인">컬렉션 조인</h4>
<p>일대다 관계 또는 다대다관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 <strong>컬렉션 조인</strong>이라함.</p>
<ul>
<li>[회원 -&gt; 팀]으로의 조인: 다대일 조인이면서 단일값 연관 필드를 사용.</li>
<li>[팀 -&gt; 회원]으로의 조인: 일대다 조인이면서 컬렉션값 연관 필드를 사용.</li>
</ul>
<pre><code class="language-java">SELECT t,m FROM Team t LEFT JOIN t.members m</code></pre>
<p><code>t LEFT JOIN t.members</code>는 팀이 보유한 회원목록을 <strong>컬렉션 값 연관 필드로 외부조인</strong></p>
<h4 id="세타-조인">세타 조인</h4>
<p>WHERE절을 사용해 세타 조인이 가능함. <strong>세타 조인은 내부 조인만을 지원</strong>. 세타 조인 사용시에는 전혀 관계없는 엔티티도 조인할 수 있음.</p>
<pre><code class="language-java">select count(m) from Member m, Team t
where m.username = t.name</code></pre>
<h4 id="join-on">JOIN ON</h4>
<p>조인할 때 ON절을 지원. ON절 사용 시에는 조인 대상을 필터링하고 조인할 수 있음.</p>
<ul>
<li>참고: 내부조인의 ON 절은 WHERE절을 사용할 때와 결과가 같기에 보통 ON절은 외부 조인에서만 사용함.</li>
</ul>
<pre><code class="language-java">select m, t from Member m
left join m.team t on t.name = &#39;A&#39;</code></pre>
<p><code>t.name = &#39;A&#39;</code>로 조인시점에 조인 대상을 필터링함.</p>
<h4 id="페치조인">페치조인</h4>
<p>페치(fetch)조인: SQL의 조인의 종류가 아닌 JPQL에서 성능 최적화를 위해 제공하는 기능. 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능으로 join fetch 명령어를 통해 사용 가능.</p>
<ul>
<li>엔티티 페치조인<pre><code class="language-java">select m 
from Member m join fetch m.team</code></pre>
</li>
</ul>
<p>join 뒤에 fetch: 연관된 엔티티나 컬렉션을 함께 조회. (여기서는 회원과 팀을 함께 조회). JPQL 조인과 다라ㅡ게 m.team 뒤에 별칭이 없는데 <strong>페치 조인은 별칭을 사용할 수 없음.</strong></p>
<p>엔티티 페치 조인 JPQL에서 select m을 통해 회원 엔티티만 선택하였지만, <strong>실행된 SQL에 따르면 회원과 연관된 팀도 함께 조회</strong>된 것을 볼 수 있음.</p>
<pre><code class="language-java">String jpql = &quot;select m from Member m join fetch m.team&quot;;

List&lt;Member&gt; members = em.createQuery(jpql, Member.class)
    .getResultList();

for (Member member:members) {
    System.out.println(&quot;username = &quot;+member.getUsername() + &quot;,&quot; + 
    &quot;teamname = &quot; +member.getTeam().name());
}</code></pre>
<p>회원을 조회할 때 페치 조인을 사용해 팀도 함께 조회했기에 <strong>연관 팀은 프록시가 아닌 실제 엔티티</strong>. 따라서 연관된 팀을 사용해도 지연로딩이 일어나지 않음. 또한, 실제 엔티티이기에 회원 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할 수 있음.</p>
<h4 id="컬렉션-페치-조인">컬렉션 페치 조인</h4>
<p>일대다 관계인 컬렉션을 페치 조인</p>
<pre><code class="language-java">select t
from Team t join fetch t.members
where t.name = &#39;팀A&#39;</code></pre>
<p>팀을 조회하면서 페치 조인을 사용해 연관된 회원 컬렉션(t.members)도 함께 조회.</p>
<p>컬렉션 페치 조인한 JPQL에서 select t로 팀만 선택했는데 실행 SQL을 보면 팀과 연관된 회원 역시 함께 조회함.  또한 TEAM 테이블에서 팀A는 하나지만 회원 테이블과 조인하면서 결과가 증가하여 팀A가 2건 조회됨.</p>
<h4 id="페치-조인과-distinct">페치 조인과 DISTINCT</h4>
<ul>
<li>SQL의 DISTINCT는 중복된 결과를 제거하는 명령. JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하는 것은 물론, 애플리케이션에서 한 번 더 중복을 제거.<pre><code class="language-java">select distinct t
from Team t join fetch t.members
where t.name = &#39;팀A&#39;</code></pre>
</li>
</ul>
<h4 id="페치-조인과-일반-조인의-차이">페치 조인과 일반 조인의 차이</h4>
<p>JPQL은 <strong>결과를 반환할 때 연관관계까지 고려하지 않음.</strong> 팀과 회원을 조인한 후에 팀을 조회한다고 해도, 회원 컬렉션은 조회하지 않음. 즉, 프록시나 아직 초기화하지 않은 컬렉션 래퍼를 반환함.</p>
<p>반면, 페치 조인은 <strong>연관된 엔티티까지 함께 조회</strong>함.</p>
<h4 id="페치-조인의-특징과-한계">페치 조인의 특징과 한계</h4>
<p>페치 조인을 사용할 때에는 SQL 한 번으로 연관된 엔티티들을 함께 조회해 SQL 호출 횟수를 줄여 성능을 최적화할 수 있음.</p>
<p><strong>글로벌 로딩 전략</strong>: 엔티티에 직접 적용하는 로딩 전략은 애플리케이션 전체에 영향을 미치기에 글로벌 로딩 전략이라 부름. 글로벌 로딩 전략을 지연로딩으로 설정해도 JPQL에서 페치 조인을 사용하면 페치조인을 적용해 함께 조회함.</p>
<blockquote>
<p>최적화를 위해 <strong>글로벌 로딩 전략을 즉시 로딩으로 설정</strong>하면 애플리케이션 전체에서 항상 <strong>즉시 로딩</strong>이 발생함. 일부 빠를 수는 있지만, 전체적으로 볼 때, 사용하지 않은 엔티티를 자주 로딩하기에 성능에 <strong>악영향을 미칠 수 있음.</strong> 
따라서 <strong>글로벌 로딩 전략은 될 수 있으면 지연 로딩을 사용하고 최적화가 필요할 때 페치 조인을 하는 것이 효과적</strong>.</p>
</blockquote>
<ul>
<li><p>페치 조인의 한계</p>
<ul>
<li><p>페치 조인 대상에는 별칭을 줄 수 없음.</p>
</li>
<li><p>둘 이상의 컬렉션을 페치할 수는 없음. 구현체에 따라 가능할 때도 있지만, 이때 카티시안 곱이 만들어지기도 하기에 주의해야함.</p>
</li>
<li><p>컬렉션을 페치 조인하면 페이징 API를 사용할 수 없음.</p>
<ul>
<li>컬렉션(일대다)이 아닌 단일값 연관필드들은 페치 조인을 해도 페이징 API 사용 불가.</li>
<li>하이버네이트에서 컬렉션을 페치 조인하고 페이징 API를 사용하면 경고 로그를 남기며 메모리에서 페이징 처리를 함. 데이터가 적으면 상관없지만, 데이터가 많으면 성능 이슈와 메모리 초과 예외가 발생할 수 있어 위험함.</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>=&gt; 페치 조인은 성능 최적화에 유용해 실무에서 자주 사용한다. 객체 그래프를 유지할 때 특히 효과적이지만, 여러 테이블을 조인해 엔티티가 가진 모양이 전혀 다른 결과를 내야할 때는 억지로 페치 조인을 사용하기 보다는 여러 테이블에서 필요한 필드들만 조회해 DTO로 반환하는 것이 더 효과적일 수 있음.</p>
<h3 id="8-경로-표현식">(8) 경로 표현식</h3>
<p>경로표현식: .(점)을 찍어 객체 그래프를 탐색하는 것을 말함.</p>
<h4 id="용어-정리">용어 정리</h4>
<ul>
<li><strong>상태 필드</strong>: 단순히 값을 저장하기 위한 필드 (필드 또는 프로퍼티)</li>
<li><strong>연관 필드</strong>: 연관관계를 위한 필드. 임베디드 타입 포함 (필드 또는 프로퍼티)</li>
<li>단일 값 연관 필드: <code>@ManyToOne</code>, <code>@OneToOne</code> 대상 엔티티</li>
<li>컬렉션 값 연관 필드: <code>@OneToMany</code>, <code>@ManyToMany</code></li>
</ul>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    private Long id; 

    @Column (name = &quot;name&quot;)
    private String username; //상태필드
    private Integer age; //상태필드

    @ManyToOne(...)
    private Team team; //연관필드

    @OneToMany(...)
    private List&lt;Order&gt; orders; //연관필드
}</code></pre>
<h4 id="경로표현식과-특징">경로표현식과 특징</h4>
<p>JPQL에서 경로 표현식을 사용해 경로 탐색을 하기 위해서는 아래 3가지 경로에 따라 어떤 특징이 있는지 이해해야함.</p>
<ul>
<li><p><strong>상태 필드 경로</strong>: 경로 탐색의 끝으로 더는 탐색이 불가능함.</p>
</li>
<li><p><strong>단일 값 연관 경로</strong>: 묵시적으로 내부 조인이 발생. 단일 값 연관 경로는 계속 탐색 가능.</p>
</li>
<li><p><strong>컬렉션 값 연관 경로</strong>: 묵시적으로 내부 조인이 발생하며 더는 탐색할 수 없다. FROM절에서 조인을 통해 별칭을 얻는 경우에는 별칭으로 탐색이 가능하다.</p>
</li>
<li><p>상태 필드 경로 탐색
<code>select m.username, m.age from Member m</code></p>
</li>
<li><p>단일 값 연관 경로 탐색
<code>select o.member from Order o</code></p>
</li>
</ul>
<p><strong>묵시적 조인</strong>: 단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부 조인이 일어나는 것을 이르는 말. 묵시적 조인은 모두 내부 조인.</p>
<ul>
<li><p>명시적 조인: JOIN을 직접 적어주는 것
<code>SELECT m FROM Member m JOIN m.team t</code></p>
</li>
<li><p>묵시적 조인: <strong>경로 표현식에 의해</strong> 묵시적으로 조인이 일어나는 것. 내부 조인만 가능하다.
<code>SELECT m.team FROM Member m</code></p>
</li>
<li><p>컬렉션 값 연관 경로 탐색
JPQL을 다루면서 가장 많이 하는 실수. <code>t.members</code>처럼 <strong>컬렉션까지는 경로탐색이 가능</strong>하지만, <code>t.members.username</code>처럼 컬렉션에서 경로 탐색을 시작하는 것은 허락하지 않음.
컬렉션에서 경로 탐색을 하려면 <strong>조인을 사용해 새로운 별칭을 획득해야함</strong></p>
</li>
</ul>
<h4 id="경로-탐색을-사용한-묵시적-조인-시-주의사항">경로 탐색을 사용한 묵시적 조인 시 주의사항</h4>
<p>경로 탐색 사용 시 묵시적 조인이 발생해 SQL에서 내부 조인이 발생할 수 있음.
주의사항:</p>
<ul>
<li>항상 <strong>내부조인</strong>이다.</li>
<li>컬렉션은 경로 탐색의 끝. 컬렉션에서 경로 탐색을 하려면 명시적으로 조인해 별칭을 얻어야함.</li>
<li>경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM절에 영향을 줌.</li>
</ul>
<p>=&gt; 성능이 중요할 때는 분석이 쉽도록 묵시적 조인보다 명시적 조인을 사용하는 것이 낫다.</p>
<h3 id="9-서브쿼리">(9) 서브쿼리</h3>
<p>JPQL에서도 서브쿼리를 지원해준다. 하지만 제약이 존재한다.
서브쿼리를 WHERE, HAVING 절에서만 사용할 수 있고, SELECT, FROM 절에서는 사용이 불가하다.</p>
<h4 id="서브쿼리-함수">서브쿼리 함수</h4>
<ul>
<li><p>[NOT] EXISTS: 서브쿼리에 결과가 존재(또는 존재하지 않으면) 참.</p>
</li>
<li><p>{ALL | ANY | SOME}: 비교 연산자와 함께 사용하며</p>
<ul>
<li>ALL: 조건을 모두 만족하면 참</li>
<li>ANY 또는 SOME: 같은 의미로 조건을 하나라도 만족하면 참.</li>
</ul>
</li>
<li><p>[NOT] IN: 서브 쿼리의 결과 중 하나라도 같은 것이 있으면 참. IN은 서브쿼리가 아닌 곳에서도 사용 가능.</p>
</li>
</ul>
<h3 id="10-조건식">(10) 조건식</h3>
<h4 id="타입표현">타입표현</h4>
<ul>
<li>문자: 작은 따옴표 사이에 표현. 작은 따옴표 표현을 위해서는 작은 따옴표를 두 번 사용.</li>
<li>숫자: L(Long), D(Double), F(Float) 지정.</li>
<li>날짜: DATE{d &#39;yyyy-mm-dd&#39;} TIME{t &#39;hh-mm-ss&#39;}</li>
<li>Boolean: TRUE, FALSE</li>
<li>Enum: 패키지명 포함한 전체 이름 예) jpabook.MemberType.Admin</li>
<li>엔티티 타입: 엔티티 타입을 표현. 예) TYPE(m) = Member</li>
</ul>
<h4 id="연산자-우선순위">연산자 우선순위</h4>
<ol>
<li>경로 탐색 연산: (.)</li>
<li>수학 연산: +, -, *, /,</li>
<li>비교 연산: =, &gt;=, &gt;, &lt;=, &lt;</li>
<li>논리 연산: AND, NOT, OR</li>
</ol>
<h4 id="컬렉션-식">컬렉션 식</h4>
<p>컬렉션에서만 사용하는 특별한 기능.</p>
<ul>
<li>빈 컬렉션 비교식
{컬렉션 연관 경로} IS [NOT] EMPTY</li>
</ul>
<p>컬렉션은 컬렉션 식만 사용할 수 있음. </p>
<ul>
<li>컬렉션 멤버 식
{엔티티/값} [NOT] MEMBER [OF] {컬렉션 값 연관 경로}</li>
</ul>
<h4 id="스칼라-식">스칼라 식</h4>
<p>스칼라는 숫자, 문자, 날짜, case, 엔티티 타입 같은 기본적인 타입을 말함.
수학식, 문자함수, 수학함수, 날짜함수 등을 사용할 수 있음.</p>
<h4 id="case-식">CASE 식</h4>
<p>특정 조건에 따라 분기할 때 사용하며 4가지가 존재함.</p>
<ul>
<li>기본 CASE</li>
<li>심플 CASE</li>
<li>COALESCE</li>
<li>NULLIF</li>
</ul>
<p><strong>기본 case</strong>:</p>
<pre><code class="language-sql">CASE
    {WHEN 조건식 THEN 스칼라식}
    ELSE 스칼라식
END</code></pre>
<p><strong>심플 case</strong>:</p>
<pre><code class="language-sql">CASE {조건대상}
    WHEN 스칼라식 THEN 스칼라식
    ELSE 스칼라식
END</code></pre>
<p><strong>COALESCE</strong>:</p>
<pre><code class="language-sql">    COALESCE(스칼라식, 스칼라식,...)</code></pre>
<p>스칼라식을 차례대로 조회해 null이 아니면 반환</p>
<p><strong>NULLIF</strong></p>
<pre><code class="language-sql">NULLIF (스칼라식, 스칼라식)</code></pre>
<p>두 값이 같으면 널을 반환하고, 다름녀 첫 번째 값을 반환 보통 집합함수와 함께 사용.</p>
<h3 id="11-다형성-쿼리">(11) 다형성 쿼리</h3>
<p>JPQL로 부모 엔티티 조회 시 그 자식 엔티티도 함께 조회함.</p>
<h4 id="type">TYPE</h4>
<p>TYPE은 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 주로 사용.</p>
<p>예)</p>
<pre><code class="language-java">select i from Item i
where type(i) IN (Book, Movie)</code></pre>
<p>이는 JPA 2.1에 추가된 기능인데 <strong>자바의 타입 캐스팅</strong>과 비슷하다. 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다. </p>
<h3 id="12-사용자-정의-함수-호출">(12) 사용자 정의 함수 호출</h3>
<p>JPA 2.1부터 사용자 정의 함수를 지원한다.
예)</p>
<pre><code class="language-java">function_invocation ::= FUNCTION (function_name {, function_arg}*)</code></pre>
<pre><code class="language-java">select function(&#39;group_concat&#39;, i.name) from Item i</code></pre>
<p>하이버네이트 구현체 사용시에는 방언 클래스를 사용해 구현하고 사용할 데이터베이스 함수를 미리 등록해야함.</p>
<h3 id="13-기타-정리">(13) 기타 정리</h3>
<ul>
<li>enum은 비교연산만 지원</li>
<li>임베디드 타입은 비교를 지원하지 않음</li>
</ul>
<h4 id="empty-string">EMPTY STRING</h4>
<p>JPA 표준은 &#39;&#39;을 길이가 0인 empty string으로 정했지만, 데이터베이스에 따라 &#39;&#39;를 null로 사용하기도 하니 확인하고 사용해야함.</p>
<h4 id="null">NULL</h4>
<ul>
<li>조건을 만족하는 데이터가 하나도 없을 때</li>
<li>NULL은 안ㄹ 수 없는 값으로 모든 수학적 계산의 결과가 NULL이 됨.</li>
<li>Null == Null은 알 수 없는 값.</li>
<li>Null is Null은 참.</li>
</ul>
<h3 id="14-엔티티-직접-사용">(14) 엔티티 직접 사용</h3>
<h4 id="기본키-값">기본키 값</h4>
<p>객체 인스턴스는 참조값으로 식별하고 테이블 로우는 기본키 값으로 식별.
따라서 JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본키 값을 사용함.</p>
<pre><code class="language-java">select count(m.id) from Member m //엔티티 아이디를 사용
select count(m) from Member m //엔티티 직접 사용</code></pre>
<p>아래처럼 엔티티의 별칭을 직접 넘겨주게되면 JPQL이 SQL로 변환될 때, 해당 엔티티의 기본키를 사용한다.</p>
<h4 id="외래키-값">외래키 값</h4>
<p>특정 팀에 소속된 회원을 찾기위해 팀의 기본키 값을 파라미터로 하여 탐색을 하면, <code>m.team</code>(멤버의 팀) 부분이 외래키와 매핑되어있기에 회원과 팀 사이에 묵시적 조인 없이 데이터를 가져올 수 있다. </p>
<h3 id="15-named-쿼리-정적-쿼리">(15) Named 쿼리: 정적 쿼리</h3>
<p>JPQL 쿼리는 크게 동적 쿼리와 정적 쿼리로 나눌 수 있음.</p>
<ul>
<li><strong>동적 쿼리</strong>: em.createQuery(&quot;select ...&quot;)처럼 JPQL을 문자로 완성해 직접 넘기는 것을 동적 쿼리라함.</li>
<li><strong>정적 쿼리</strong>: 미리 정의한 쿼리에 이름을 부여해 필요할 때 사용할 수 있는데 이것을 Named 쿼리라함. 이는 한 번 정의하면 변경할 수 없는 정적인 쿼리.</li>
</ul>
<p>Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해둔다. 따라서 오류를 빨리 확인할 수 있고, 사용 시점에 파싱된 결과를 재사용하므로 성능상의 이점도 존재한다. 또한 Named 쿼리는 변하지 않는 정적 SQL이 생성되므로 데이터베이스 조회 성능 최적화에도 도움이 됨.</p>
<p>Named 쿼리는 <code>@NamedQuery</code> 어노테이션을 사용해 자바 코드에 작성하거나 XML 문서에 작성할 수 있음.</p>
<h4 id="named-쿼리를-어노테이션에-정의">Named 쿼리를 어노테이션에 정의</h4>
<p>Named 쿼리는 <strong>쿼리에 이름을 부여해 사용하는 방법</strong>. </p>
<pre><code class="language-java">@Entity
@NamedQuery {
    name = &quot;Member.findByUsername&quot;,
    query = &quot;select m from Member m where m.username = :username&quot;)
} public class Member {

}</code></pre>
<p><code>@NamedQuery.name</code>에 쿼리 이름을 부여하고, <code>@NamedQuery.query</code>에 사용할 쿼리를 입력.</p>
<p>하나의 엔티티에 2개 이상의 Named 쿼리를 정의하려면 <code>@NamedQueries</code> 어노테이션을 사용하면 됨.</p>
<ul>
<li><p><code>@NamedQuery</code> 어노테이션</p>
<ul>
<li>lockMode: 쿼리 실행 시 락을 건다.</li>
<li>hints: JPA 구현체에게 제공하는 힌트로 2차 캐시를 다룰 때 사용하거나 한다.</li>
</ul>
</li>
</ul>
<h4 id="named-쿼리를-xml에-정의">Named 쿼리를 XML에 정의</h4>
<p>JPA에서 어노테이션으로 작성할 수 있는 것은 XML로도 작성할 수 있음. 어노테이션을 사용하는 것이 직관적이고 편리한편이지만, Named 쿼리 작성 시에는 XML을 사용하는 것이 직관적이고 편리함.</p>
<p>xml에 정의한 후에는 MEAT-INF/persistence.xml에 코드를 추가해 주어야함.</p>
<pre><code class="language-java">&lt;persistence-unit name=&quot;jpabook&quot;&gt;
    &lt;mapping-file&gt;META-INF/ormMember.xml&lt;/mappingfile&gt;</code></pre>
<hr>
<h2 id="103-criteria">10.3 Criteria</h2>
<p>Criteria 쿼리는 JPQL을 자바 코드로 작성하도록 도와주는 빌더 클래스 API. 이를 사용하면 문자가 아닌 코드로 JPQL을 작성하기 때문에 문법 오류를 <strong>컴파일 단계에서</strong> 잡을 수 있고 , 문자 기반의 JPQL보다 동적 쿼리를 안전하게 생성한다는 장점이 존재. 다만 코드가 복잡하고 장황해 이해가 힘들다는 단점 역시 존재한다.</p>
<h3 id="1-criteria-기초">(1) Criteria 기초</h3>
<p>예)</p>
<pre><code class="language-java">//JPQL: select m from Member m

CriteriaBuilder cb = em.getCriteriaBuilder(); //Criteria 쿼리 빌더
CriteriaQuery&lt;Member&gt; cq =cb.createQuery(Member.class)

Root&lt;Member&gt; m = cq.from(Member.class);
cq.select(m);

TypedQuery&lt;Member&gt; query = em.createQuery(cq);
List&lt;Member&gt; members = query.getResultList();</code></pre>
<p>모든 회원 엔티티를 조회하는 JPQL의 Criteria로 작성.</p>
<ul>
<li>Criteria 쿼리 생성을 위해서는 Criteria 빌더가 필요. 빌더는 EntityManger 또는 EntityManagerFactory에서 얻을 수 있음.</li>
<li>Criteria 쿼리 빌더에서 Criteria 쿼리르 새엇ㅇ. 이때 <strong>반환 타입 지정 가능</strong>.</li>
<li>FROM 절을 생성. 반환된 값은 Criteria에서 사용하는 특별한 별칭. m을 조회의 시작이라는 의미로 <strong>쿼리 루트</strong>라함.</li>
<li>SELECT 절을 생성.</li>
</ul>
<p>아래는 WHERE절과 ORDER BY를 넣어주는 경우.</p>
<pre><code class="language-java">//JPQL : select m from Member m
//         where m.username = &#39;회원1&#39;
//         order by m.age desc

CriteriaBuilder cb = em.getCriteriaBuilder(); //Criteria 쿼리 빌더 생성

//Criteria 생성, 반환 타입 지정
CriteriaQuery&lt;Member&gt; cq = cb.createQuery(Member.class);

Root&lt;Member&gt; m = cq.from(Member.class); //FROM절-쿼리루트 반환

//검색 조건 정의
Predicate usernameEqual = cb.equal(m.get(&quot;username&quot;), &quot;회원1&quot;);

//정렬 조건 정의
javax.persistence.criteria.Order ageDesc = cb.desc(m.get(&quot;age&quot;));

//쿼리 생성
cq.select(m) //SELECT절
  .where(usernameEqual) //WHERE절 생성
  .orderBy(ageDesc); //ORDER BY절 생성


TypedQuery&lt;Member&gt; query = em.createQuery(cq);
List&lt;Member&gt; members = query.getResultList();</code></pre>
<p><strong>쿼리 루트와 별칭</strong></p>
<pre><code> **쿼리 루트와 별칭**
- Root&lt;Member&gt; m = cq.from(Member.class): 여기서 m이 쿼리 루트.
  조회의 시작점이 되며, Criteria에서 사용되는 특별한 별칭. 이 별칭은 엔티티에서만 부여할 수 있음.

Criteria는 코드로 JPQL을 완성하는 도구이기에 경로 표현식 역시 존재한다.
- `m.get(&quot;username&quot;)`은 JPQL의 `m.username`과 같음.
- `m.get(&quot;team&quot;).get(&quot;name&quot;)`는 JPQL의 `m.team.name`과 같음.</code></pre><h3 id="2-criteria-쿼리-생성">(2) Criteria 쿼리 생성</h3>
<p>CriteriaBuilder.createQuery() 메소드를 사용해 Criteria 쿼리를 생성하면 됨.</p>
<pre><code class="language-java">public interface CriteriaBuilder {
    CriteriaQuery&lt;ObjecT&gt; createQuery(); //조회값 반환 타입

    &lt;T&gt; CriteriaQuery&lt;T&gt; createQuery(Class&lt;T&gt; resultClass);
    CriteriaQuery&lt;Tuple&gt; createTupleQuery(); //조회값 반환 타입 Tuple
}</code></pre>
<p>Criteria 쿼리 생성 시에는 <strong>파라미터로 쿼리 결과에 대한 반환 타입을 지정</strong>할 수 있음. CriteriaQuery 생성 시 Member.class를 반환 타입으로 지정한다면, em.createQuery(cq)에서 반환 타입을 지정하지 않아도 됨.</p>
<ul>
<li>반환타입을 지정할 수 없거나 반환 타입이 둘 이상일 때는 타입을 지정하지 않고 Object로 반환을 받으면 됨.</li>
<li>반환 타입이 둘 이상이면 Object[]를 사용하는 것이 편리하다.</li>
<li>반환 타입을 튜플로 받고 싶을 때에서는 튜플을 사용하면 <code>CreateQuery&lt;Tuple&gt;</code>을 사용하면 된다.</li>
</ul>
<h3 id="3-조회">(3) 조회</h3>
<p>SELECT 절을 만드는 부분인 <code>select()</code>에 관련한 내용이다.</p>
<pre><code class="language-java">public interface CriteriaQuery&lt;T&gt; extends AbstractQuery&lt;T&gt; {
    //한 건 지정
    CriteriaQuery&lt;T&gt; select(Selection&lt;? extends T&gt; selection);

    //여러 건 지정
    CriteriaQuery&lt;T&gt; multiselct(Selection&lt;?&gt; ... selection)s;

    //여러 건 지정
    CriteriaQuery&lt;T&gt; multiselect(List&lt;Selection&lt;?&gt;&gt; selectionList);
}</code></pre>
<h4 id="조회-대상을-한-건-여러-건-지정">조회 대상을 한 건, 여러 건 지정</h4>
<p>select에 조회 대상을 하나만 지정할 때는 아래처럼 작성함.</p>
<pre><code class="language-java">cq.select(m);</code></pre>
<p>조회 대상을 여러 건 지정하려면 multiselect를 사용한다.</p>
<pre><code class="language-java">//JPQL: select m.username, m.age
cq.multiselect(m.get(&quot;username&quot;), m.get(&quot;age&quot;));</code></pre>
<p>여러 건을 지정할 때는 <code>cb.array</code>를 사용해도 된다.</p>
<pre><code class="language-java">CriteriaBuilder cb =em.getCriteriaBuilder();
//JPQL: select m.username, m.age
cq.select (cb.array(m.get(&quot;username&quot;), m.get(&quot;age&quot;)) );</code></pre>
<h4 id="distinct">DISTINCT</h4>
<p>distinct는 select, multiselect 뒤에 distinct(true)를 사용하면 됨.</p>
<pre><code class="language-java">//JPQL: select distinct m.username, m.age
cq.multiselect(m.get(&quot;username&quot;), m.get(&quot;age&quot;)).distinct(true);</code></pre>
<h4 id="new-construct">NEW, construct()</h4>
<p>JPQL에서 select new 생성자() 구문을 Criteria에서는 cb.construct(클래스 타입, ...)로 사용함.</p>
<pre><code class="language-java">&lt;Y&gt; CompoundSelection&lt;Y&gt; construct(Class&lt;Y&gt; resultClass,
Selection&lt;?&gt; ... selections)</code></pre>
<pre><code class="language-java">//JPQL: select new jpabook.domain.MemberDTO(m.username, m.age)
//from Member m

CriteriaQuery&lt;MemberDTO&gt; cq = cb.createQuery(MemberDTO.class);
Root&lt;Member&gt; m = cq.from(Member.class);

cq.select(cb.construct(MemberDTO.class), m.get(&quot;username&quot;),
m.get(&quot;age&quot;)));

TypedQuery&lt;MemberDTO&gt; query = em.createQuery(cq);
List&lt;MemberDTO&gt; resultList = query.getResultList();</code></pre>
<h4 id="튜플">튜플</h4>
<p>Criteria는 Map과 비슷한 튜플이라는 특별한 반환 객체를 제공한다.</p>
<p>튜플을 사용하기 위해서는 <code>cb.createTupleQuery()</code> 또는 <code>cb.createQuery(Tuple.class)</code>로 Criteria를 생성.</p>
<p>튜플은 이름 기반이기에 순서 기반인 Object[]보다 안전하며, <code>tuple.getElements()</code> 같은 메소드를 사용해 현재 튜플의 별칭과 자바 타입 역시 조회할 수 있다.</p>
<p>튜플을 사용해 <strong>엔티티도 조회</strong>할 수 있으면 튜플 사용시에는 <strong>별칭을 필수</strong>로 주어야한다.</p>
<h3 id="4-집합">(4) 집합</h3>
<h4 id="group-by">GROUP BY</h4>
<pre><code class="language-java">cq.groupBy(m.get(&quot;team&quot;).get(&quot;name&quot;));</code></pre>
<h4 id="having">HAVING</h4>
<pre><code class="language-java">cq.multiselect(m.get(&quot;team&quot;).get(&quot;name&quot;), maxAge, minAge)
    .groupBy(m.get(&quot;team&quot;).get(&quot;name&quot;))
    .having(cb.gt(minAge, 10)); //HAVING</code></pre>
<h3 id="5-정렬">(5) 정렬</h3>
<p>정렬 조건 역시 Criteria 빌더를 통해 생성할 수 있다.</p>
<pre><code class="language-java">//cb.desc(...) 또는 cb.asc()로 생성

cq.select(m)
    .where(agetGt)
    .orderBy(cb.desc(m.get(&quot;age&quot;)));</code></pre>
<h3 id="6-조인">(6) 조인</h3>
<p>조인 <code>join()</code> 메소드와 JoinType 클래스를 사용한다.</p>
<pre><code class="language-java">public enum JoinType {
    INNER,
    LEFT,
    RIGHT
}</code></pre>
<p>예)</p>
<pre><code class="language-java">Join&lt;Member, Team&gt; t = m.join(&quot;team&quot;, JoinType.INNER); //내부조인</code></pre>
<ul>
<li>외부조인의 경우 <code>JoinType.LEFT</code>로 설정</li>
<li>FETCH JOIN의 경우 <code>m.fetch(&quot;team&quot;, JoinType.LEFT);</code>로 사용.</li>
</ul>
<h3 id="7-서브-쿼리">(7) 서브 쿼리</h3>
<h4 id="간단한-서브-쿼리">간단한 서브 쿼리</h4>
<p>메인 쿼리와 서브 쿼리 간의 관련이 없는 단순한 서브쿼리</p>
<pre><code class="language-java">CriteriaBuilder cb =em.getCriteriaBuilder();
CriteriaQuery&lt;Member&gt; mainQuery = cb.createQuery(Member.class);

//서브쿼리
Subquery&lt;Double&gt; subQuery = mainQuery.subquery(Double.class);
Root&lt;Member&gt; m2 = subQuery.from(Member.class);
subQuery.select(cb.avg(m2.&lt;Integer&gt;get(&quot;age&quot;)));</code></pre>
<h4 id="상호-관련-서브-쿼리">상호 관련 서브 쿼리</h4>
<p>메인 쿼리와 서브 쿼리 간의 서로 관련이 있을 때의 서브 쿼리는 <strong>서브쿼리에서 메인 쿼리의 정보를 사용하려면 메인 쿼리에서 사용한 별칭을 얻어야함</strong>. 서브 쿼리는 메인 쿼리의 Root나 Join을 통해 생성된 별칭을 받아 사용한다.</p>
<pre><code class="language-java">.where(cb.equal(subM.get(&quot;username&quot;), m.get(&quot;username&quot;)));</code></pre>
<h3 id="8-in-식">(8) IN 식</h3>
<p>IN식은 Criteria 빌더에서 <code>in {...}</code> 메소드를 사용함</p>
<pre><code class="language-java">CriteriaBuilder cb =em.getCriteriaBuilder();
CriteriaQuery&lt;Member&gt; query = cb.createQuery(Member.class);
Root&lt;Member&gt; m = cq.from(Member.class);

cq.select (m)
  .where(cb.in(m.get(&quot;username&quot;))
  .value(&quot;회원1&quot;)
  .value(&quot;회원2&quot;));</code></pre>
<h3 id="9-case-식">(9) CASE 식</h3>
<p>CASE 식에는 <code>selectCase()</code> 메소드와 <code>when()</code>, <code>otherwise()</code> 메소드를 사용함</p>
<pre><code class="language-java">Root&lt;Member&gt; m = cq.from(Member.class);

cq.multiselect(
    m.get(&quot;username&quot;),
    cb.selectCase()
        .when(cb.ge(m.&lt;Integer&gt;get(&quot;age&quot;), 60), 600)
        .when(cb.le(m.&lt;Integer&gt;get(&quot;age&quot;), 15), 500)
        .otherwise(100) );</code></pre>
<h3 id="10-파라미터-정의">(10) 파라미터 정의</h3>
<p>JPQL에서 <strong>:PARAM1</strong>처럼 파라미터를 정의했듯이 Criteria도 파라미터를 정의할 수 있음.</p>
<pre><code class="language-java">CriteriaBuilder cb =em.getCriteriaBuilder();
CriteriaQuery&lt;Member&gt; query = cb.createQuery(Member.class);
Root&lt;Member&gt; m = cq.from(Member.class);

//정의
cq.select(m)
    .where(cb.equal(m.get(&quot;username&quot;), cb.parameter(String.class, &quot;usernameParam&quot;)));

List&lt;Member&gt; resultList = em.createQuery(cq)
    .setParameter(&quot;usernameParam&quot;, &quot;회원1&quot;) //바인딩
    .getResultList();</code></pre>
<ul>
<li><code>cb.parameter(타입, 파라미터 이름)</code> 메소드를 사용해 파라미터를 정의</li>
<li><code>setParameter(&quot;usernameParam&quot;, &quot;회원1&quot;)</code>을 사용해 파라미터에 사용할 값을 바인딩.</li>
</ul>
<h3 id="11-네이티브-함수-호출">(11) 네이티브 함수 호출</h3>
<p>네이티브 SQL 함수 호출을 위해서는 <code>cb.function()</code> 메소드를 사용하면 됨.</p>
<pre><code class="language-java">Root&lt;Member&gt; m = cq.from(Member.class);
Expression&lt;Long&gt; function = cb.function(&quot;SUM&quot;, Long.class, m.get(&quot;age&quot;));
cq.select(function);</code></pre>
<h3 id="12-동적-쿼리">(12) 동적 쿼리</h3>
<p><strong>동적 쿼리</strong>: 다양한 검색 조건에 따라 실행 시점에 쿼리를 생성하는 것을 말함.</p>
<p>동적 쿼리는 문자 기반인 JPQL보다는 코드 기반인 Criteria로 작성하는 것이 더 편리함.</p>
<p>Criteria로 동적 쿼리를 구성하면 최소 공백이나, where, and의 위치로 인한 에러가 발생하지 않기에 동적 쿼리 한정 Criteria 작성이 편리할 수 있다. 다만, 장황하고 복잡하다는 단점은 여전히 존재한다.</p>
<h3 id="13-함수-정리">(13) 함수 정리</h3>
<p>Criteria는 JPQL의 빌더 역할을 하기에 <strong>JPQL 함수를 코드로 지원</strong>한다.
대부분은 CriteriaBuilder에 정의되어 있다.</p>
<p><strong>조건함수</strong>: and(), or(), not(), equal(), notEqual(), lt() = lessThan(), bewteen(), isTrue(), in(), not(in()) 등</p>
<p><strong>스칼라와 기타 함수</strong>: sum(), prod(), neg(), some(), any() 등</p>
<p><strong>집합 함수</strong>: avg(), max(), min(), count() 등</p>
<h3 id="14-criteria-메타-모델-api">(14) Criteria 메타 모델 API</h3>
<p>Criteria는 코드 기반이기에 컴파일 시점에 오류를 발견할 수 있다. 다만 <code>m.get(&quot;age&quot;)</code>라는 코드를 작성 할 때 &#39;age&#39;는 문자이기에 이런 부분을 잘못 적는 것은 컴파일 시점에 에러를 발견하지 못한다.</p>
<p>이런 부분까지 코드로 작성하기 위해 <strong>메타 모델 API</strong>를 사용한다.</p>
<pre><code class="language-java">cq.select(m)
    .where(cb.gt(m.get(Member_.age), 20))
    .orderBy(cb.desc(m.get(Member_.age)));</code></pre>
<p>다만 위를 적용하기 위해서는 <strong>메타 모델 클래스</strong>가 필요하다.</p>
<pre><code class="language-java">@GeneratedValue(value = &quot;org.hibernate.jpmodelgen.JPAMetaModelEntityProcessor&quot;)
@StaticMetamodel(Member.class)
public abstract class Member {
    public static volatile SingularAttribute&lt;Member, Long&gt; id;
    public static volatile SingularAttribute&lt;Member, String&gt; username;
    public static volatile SingularAttribute&lt;Member, Integer&gt; age;
    public static volatile SingularAttribute&lt;Member,Order&gt; orders;
    public static volatile SingularAttribute&lt;Member,Team&gt; team;
}</code></pre>
<p>위의 클래스를 표준(CANONICAL) 메타 모델 클래스라 하며 줄여서 메타 모델이라 한다.</p>
<p>Member_ 메타 모델 클래스는 Member 엔티티를 기반으로 만들어야한다. 다만, 개발자가 직접 이런 복잡한 코드를 작성하지는 않고, 코드 자동 생성기가 엔티티 클래스를 기반으로 메타 모델 클래스들을 만들어준다.</p>
<h4 id="코드-생성기-설정">코드 생성기 설정</h4>
<p>코드 생성기는 보통 Maven, Gradle 같은 빌드 도구들을 사용해서 실행한다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 8주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-8%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-8%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sat, 16 May 2026 10:29:53 GMT</pubDate>
            <description><![CDATA[<h1 id="09장-값-타입">09장. 값 타입</h1>
<p>JPA의 데이터 타입</p>
<ul>
<li><p>엔티티 타입:</p>
<ul>
<li><code>@Entity</code>로 정의하는 객체</li>
<li>식별자를 통해 지속해서 추적 가능</li>
</ul>
</li>
<li><p>값 타입: </p>
<ul>
<li>int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입 또는 객체</li>
<li>식별자가 없고 문자/숫자같은 속성만 있어 추적이 불가능함.</li>
</ul>
</li>
</ul>
<blockquote>
<h4 id="값-타입의-종류">값 타입의 종류</h4>
<p><strong>기본값 타입</strong>: 자바 기본 타입(int, double), 래퍼 클래스(Integer), String
<strong>임베디드 타입</strong>: 복합 값 타입
<strong>컬렉션 값 타입</strong></p>
</blockquote>
<h2 id="91-기본값-타입">9.1 기본값 타입</h2>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;
    private int age;
}</code></pre>
<h4 id="값-타입">값 타입</h4>
<ul>
<li>Member의 String, int가 <strong>값 타입</strong>에 해당 </li>
<li>Member 엔티티는 id라는 식별자 값을 가지고, 생명주기 역시 있지만, 값 타입인 name, age속성은 식별자값도 없고 생명주기 역시 회원 엔티티에 의존하게 된다.</li>
<li>회원 엔티티 인스턴스를 제거하게되면 name, age 역시 사라지게 된다. </li>
<li>값 타입은 공유해서는 안된다.
  예) 다른 회원 엔티티의 이름을 변경하는데 나의 이름까지 변경됨.</li>
</ul>
<hr>
<h2 id="92-임베디드-타입복합-타입">9.2 임베디드 타입(복합 타입)</h2>
<p><strong>임베디드 타입</strong>: 새로운 값 타입을 정의해서 사용하는 것을 말함.</p>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @Temporal(TemporalType.DATE) java.util.Date startDate;
    @Temporal(TemporalType.DATE) java.util.Date endDate;

    private String city;
    private String street;
    private String zipcode;
}</code></pre>
<p>위의 회원 엔티티는 단순히 정보를 풀어둔 것에 불과한다. 회원이 상세한 데이터를 그대로 가지고 있는 상태는 <strong>객체지향적이지 않아</strong> 응집력을 떨어뜨린다.</p>
<p>해결: 근무기간, 주소 타입 같은 타입을 가지도록 <strong>임베디드 타입</strong>을 사용한다.</p>
<pre><code class="language-java">@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @Embedded Period workPeriod;
    @Embedded Address homeAddress;
}

//기간 임베디드 타입
@Embeddable
public class Period {

    @Temporal (TemporalType.DATE) java.util.startDate;
    @Temporal (TemporalType.DATE) java.util.Date endDate;

    public boolean isWork (Date dat) {}
}

//주소 임베디드 타입
@Embeddable
public class Address {
    @Column (name=&quot;city&quot;) //매핑할 컬럼 정의 가능
    private String city;

    private String city;
    private String zipcode;
}</code></pre>
<ul>
<li><code>startDate</code>와 <code>endDate</code>를 합해서 Period(기간) 클래스를 만듦</li>
<li><code>city</code>, <code>street</code>, <code>zipcode</code>를 합해 Address(주소) 클래스를 만듦</li>
</ul>
<p>새로 정의한 값 타입들을 <strong>재사용</strong>을 할 수 있고 <strong>응집도 역시 높음</strong>.
해당 값 타입만 사용하는 메소드 역시 만들 수 있음.</p>
<h4 id="임베디드-타입을-사용하기-위한-어노테이션">임베디드 타입을 사용하기 위한 어노테이션</h4>
<ul>
<li><code>@Embeddable</code>: 값 타입을 정의하는 곳에 표시</li>
<li><code>@Embedded</code>: 값 타입을 사용하는 곳에 표시</li>
</ul>
<p>또한 임베디드 타입은 <strong>기본 생성자가 필수</strong>적이다. 임베디드 타입을 포함한 모든 값 타입은 엔티티의 생명주기에 의존하기 때문에 UML로 엔티티-임베디드 타입의 관계를 표현하면 <strong>컴포지션 관계</strong>가 됨.</p>
<h3 id="1-임베디드-타입과-테이블-매핑">(1) 임베디드 타입과 테이블 매핑</h3>
<p>임베디드 타입 = 엔티티의 값. 따라서 값이 속한 엔티티의 테이블에 매핑.
임베디드 타입은 <strong>객체와 테이블을 세밀하게 매핑</strong>하는 것이 가능하다. 잘 설계한 ORM 애플리케이션의 경우는 매핑한 테이블의 수보다 클래스의 수가 더 많다.</p>
<p>ORM을 사용하지 않고 개발하게되면 테이블 컬럼과 객체 필드를 대부분 1:1로 매핑하게 된다. 근무기간이나 주소같은 값 타입 클래스를 만들어 개발하기에는 SQL을 직접 다룰 때 테이블 하나에 여러 클래스를 매핑하는 등 복잡한 과정이 발생하기 때문이다. 다만, ORM을 사용해 이런 반복적인 작업을 JPA에 위임하고 객체지향 모델 설계에 집중할 수 있다.</p>
<h3 id="2-임베디드-타입과-연관관계">(2) 임베디드 타입과 연관관계</h3>
<p>임베디드 타입의 경우 <strong>값 타입을 포함하거나 엔티티를 참조</strong>할 수 있다. </p>
<pre><code class="language-java">@Entity
public class Member {
    @Embedded Address address;
    @Embedded PhoneNumber phoneNumber;
}

@Embeddable
public class Address{
    String street;
    String city;
    String state;
    @Embedded Zipcode zipcode;
}

@Embeddable
public class Zipcode {
    String zip;
    String plusFour;
}

@Embeddable
public class PhoneNumber {
    String areaCode;
    String localNumber;
    @ManyToOne
    PhoneServiceProvider provider;
}

@Entity
public class PhoneServiceProvider {
    @Id String name;
}</code></pre>
<p>위를 보면 값 타입에 해당하는 Address가 값 타입인 Zipcode를 포함하고 있고, 값 타입 PhoneNumber가 엔티티 타입인 PhoneServiceProvider를 참조한다.</p>
<h3 id="3-attributeoverride-속성-재정의">(3) <code>@AttributeOverride</code>: 속성 재정의</h3>
<p>임베디드 타입에 정의한 매핑정보를 재정의하기 위해서는 엔티티에 <code>@AttributeOverride</code>를 사용하면 된다. </p>
<p>예) 회원에게 주소가 하나 더 필요할 때</p>
<pre><code class="language-java">@Entity
public class Member {
    @Id @GeneratedValue
    privaet Long id;

    private String name;

    @Embedded Address homeAddress;
    @Embedded Address companyAddress;
}

위의 코드에서 주소를 추가하는 것은 쉽지만 문제는 **테이블에 매핑하는 컬럼명이 중복**된다. 이때는 `@AttributeOverrides`를 사용해 매핑정보를 재정의해야한다.

```java
@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @Embedded Address homeAddress;

    @Embedded 
    @AttributeOverrides ({
        @AttributeOverride(name=&quot;city&quot;, column=@Column(name=&quot;COMPANY_CITY&quot;)),
        @AttributeOverride(name=&quot;street&quot;, column=@Column(name=&quot;COMPANY_STREET&quot;)),
        @AttributeOverride(name=&quot;zipcode&quot;, column=@Column(nmae=&quot;COMPANY_ZIPCODE&quot;)
    })
    Address companyAddress;
}</code></pre>
<p><code>@AttributeOverride</code>를 사용했을 때, 어노테이션을 너무 많이 사용하여 엔티티 코드가 지저분해진다는 점이 있다. 하지만 한 엔티티에 같은 임베디드 타입을 중복해서 사용하는 일은 많지 않다.</p>
<h3 id="4-임베디드-타입과-null">(4) 임베디드 타입과 null</h3>
<p>임베디드 타입이 null일 경우에는 <strong>매핑한 컬럼 값이 모두 null</strong>이 된다.</p>
<pre><code class="language-java">member.setAddress(null);
em.persist(member);</code></pre>
<hr>
<h2 id="93장-값-타입과-불변-객체">9.3장 값 타입과 불변 객체</h2>
<p>값 타입은 복잡한 객체를 단순화하기 위해 만든 개념.</p>
<h3 id="1-값-타입-공유-참조">(1) 값 타입 공유 참조</h3>
<p>임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하게되면 위험하다.</p>
<pre><code class="language-java">member1.setHomeAddress(new Address(&quot;OldCity&quot;));
Address address = member1.getHomeAddress();

address.setCity(&quot;NewCity&quot;);
member2.setHomeAddress(address);</code></pre>
<p>문제점: 회원2에 새로운 주소를 할당할 때 회원1의 주소를 그대로 참조하여 사용. -&gt; 회원2의 주소만 NewCity로 바뀌는 것이 아니라 회원1의 주소 역시 NewCity로 변경되어버림.</p>
<p>이유: 회원1과 회원2가 같은 인스턴스를 참조하기 때문에 영속성 컨텍스트는 회원1과 회원2 둘 모두 city 속성이 변경되었다고 판단하여 각각을 UPDATE SQL을 실행한다. <strong>이런 공유 참조로 인한 버그는 찾아내기가 어렵다.</strong> </p>
<p><strong>부작용</strong>: 뭔가 수정했는데 전혀 예상하지 못한 곳에서 문제가 발생하는 것. 방지를 위해서는 값을 복사해 사용하면 된다.</p>
<h3 id="2-값-타입-복사">(2) 값 타입 복사</h3>
<p>값 타입의 실제 인스턴스 값을 공유하는 것은 위험하기에 <strong>값을 복사해서 사용</strong>해야한다.</p>
<pre><code class="language-java">member1.setHomeAddress(new Address(&quot;OldCity&quot;));
Address address = member1.getHomeAddress();

Address newAddress = address.clone();

newAddress.setCity(&quot;NewCity&quot;);
member2.setHomeAddress(newAddress);</code></pre>
<p>회원2에 새로운 주소를 할당하기 위해 <code>clone()</code> 메소드를 만들었는데, 이 메소드는 스스로를 복사해 반환하도록 구현되어있다. 즉, 회원1의 주소 인스턴스를 복사하여 사용한다.</p>
<p>위의 코드는 의도대로 회원2의 주소만 NewCity로 변경하게된다. 또한 영속성 컨텍스트 역시 회원2의 주소만 변경된 것으로 판단하여 회원2에 대해서만 UPDATE SQL을 실행하게된다.</p>
<p>이렇게 <strong>값을 항상 복사해서 사용</strong>하게되면 공유 참조로 인한 부작용을 피할 수 있다. </p>
<h4 id="문제1">[문제1]</h4>
<p>하지만, 문제는 <strong>임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입</strong>이라는 점이다.</p>
<p>자바의 기본 타입은 항상 값을 복사해서 전달하지만, 객체 타입의 경우에는 항상 <strong>참조값</strong>을 전달한다. 즉, 두 객체가 <strong>같은 인스턴스를 공유 참조</strong>하는 일이 발생한다.</p>
<h4 id="해결1">[해결1]</h4>
<p>객체를 대입할 때마다 <strong>인스턴스를 복사해 대입하면 공유 참조를 피할 수 있다.</strong> </p>
<hr>
<h4 id="문제2">[문제2]</h4>
<p>복사하지 않고 원본의 참조값을 직접 넘기는 것을 막을 수 있는 방법이 없다. 자바에서는 대입하려는 것이 값 타입인지의 여부를 신경 쓰지 않고, 자바 기본 타입인 경우 값을 복사하고 객체일 경우 참조를 넘긴다.</p>
<h4 id="해결2">[해결2]</h4>
<p>즉, <strong>객체의 공유 참조는 피할 수 없다</strong>. 따라서 해결책이 필요한데 가장 단순한 방법은 객체의 값을 수정하지 못하게 막으면 된다.</p>
<p>예를 들면 Address 객체의 setCity() 같은 수정자 메소드를 모두 제거하는 것이다. 이렇게 되면 공유 참조를 해도 값을 변경하지 못하기에 부작용의 발생을 막을 수 있다.</p>
<h3 id="3-불변-객체">(3) 불변 객체</h3>
<p><strong>객체를 불변하게 만들면 값을 수정할 수 없기에 부작용을 원천 차단할 수 있다. 따라서 값 타입은 될 수 있으면 불변 객체로 설계해야한다.</strong></p>
<p>불변 객체: 한 번 만들면 절대 변경할 수 없는 객체로 조회는 가능하지만 수정은 불가능.</p>
<p>하지만 불변 객체 역시 객체이기에 인스턴스의 참조 값 공유를 피할 수는 없음. 다만, 참조값을 공유하더라도 인스턴스의 값을 수정할 수 없기에 부작용은 발생하지 않음.</p>
<p>구현방법: 생성자로만 값을 설정하고 수정자를 만들지 않는다.</p>
<p>예) Address</p>
<pre><code class="language-java">@Embeddable
public class Address {
    private String city;

    protected Address () {this.city = city}

    //접근자(Getter)는 노출.
    public String getCity() {return city;}
}</code></pre>
<p>불변 객체의 사용</p>
<pre><code class="language-java">Address address = member1.getHomeAddress();
Address newAddress = new Address(address.getCity());
member2.setHomeAddress(newAddress);</code></pre>
<p>위의 Address는 불변객체로 값을 수정할 수 없기에 공유해도 부작용이 발생하지 않는다. 만약 값을 수정해야한다면, <strong>새로운 객체</strong>를 만들어 사용해야한다.</p>
<p>+) Integer, String은 자바가 제공하는 대표적인 불변 객체.</p>
<hr>
<h2 id="94-값-타입의-비교">9.4 값 타입의 비교</h2>
<pre><code class="language-java">int a = 10;
int b = 10;

Address a = new Address(&quot;서울시&quot;, &quot;종로구&quot;, &quot;1번지&quot;);
Address b = new Address(&quot;서울시&quot;, &quot;종로구&quot;, &quot;1번지&quot;);</code></pre>
<ul>
<li>int a의 숫자 10과 int b의 숫자 10은 같다고 표현</li>
<li>Address a와 Address b는 같다고 표현</li>
</ul>
<p>자바가 제공하는 객체 비교</p>
<ul>
<li><strong>동일성 비교</strong>: 인스턴스의 참조값을 비교하며 <code>==</code>를 사용.</li>
<li><strong>동등성 비교</strong>: 인스턴스의 값을 비교, <code>equals()</code> 사용.</li>
</ul>
<p>위의 코드에서 Address 값 타입을 <code>a==b</code>로 동일성 비교를 하는 경우 둘은 서로 다른 인스턴스 이기에 결과가 거짓이된다.</p>
<p>다만, 값 타입의 경우는 <strong>인스턴스가 달라도 그 안의 값이 같으면 같은 것으로 봐야한다.</strong> 그렇기에 값 타입 비교를 위해서는 <strong>동등성 비교</strong>를 해야한다. 이를 위해서는 Address의 <code>equals()</code> 메소드를 재정의해야한다.</p>
<p>값 타입의 <code>equals()</code> 메소드를 재정의할 때는 보통 모든 필드의 값을 비교하도록 구현한다.</p>
<hr>
<h2 id="95-값-타입-컬렉션">9.5 값 타입 컬렉션</h2>
<p>값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 <code>@ElementCollection</code>, <code>@CollectionTable</code> 어노테이션을 사용하면 된다.</p>
<pre><code class="language-java">@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    @Embedded
    private Address homeAddress;

    @ElementCollection
    @CollectionTable(name = &quot;FAVORITE_FOODS&quot;,
        joinColumns = @JoinColumn(name = &quot;MEMBER_ID&quot;))
    @Column(name = &quot;FOOD_NAME&quot;)
    private Set&lt;String&gt; favoriteFoods = new HashSet&lt;String&gt;();

    @ElementCollection
    @CollectionTable(name = &quot;ADDRESS&quot;, joinColumns = @JoinColumn(name = &quot;MEMBER_ID&quot;))
    private List&lt;Address&gt; addressHistory = new ArrayList&lt;Address&gt;();
}</code></pre>
<p>값 타입 컬렉션을 사용하는 <code>favoriteFoods</code>, <code>addressHistory</code>에 <code>@ElementCollection</code>을 지정하였다. </p>
<p><code>favoriteFoods</code>의 경우 기본값 타입인 String 컬렉션을 가진다. 이를 데이터베이스 테이블로 매핑해야하는데 관계형 데이터베이스의 테이블은 컬럼 안에 컬렉션을 포함할 수는 없다.</p>
<p>따라서 별도의 테이블을 추가하고 <code>@CollectionTable</code>를 사용해 추가한 테이블을 매핑해야한다. 또한 만약 <code>favoriteFoods</code>처럼 값으로 사용되는 컬럼이 하나일 때는 <code>@Column</code>을 사용해 컬럼명을 지정할 수 있다.</p>
<p>addressHistory는 임베디드 타입인 Address를 컬렉션으로 가진다. 이것 역시 마찬가지로 별도의 테이블을 사용해야한다. 테이블의 매핑정보는 <code>@AtrributeOverride</code>를 사용해서 재정의할 수 있음.</p>
<h3 id="1-값-타입-컬렉션-사용">(1) 값 타입 컬렉션 사용</h3>
<pre><code class="language-java">Member member = new Member();

//임베디드 값 타입
member.setHomeAddress(new Address(&quot;통영&quot;, &quot;몽돌해수욕장&quot;, &quot;660-123&quot;);

//기본값 타입 컬렉션
member.getFavoriteFoods().add(&quot;짬뽕&quot;);
member.getFavoriteFoods().add(&quot;짜장&quot;);
member.getFavoriteFoods().add(&quot;탕수육&quot;);

//임베디드 값 타입 컬렉션
member.getAddressHistory().add(new Address(&quot;서울&quot;, &quot;강남&quot;, &quot;123-123&quot;));
member.getAddressHistory().add(new Address(&quot;서울&quot;, &quot;강북&quot;, &quot;000-000&quot;));

em.persist(member);</code></pre>
<p>등록하는 코드를 보면 마지막에 member 엔티티만 영속화하였다. JPA는 이때 member 엔티티의 값 타입 역시 함께 저장한다. 실제 데이터베이스에 실행되는 INSERT SQL은 아래와 같다.</p>
<ul>
<li>member: INSERT SQL 1번</li>
<li>member.homeAddress: 컬렉션이 아닌 임베디드 값 타입이므로 회원테이블을 저장하는 SQL에 포함됨</li>
<li>member.favoriteFoods: INSERT SQL 3번</li>
<li>member.addressHistory: INSERT SQL 2번</li>
</ul>
<p>따라서 em.persist() 한 번으로 총 6번의 INSERT SQL을 실행하게 된다.</p>
<p><strong>값 타입 컬렉션 역시 조회 시 fetch 전략을 선택할 수 있는데, 기본은 LAZY이다.</strong></p>
<p>예) 지연로딩으로 모두 설정했다고 가정하고 아래킝 코드를 실행</p>
<pre><code class="language-java">Member member = em.find(Member.class, 1L);
Address homeAddress = member.getHomeAddress();
Set&lt;String&gt; favoriteFoods = member.getFavoriteFoods(); //LAZY

for (String favoriteFood : favoriteFoods) {
    System.out.println(&quot;favFood: &quot;+favoriteFood);
}

List&lt;Address&gt; addressHistory = member.getAddressHistory(); //LAZY

addressHistory.get(0);</code></pre>
<ul>
<li>member: 회원을 조회한다. 이때 임베디드 값인 homeAddress도 함께 조회한다. SELECT SQL을 한 번 호출.</li>
<li>member.homeAddress: 앞에서 회원 조회 시 같이 조회해둔다.</li>
<li>member.favoriteFoods: LAZY로 설정하여 실제 컬렉션 사용 시 SELECT SQL을 1번 호출.</li>
<li>member.addressHistory: LAZY로 설정하여 실제 컬렉션 사용 시 SELECT SQL을 1번 호출.</li>
</ul>
<p>예) 값 타입 컬렉션의 수정</p>
<pre><code class="language-java">Member member = em.find(Member.class, 1L);

member.setHomeAddress(new Address(&quot;새로운도시&quot;, &quot;신도시1&quot;, &quot;123456&quot;);

Set&lt;String&gt; favoriteFoods = member.getFavoriteFoods();
favoriteFoods.remove(&quot;탕수육&quot;);
favoriteFoods.add(&quot;치킨&quot;);

List&lt;Address&gt; addressHistory = member.getAddressHistory();
addressHistory.remove(new Address(&quot;서울&quot;, &quot;기존 주소&quot;, &quot;123-123-&quot;);
addressHistory.add(new Address(&quot;새로운 도시&quot;, &quot;새로운 주소&quot;, &quot;123-456&quot;);</code></pre>
<ul>
<li>임베디드 값 타입 수정: homeAddress 임베디드 값 타입은 MEMBER 테이블과 매핑하였으므로 MEMBER 테이블만 UPDATE하게된다. 사실 Member 엔티티를 수정하는 것과 같음.</li>
<li>기본값 타입 컬렉션 수정: 탕수육을 치킨으로 수정하기 위해서는 탕수육을 제거하고 치킨을 추가해야한다. 자바의 String 타입은 수정이 불가하다.</li>
<li>임베디드 값 타입 컬렉션 수정: 값 타입은 불변해야하기에 기존 주소를 삭제하고 새로운 주소를 등록하는 방식을 취한다. 값 타입은 <code>equals()</code>와 <code>hashcode</code>를 반드시 구현해야한다.</li>
</ul>
<h3 id="2-값-타입-컬렉션의-제약사항">(2) 값 타입 컬렉션의 제약사항</h3>
<p>엔티티는 식별자가 있어 값을 변경하여도 식별자로 데이터베이스에 저장된 원본 데이터를 쉽게 찾아 변경할 수 있지만, <strong>값 타입은 식별자 개념이 없고 단순한 값들의 모음이기에 값을 변경해버리면 데이터베이스에 저장된 원본 데이터를 찾기 어렵다</strong>.</p>
<p>특정 엔티티 하나에 소속된 값 타입은 값이 변경되어도 자신이 소속된 엔티티를 데이터베이스에서 찾고 값을 변경하면된다. 다만, <strong>값 컬렉션은 보관된 값 타입들을 별도의 테이블에 보관하기에 여기에 보관된 값 타입의 값이 변경되면 데이터베이스에 있는 원본 데이터를 찾기 어렵다.</strong></p>
<p>따라서 JPA 구현체들을 값 타입 컬렉션에 변경 사항이 발생하면 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장한다.</p>
<p>따라서 실무에서 <strong>값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신 일대다 관계를 고려</strong>해야함.</p>
<p>+) 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야한다. 따라서 데이터베이스 기본 키 제약 조건으로 인해 컬럼으로 null을 입력할 수 없고, 같은 값을 중복해서 저장할 수 없는 제약도 존재한다.</p>
<p>위의 문제를 해결하기 위해서는 <strong>값 타입 컬렉션을 만들기 위해서는 새로운 엔티티를 만들어 일대다 관계로 설정하면 된다.</strong> 여기에 추가로 영속성 전이 + 고아 객체 제거 기능을 적용하여 값 타입 컬렉션처럼 사용할 수 있다.</p>
<pre><code class="language-java">@Entity
public class AddressEntity {
    @Id
       @GeneratedValue
    private Long id;

    @Embedded Address address;
}</code></pre>
<p>설정은 아래와 같이 하면 된다.</p>
<pre><code class="language-java">@OneToMany (cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = &quot;MEMBER_ID&quot;)
private List&lt;AddressEntity&gt; addressHistory = new ArrayList&lt;AddressEntity&gt;();</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 팀프로젝트 2주차 작업로그]]></title>
            <link>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9E%91%EC%97%85%EB%A1%9C%EA%B7%B8-2</link>
            <guid>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9E%91%EC%97%85%EB%A1%9C%EA%B7%B8-2</guid>
            <pubDate>Thu, 14 May 2026 09:15:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>5월 14일 작업로그</strong></p>
</blockquote>
<ul>
<li>ERD 및 논리적 설계 수정</li>
<li>함수 종속성 다이어그램</li>
<li>정규화</li>
</ul>
<h2 id="1-erd-및-논리적-설계-수정">1. ERD 및 논리적 설계 수정</h2>
<ul>
<li><p>장바구니 상품에 가격 속성이 있었는데 없애는게 나아 그 부분을 수정하여 논리적 설계도 그거에 맞춰 수정하였다.</p>
<p>  ⇒ 주문 시에는 주문 시점의 가격을 따로 기록할 필요가 있는데 장바구니는 상품의 가격이 변할 때마다 그거에 맞춰 변하는거니까 가격을 기록하지 않고 그냥 가져와서 보여주는게 좋을 듯 했다.</p>
</li>
</ul>
<h2 id="2-함수-종속성-다이어그램">2. 함수 종속성 다이어그램</h2>
<ul>
<li><p>이상현상: 기본키가 아니면서 결정자인 속성(비후보키 결정자 속성)이 릴레이션에 존재할 때 발생</p>
<p>  ⇒ 릴레이션을 결정자인 속성이 기본키가 되도록 분해해 이상현상 제거</p>
</li>
</ul>
<h4 id="상품">상품</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/4098bfda-0293-49d5-b573-3206afae7d98/image.png" alt=""></p>
<ul>
<li>다른 부분에서 기본키 이외에 결정자인 속성이 존재하지 않는 것으로 보인다.</li>
<li>상품-옵션 조합 릴레이션 같은 경우는 상품별 옵션이 뭐가 있는지 살펴보는 용도라 상품상세ID로 옵션을 하나 결정할 수 없고, 옵션으로도 하나를 결정할 수 없는 관계라 이게 괜찮은 건지..? 잘 모르겠다.</li>
</ul>
<h4 id="장바구니">장바구니</h4>
<img src="https://velog.velcdn.com/images/summeryoung_/post/488fab76-db88-4981-939f-101a2c43df69/image.png" width=60%>


<ul>
<li>이걸 그리면서 계속 또 고민인건 장바구니가 진짜 필요할지…? 속성도 하나만 갖는데 그게 회원ID라 필요가 없는거 아닌가?하는 생각이 계속든다.</li>
</ul>
<h2 id="3-정규화">3. 정규화</h2>
<h4 id="제1정규형">제1정규형</h4>
<ul>
<li><input disabled="" type="checkbox"> 릴레이션의 모든 속성이 더 이상 분해되지 않는 원자값만 가질 때</li>
</ul>
<h4 id="제2정규형">제2정규형</h4>
<ul>
<li><input disabled="" type="checkbox"> 릴레이션이 제1정규형에 속할 것</li>
<li><input disabled="" type="checkbox"> 기본키가 아닌 모든 속성이 기본키에 완전 함수 종속될 것</li>
</ul>
<p>+) 완전 함수 종속: A → B 종속성이 성립할 때, B가 A의 속성 전체에 함수 종속하고, 부분집합 속성에 함수 종속하지 않을 경우.</p>
<p>+) 부분 함수 종속(불완전 함수 종속): A의 속성 일부를 제거해도 종속성이 성립하는 경우.</p>
<h4 id="제3정규형">제3정규형</h4>
<ul>
<li><input disabled="" type="checkbox"> 릴레이션이 제2정규형에 속할 것</li>
<li><input disabled="" type="checkbox"> 기본키가 아닌 모든 속성이 기본키에 비이행적 함수 종속</li>
</ul>
<p>+) 이행적 함수 종속: 릴레이션을 구성하는 3개의 속성 집합 X, Y, Z에 대해 X→ Y,  Y→Z가 존재하면 X→Z가 성립</p>
<p>+) 비이행적 함수 종속: 속성이 기본키에 직접적으로만 종속되고, 다른 속성을 거쳐 종속되지 않은 경우</p>
<h4 id="bcnf-정규형">BCNF 정규형</h4>
<ul>
<li><input disabled="" type="checkbox"> 릴레이션의 함수종속성 X→Y가 성립할 때, 모든 결정자 X가 후보키</li>
</ul>
<hr>
<ol>
<li>상품관련 정규화: <a href="https://www.notion.so/35f565fee5c08005a9ccc453999f1e06?pvs=21">상품 정규화 노션</a> </li>
<li>장바구니 관련 정규화: <a href="https://www.notion.so/35f565fee5c0806e81bcf5f734ec9010?pvs=21">장바구니 정규화 노션</a> </li>
</ol>
<p>위에서 정리한 체크리스트? 기반으로 일단 각각 정규화를 진행했다.</p>
<p>정규화를 하면서 깨달은건 장바구니의 함수 종속성 다이어그램을 잘못 그렸었다. </p>
<img src="https://velog.velcdn.com/images/summeryoung_/post/8149db9b-c43b-4fe1-bdc2-e270180e7c88/image.png" width=60%>


<p>위처럼 수정했는데, 일전에 논리적 설계할 때 (회원ID, 상품상세ID)를 UNIQUE로 설정했었는데 이거를 까먹고 있어서 수정하고 정규화를 체크했다.</p>
<h2 id="4-물리적-설계-테스트">4. 물리적 설계 (테스트)</h2>
<h4 id="1-옵션-관련-문제">(1) 옵션 관련 문제</h4>
<img src="https://velog.velcdn.com/images/summeryoung_/post/30a29a27-1bde-476b-8ddd-847fe5abd00b/image.png" width=40%>


<ul>
<li><p>정규화 확인하기 힘들거 같아서 그냥 한 번 테이블 예시를 써봤는데? 옵션 종류와 옵션 상세를 이렇게 나누는게 좋은지 안좋은지 잘 모르겠다.</p>
<ul>
<li><p>과한 거 같기도한데…? 또 막상 합치자니 옵션종류ID로 옵션값 결정이 안될 것 같고</p>
<ul>
<li>(옵션상세ID, 옵션종류ID) → 이렇게 해야 결정이 되고?</li>
</ul>
</li>
<li><p>그렇다고 완전 하나의 거대한 테이블로 만들면 중복 데이터가 많아질 것 같다.</p>
<ul>
<li>하나의 테이블에 합치면 거기에 S-연청 이런식의 데이터가 여러 개의 상품마다 계속 중복으로 들어갈 것 같아서 또 분리해야하는 거 같아지는…?</li>
</ul>
<p>⇒ 우선은 분리해서 가져가는게 나아 보여서 그렇게 하기로 했다.</p>
</li>
</ul>
</li>
</ul>
<h4 id="2-상품옵션조합-테이블-문제">(2) 상품옵션조합 테이블 문제</h4>
<img src="https://velog.velcdn.com/images/summeryoung_/post/1ded7f41-6c16-42bc-bc0b-15a29e7f6231/image.png" width=30%>


<ul>
<li>상품마다의 옵션을 연결하는걸 위에 릴레이션으로 만들어서 그때그때 사용해서 조인하려고 했다. 상품상세하고 옵션상세가 다대다 관계니까 일단 릴레이션으로 빼긴 빼야할 것 같았다.</li>
<li>그런데? 러프하게 데이터 넣고 보니까, 상품 하나에 S 빨강, S 파랑, M 빨강, M 파랑 이렇게 한 종류의 옵션이 아니라 여러 옵션이 들어갈 수 있었다.</li>
<li>잘못된 줄 알았는데 또 막상 실제 상품명이랑 옵션명을 적어놓고 보니까 괜찮게 돌아갈 것 같았다.</li>
</ul>
<img src="https://velog.velcdn.com/images/summeryoung_/post/e2cbd5bc-860e-4d8d-87b6-48fc9c397eee/image.png" width=30%>


<p>⇒ 좀 완전하게 하고 DDL 작성하고 하려고 했는데 이런걸 확인하려면 일단 작성하고 직접 쿼리 써서 테스트해보는게 나을 것 같아서 계획을 바꾸는게 좋을 것 같다…</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 팀프로젝트 1주차 작업로그]]></title>
            <link>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9E%91%EC%97%85%EB%A1%9C%EA%B7%B8-1</link>
            <guid>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9E%91%EC%97%85%EB%A1%9C%EA%B7%B8-1</guid>
            <pubDate>Thu, 14 May 2026 08:47:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>5월 12일 작업로그</p>
</blockquote>
<ul>
<li>개념적 설계 (ERD) 작업</li>
<li>논리적 설계</li>
</ul>
<h2 id="1-개념적-설계-erd-작업">1. 개념적 설계 (ERD 작업)</h2>
<h3 id="상품-개체">상품 개체</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/1c44754e-d06b-4ad5-8af0-63ccc3251a94/image.png" alt=""></p>
<ul>
<li><p>기존에 상품 개체 하나 정도에 카테고리 개체 하나 이렇게 있던 상태에서 장바구니와 옵션을 추가하겠다고 생각하며, 꽤 수정을 많이 하게 되었다.</p>
</li>
<li><p>우선 카테고리 테이블을 만들어, 쇼핑몰 안에 있는 모든 상품들을 카테고리 테이블 내의 범주에서 분류할 수 있고, 카테고리 ID 정도만 외래키로 두는 것을 생각하고 ERD를 그렸다.</p>
</li>
<li><p>옵션을 처음 만들었을 때는 상품 아래 속성에 바로 작성하였는데, 추후에 옵션의 종류(예: 색상/크기)와 종류에 따른 실제 상세 옵션/옵션값(예: 진청/청/하양, S/M/L/XL) 등이 존재할 수 있기에 각각을 테이블로 분류해야할 것 같다는 생각이 들었다</p>
<p>  → 옵션 종류 테이블, 옵션 상세 테이블을 일단? 개체로 만들어두고 다시 상품을 먼저 완성하기로 했다.</p>
</li>
<li><p>옵션-상품 관계를 어떻게 해야할까? 하는 생각을 많이 했다. 처음에는 상품의 외래키 속성 정도면 충분할 것 같았는데? 그러면 사실 상품명/기본가격/제조업체 같은 내용의 중복이 많아진다.</p>
<p>  → 옵션-상품은 다대다 관계일거같아 테이블로 하나 빼는게 좋아보였다. 매핑 테이블 식으로 “<strong>상품 옵션 조합</strong>” 테이블을 만들었다.</p>
</li>
<li><p>쇼핑몰 블록/화면에 뜨는 상품하고, 실제 주문하는 상품이 다르기에(예: 청바지 → 검은 청바지 M) 상품 테이블과 또 별개로 실제 주문하는 상품(주문상품?)이 될 “<strong>상품 상세</strong>” 개체를 만들었다.</p>
</li>
<li><p>상품 상세 개체를 만든 후에는 상품 아래에 있던 재고 수량, 이미지, 판매량 등이 상품 상세 쪽, 즉 실제 판매되는 상품 쪽으로 옮겨가는게 좋을 것 같아 상품 상세쪽으로 속성을 옮겨 수정해주었다.</p>
</li>
</ul>
<aside>

<h4 id="상품-관련-정리">상품 관련 정리</h4>
<ul>
<li>상품: 대표 상품(청바지, 후드집업) 등의 정보 → 옵션 제외 <strong>공통적인 정보를 담아둠</strong></li>
<li>상품 상세: 실제 판매되는 상품=장바구니에 들어가서 결제될 상품. (연한 청바지 S 같이) → 상품을 외래키로 가지고, 재고, 옵션에 따른 추가금, 판매량 등 판매 관련 실제 정보가 존재하고, 매핑 테이블을 통해 옵션과 조합될 개체</li>
<li>상품 옵션 조합: 상품상세와 옵션(상세값)을 각각 외래키로 가지는 매핑 테이블. 둘이 복합키를 구성.</li>
<li>카테고리: 상품(대표 상품/공통 상품)이 어느 카테고리에 속하는지 여부를 결정. 상품 테이블에서 외래키로 소유.</aside>

</li>
</ul>
<h3 id="옵션-개체">옵션 개체</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/12cde601-3f64-408f-8a93-9a7b388c9624/image.png" alt=""></p>
<ul>
<li><p>큰 옵션 범주(사이즈/색상)과 안으로 들어가 세부적인 값을 가질 옵션 상세를 우선은 나누었다.</p>
<p>  → 현재 크기의 쇼핑몰? 단위에서는 사실 굳이 필요할까 싶긴했는데, 비즈니스 로직을 생각하면 색상 → 검정/하양 이렇게 선택하니까 큰 범주를 테이블로 빼두면 구현하는 것도 좋을 것 같았다.</p>
</li>
<li><p>약한 개체가 여전히 헷갈리긴하지만, 옵션상세가 단순히 옵션값으로만 구별된다기보다는 옵션종류+실제 그 옵션값 이렇게 분리되어야할 것 같아서 처음에는 복합키를 생각했는데 이래저래 구현하는 것 생각하고 외래키로 가져야하는거 생각하면 대리키를 두는게 좋을 것 같아 옵션ID라는 대리키를 두고 PK로 삼는게 좋아보여서 그렇게 ERD를 설계했다.</p>
</li>
</ul>
<aside>

<h4 id="옵션-관련-정리">옵션 관련 정리</h4>
<ul>
<li>옵션 종류: 큰 옵션 범주. 예) 사이즈, 색상 …</li>
<li>옵션 상세: 종류 내의 세부적인 옵션. 예) S/M/L/XL, 카키, 검정, 하양</aside>

</li>
</ul>
<h3 id="장바구니-개체">장바구니 개체</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/927f8942-9b20-4a30-a3e2-cb3c88b48f2e/image.png" alt=""></p>
<ul>
<li>장바구니 자체는 사실 회원이 1:1로 소유하는 것이기에 굳이? 개체/테이블로 만들어야할까하는 생각이 들었는데 일단은…확장 가능성?도 있을 수 있고 해서 만들어보긴했는데 여전히 실제 필요할까? 싶긴하다.</li>
<li>장바구니 상품은 장바구니 안에 들어가는 상품의 데이터를 기록하기 위한 테이블/개체가 필요할 거라고 생각해 만들었는데..? 사실 처음에는 주문 상세같은 쪽에서 컬럼을 하나 두어 상태컬럼으로 장바구니 이런 식으로 두는 것도 방법이지 않을까?했는데 장바구니 특성상 실제 넣었다가 주문하지 않을 수도 있고 하기에 일단 분리하여 관리하는 쪽이 좋을 것 같았다.</li>
<li>장바구니 담았을 때의 가격도 일단 가지게 했는데 필요할지..? 상품상세ID를 이용해서 조인으로 가져올 수 있을 것 같긴한데..? 근데 또 주문마다 조인해서 가져오려면 양이 많아질 것 같아서 아직 고민이다.</li>
</ul>
<aside>

<h4 id="장바구니-관련-정리">장바구니 관련 정리</h4>
<ul>
<li><p>장바구니 종류: 실질적으로는 회원ID 1:1 매핑.</p>
</li>
<li><p>장바구니 상품: 장바구니 안에 담기는 상품들에 대한 정보. 수량이나, 상품 상세(실제 판매 상품), 담긴 장바구니(=회원 ID) 등이 존재하고, 이 모든걸 대리키(장바구니 상품 ID)로 관리.</p>
<p>  ⇒ 가격이 굳이 있어야할까…?는 아직 미정…</p>
</li>
</ul>
</aside>

<h2 id="2-논리적-설계">2. 논리적 설계</h2>
<aside>

<h4 id="상품-논리적-설계">상품 논리적 설계</h4>
<h4 id="상품">상품</h4>
<p><strong>상품 (상품ID(PK), 제조업체ID(FK), 상품명, 가격, 카테고리(FK), 이미지)</strong></p>
<ul>
<li>상품ID (INT): PK, NOT NULL, AUTO INCREMENT</li>
<li>제조업체ID (INT): FK, NOT NULL<ul>
<li>제조업체ID 참조</li>
</ul>
</li>
<li>상품명 (VARCHAR): NOT NULL</li>
<li>가격 (INT): NOT NULL</li>
<li>카테고리: FK, NOT NULL<ul>
<li>카테고리ID 참조</li>
</ul>
</li>
<li>이미지 (VARCHAR)<ul>
<li>이미지 URL 정보</li>
</ul>
</li>
</ul>
<h4 id="상품상세">상품상세</h4>
<p><strong>상품상세 (상품상세ID(PK), 상품ID(FK), 재고수량, 추가금, 판매량, 이미지)</strong></p>
<ul>
<li>상품상세ID (INT): PK, NOT NULL, AUTO INCREMENT</li>
<li>상품ID: FK, NOT NULL<ul>
<li>상품ID 참조</li>
</ul>
</li>
<li>재고수량 (BIGINT): NOT NULL, DEFAULT 0</li>
<li>추가금 (BIGINT): NOT NULL, DEFAULT 0</li>
<li>판매량 (BIGINT): NOT NULL, DEFAULT 0</li>
<li>이미지 (VARCHAR)<ul>
<li>이미지 URL 정보</li>
</ul>
</li>
</ul>
<h4 id="상품옵션-조합">상품옵션 조합</h4>
<p><strong>상품옵션 조합 (상품상세ID(PK, FK), 옵션ID(PK,FK))</strong></p>
<ul>
<li>상품상세ID (INT): PK, FK, NOT NULL<ul>
<li>외래키, 복합키</li>
<li>상품상세 참조</li>
</ul>
</li>
<li>옵션ID (INT): PK, FK, NOT NULL<ul>
<li>외래키, 복합키</li>
<li>옵션상세 참조</li>
</ul>
</li>
</ul>
<h4 id="카테고리">카테고리</h4>
<p><strong>카테고리 (카테고리ID(PK), 카테고리명)</strong></p>
<ul>
<li>카테고리ID (INT): PK, NOT NULL, AUTO INCREMENT</li>
<li>카테고리명 (VARCHAR): NOT NULL</li>
</ul>
<h4 id="제조업체">제조업체</h4>
<p><strong>제조업체 (업체번호(PK), 업체명, 소유자)</strong></p>
<ul>
<li>업체번호 (INT): PK, AI, NOT NULL</li>
<li>업체명 (VARCHAR): NOT NULL</li>
<li>소유자 (VARCHAR)</li>
</ul>
<h4 id="장바구니-논리적-설계">장바구니 논리적 설계</h4>
<h4 id="장바구니">장바구니</h4>
<p><strong>장바구니 ( 회원ID(PK, FK) )</strong></p>
<ul>
<li>회원ID (INT) : PK, FK<ul>
<li>식별관계</li>
<li>약한 개체</li>
</ul>
</li>
</ul>
<h4 id="장바구니-상품">장바구니 상품</h4>
<p><strong>장바구니 상품 (장바구니상품ID(PK), 장바구니ID(FK), 상품상세ID(FK), 담은수량, 가격)</strong></p>
<ul>
<li>장바구니 상품ID (INT): PK, NOT NULL, AUTO INCREMENT</li>
<li>회원ID (INT): FK<ul>
<li>장바구니ID 참조 (실제로는 회원ID)</li>
</ul>
</li>
<li>상품상세ID (INT): FK<ul>
<li>상품상세ID 참조</li>
</ul>
</li>
<li>담은수량 (BIGINT): NOT NULL, DEFAULT 1</li>
<li>가격 (BIGINT): NOT NULL</li>
</ul>
<h4 id="옵션-논리적-설계">옵션 논리적 설계</h4>
<h4 id="옵션-종류">옵션 종류</h4>
<p><strong>옵션 종류 (옵션 종류ID(PK), 옵션 종류명)</strong></p>
<ul>
<li>옵션 종류ID (INT): PK, NOT NULL, AUTO INCREMENT</li>
<li>옵션 종류명 (VARCHAR): NOT NULL</li>
</ul>
<h4 id="옵션-상세">옵션 상세</h4>
<p><strong>옵션 상세 (옵션상세ID(PK), 옵션 종류ID(FK), 옵션값)</strong></p>
<ul>
<li>옵션상세ID (INT): PK, NOT NULL, AUTO INCREMENT</li>
<li>옵션 종류ID (INT): FK<ul>
<li>옵션 종류ID 참조</li>
</ul>
</li>
<li>옵션값 (VARCHAR): NOT NULL</li>
</ul>
</aside>

<ul>
<li>양이 많아서 토글로 정리합니당</li>
<li>ERD 짜면서 고민했던 부분들이라 슥슥 작성하긴했는데 NOT NULL 외에 UNIQUE 등 다른 제약을 걸 부분들이 있을 수도 있을 것 같다는…생각이 들긴합니다. (VARCHAR 제한?)</li>
</ul>
<h2 id="3-sql-설정">3. SQL 설정</h2>
<p>깃허브 + 스프링 프로젝트 사용해서 일단 협업을 진행할 생각이었어서 sql 코드 공유를 어떻게 할 지? 찾아봤는데 resources 아래에 application.yaml 파일을 잘 설정하고 data.sql, schema.sql에 sql 코드 작성하면 잘 돌릴 수 있을 것 같았다.</p>
<p>사용자명하고 비밀번호는 올리면 안되니까 application-local.yaml로 분리하는 방식으로 가져가고, application.yaml 파일 설정한 후에 깃허브에 data.sql, schema.sql 파일까지 올려서 설정하였다.</p>
<p>이렇게 하면 schema.sql에 DDL 작성하여서 공유하면 각자 노트북에서 이걸 돌려도 같은 결과를 얻을 수 있을 것 같아서 이런식으로 진행하면 좋을 것 같았다. data.sql에 더미 데이터 관련 insert 쿼리 등을 올리면 되는 식이다.</p>
<h4 id="applicationyaml">application.yaml</h4>
<pre><code class="language-yaml">spring:
  profiles:
    active: local
  application:
    name: {이름}

  sql:
    init:
      mode: always

  jpa:
    defer-datasource-initialization: true
    hibernate:
      ddl-auto: none
    properties:
      show_sql: true
      format_sql: true
      highlight_sql: true</code></pre>
<h4 id="application-localyaml">application-local.yaml</h4>
<pre><code class="language-yaml">spring:
  datasource:
    url: jdbc:mysql://localhost:3306/{스키마이름}?useSSL=false&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=UTC&amp;createDatabaseIfNotExist=true&amp;allowPublicKeyRetrieval=true
    username: &#39;사용자이름&#39;
    password: &#39;비밀번호&#39;
    driver-class-name: com.mysql.cj.jdbc.Driver</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 7주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-7%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-7%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sat, 09 May 2026 09:56:05 GMT</pubDate>
            <description><![CDATA[<h1 id="08장프록시">08장.프록시</h1>
<blockquote>
</blockquote>
<ul>
<li><strong>프록시와 지연로딩, 즉시로딩</strong>: 객체가 데이터베이스에 저장되어 연관된 객체를 마음껏 탐색하기 어려운 상황에서 JPA 구현체들은 이 문제를 해결하기 위해 프록시를 사용함. 프록시를 사용하면 연관된 객체를 처음부터 데이터베이스에서 조회하는 것이 아니라 <strong>실제 사용하는 시점</strong>에 데이터베이스에서 조회할 수 있음. 다만, 자주 함께 사용하는 객체들은 조인을 사용해 함께 조회하는 것이 효과적임.</li>
<li><strong>영속성 전이와 고아 객체</strong>: JPA는 연관된 객체를 함께 저장하거나 함께 삭제할 수 있는 영속성 전이와 고아 객체 제거라는 편리한 기능을 제공함.</li>
</ul>
<hr>
<h2 id="81-프록시">8.1 프록시</h2>
<p>엔티티 조회 시 연관된 엔티티들이 항상 사용되지는 않는다.</p>
<p><strong>예) 회원 엔티티 조회 시 연관된 팀 엔티티의 사용 여부</strong></p>
<ul>
<li><p>회원과 팀 정보를 출력하는 비즈니스 로직</p>
<pre><code class="language-java">public void printUserAndTeam(String memberId) {
  Member member = em.find(Member.class, memberId);
  Team team = member.getTeam();

  System.out.println(&quot;회원이름: &quot;+member.getUsername());
  System.out.println(&quot;소속팀: &quot;+team.getName());
}</code></pre>
</li>
<li><p>회원 정보만 출력하는 비즈니스 로직</p>
<pre><code class="language-java">public String printUser(String memberId) {
  Member member = em.find(Member.class, memberId);
  System.out.println(&quot;회원 이름: &quot;+member.getUsername());
}</code></pre>
</li>
</ul>
<p><code>printUsername()</code>의 경우 memberId를 사용해 회원 엔티티와 팀을 모두 출력하는 반면, <code>printUser()</code>의 경우 회원 엔티티만을 출력한다.</p>
<p>이때 <code>printUser()</code> 메소드는 회원 엔티티만 사용하기에 <code>em.find()</code>로 회원 엔티티를 조회할 때 회원과 연관된 팀 엔티티까지 데이터베이스에서 함께 조회해 두는 것은 비효율적이다.</p>
<p>따라서 JPA는 이런 문제 해결을 위해 <strong>엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법</strong>을 제공하는데 이것을 <strong>지연 로딩</strong>이라고 한다.</p>
<p>즉, <code>team.getName()</code>처럼 엔티티 값을 실제로 사용할 때 데이터베이스에서 필요한 데이터를 조회하는 것이다.</p>
<p>이런 지연 로딩 기능에서 실제 엔티티 객체 대신, 데이터베이스 조회를 <strong>지연할 수 있는 가짜 객체</strong>를 <strong>프록시 객체</strong>라고 한다.</p>
<h3 id="1-프록시-기초">(1) 프록시 기초</h3>
<p>JPA에서 식별자로 엔티티 하나를 조회할 때에는 <strong><code>EntityManager.find()</code></strong>를 사용한다. 해당 메소드는 영속성 컨텍스트에 엔티티가 없으면 데이터베이스를 조회한다.</p>
<pre><code class="language-java">Member member = em.find(Member.class, &quot;member1&quot;);</code></pre>
<p>즉, 위처럼 엔티티를 직접 조회하면 조회한 <strong>엔티티의 사용여부와 관계없이 데이터베이스를 조회</strong>하게된다. </p>
<p>만일 데이터베이스 조회를 실제 사용 시점까지 미룰 때는 <strong><code>EntityManager.getReference()</code></strong> 메소드를 사용한다. 이때 JPA는 데이터베이스를 조회하지 않고, 실제 엔티티 객체도 생성하지 않는 대신 데이터베이스 접근을 위임한 <strong>프록시 객체</strong>를 위임한다.</p>
<h4 id="프록시-객체의-초기화">프록시 객체의 초기화</h4>
<p>프록시 객체의 초기화: 실제 엔티티가 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는 것</p>
<pre><code class="language-java">//MemberProxy 변환
Member member = em.getReference(Member.class, &quot;id1&quot;);
member.getName();</code></pre>
<pre><code class="language-java">class MemberProxy extends Member {
    Member target = null;

    public String getName() {
        if (target == null) {
            //2. 초기화 요청
            //3. DB 조회
            //4. 실제 엔티티 생성 및 참조 보관
            this.target = ...;
        }

        //5. target.getName();
        return target.getName();
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/7ae4dd4c-4a94-4094-a51e-9523bfbb5454/image.png" alt=""></p>
<ol>
<li>프록시 객체에 <code>member.getName()</code>을 호출해서 실제 데이터를 조회한다.</li>
<li>프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청하는데, 이를 초기화라한다.</li>
<li>영속성 컨텍스트는 데이터베이스를 조회해 실제 엔티티 객체를 생성한다.</li>
<li>프록시 객체는 생성된 실제 엔티티 객체의 참조를 Member target 변수에 보관한다.</li>
<li>프록시 객체는 실제 엔티티 객체의 <code>getName()</code>을 호출해서 결과를 반환</li>
</ol>
<h4 id="프록시의-특징">프록시의 특징</h4>
<ul>
<li>실제 클래스를 상속받아 만들어지기에 실제 클래스와 겉모양이 동일하다. </li>
<li>즉, 사용할 때는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용해도 된다.</li>
<li>실제 객체에 대한 <strong>참조</strong>를 보관한다.</li>
<li>프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.</li>
<li>프록시 객체는 <strong>처음 사용 시 한 번만 초기화</strong>됨.</li>
<li>프록시 객체를 초기화했다고, <strong>프록시 객체가 실제 엔티티로 바뀌는 것은 아니다</strong>. 프록시 객체가 초기화되며, 프록시 객체를 통해 실제 엔티티에 접근할 수 있다.</li>
<li>프록시 객체는 원본 엔티티를 상속받은 객체이므로 <strong>타입 체크 시에 주의해 사용</strong>해야한다.</li>
<li>영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없기에 <code>em.getReference()</code>를 호출해도 프록시가 아닌 실제 엔티티를 반환한다.</li>
<li>초기화는 <strong>영속성 컨텍스트의 도움</strong>을 받아야한다. 따라서 <strong>준영속 상태의 프록시를 초기화하면 문제가 발생</strong>한다.</li>
</ul>
<h4 id="준영속-상태와-초기화">준영속 상태와 초기화</h4>
<pre><code class="language-java">//MemberProxy 반환
Member member = em.getReference(Member.class, &quot;id1&#39;);
transaction.commit();
em.close(); //영속성 컨텍스트 종료

member.getName(); //준영속 상태 초기화 시도 -&gt; 예외발생</code></pre>
<p><code>em.close()</code>를 통해 영속성 컨텍스트를 종료하게되면, <code>member</code>는 준영속 상태가 된다. 이때 <code>member.getName()</code>을 호출하면 프록시를 초기화해야하는데 영속성 컨텍스트가 없기에 실제 엔티티 조회가 불가해 예외가 발생한다.</p>
<h3 id="2-프록시와-식별자">(2) 프록시와 식별자</h3>
<p>엔티티를 프록시로 조회할 때 <strong>식별자(PK)값을 파라미터로 전달</strong>하는데 <strong>프록시 객체는 이 식별자값을 보관</strong>한다.</p>
<pre><code class="language-java">Team team = em.getReference(Team.class, &quot;team1&quot;); //식별자 보관
team.getId(); //초기화X</code></pre>
<p>프록시 객체는 식별자를 이미 가지고 있기에, 식별자 값을 조회해도 프록시 초기화가 발생하지는 않는다. 단, 엔티티 접근방식을 프로퍼티(<code>@Access(AccessType.PROPERTY</code>)로 설정한 경우에만 초기화를 하지 않는다.</p>
<p>만약 엔티티 접근방식을 필드(<code>@Access(AccessType.FIELD)</code>)로 설정하면 JPA는 <code>getId()</code> 메소드가 id만 조회하는 메소드인지 다른 필드까지 활용해서 어떤 일을 하는 메소드인지 알지 못하기에 프록시 객체를 초기화한다.</p>
<p>프록시는 다음처럼 연관관계 설정 시 유용하게 사용할 수 있다.</p>
<pre><code class="language-java">Member member = em.find(Member.class, &quot;member1&quot;);
Team team = em.getReference(Team.class, &quot;team1&quot;); //SQL 실행X
member.setTeam(team);</code></pre>
<p>연관관계 설정 시에는 식별자값만 사용하기에 프록시를 사용하게되면 데이터베이스 접근 횟수를 줄일 수 있음. 연관관계 설정 시에는 엔티티 접근 방식을 필드로 설정해도 프록시를 초기화하지 않는다.</p>
<h3 id="3-프록시-확인">(3) 프록시 확인</h3>
<p>JPA에서 제공하는 *<em><code>PersistenceUnitUtil.isLoaded(Object entity)</code> *</em> 메소드를 사용하면 프록시 인스턴스의 초기화 여부를 확인할 수 있음. 아직 초기화되지 않은 프록시 인스턴스는 false를 반환한다. 이미 초기화되었거나 프록시 인스턴스가 아닐 때에는 true를 반환한다.</p>
<pre><code class="language-java">boolean isLoaded = em.getEntityManagerFactory()
                    .getPersistenceUnitUtil().isLoaded(entity);
//또는 boolean isLoad = emf.getPersistenceUnitUtil().isLoaded(entity);</code></pre>
<p>조회한 엔티티가 진짜 엔티티인지 프록시로 조회한 것인지를 확인하기 위해서는 <strong>클래스명</strong>을 직접 출력하면 된다. <code>...javassist...</code>라고 적혀있으면 프록시인 것을 알 수 있다. 출력 결과는 프록시 생성 라이브러리에 따라 달라질 수 있다.</p>
<pre><code class="language-java">System.out.println(&quot;memberProxy =         qw&quot;+member.getClass().getName());</code></pre>
<hr>
<h2 id="82-즉시-로딩과-지연로딩">8.2 즉시 로딩과 지연로딩</h2>
<p>프록시 객체는 주로 <strong>연관된 엔티티를 지연로딩</strong>하기 위해 사용한다.</p>
<pre><code class="language-java">Member member = em.find(Member.class, &quot;member1&quot;);
Team team = member.getTeam(); //객체 그래프 탐색
System.out.println(team.getName()); //팀 엔티티 사용</code></pre>
<p>JPA는 개발자가 연관된 <strong>엔티티의 조회 시점을 선택</strong>할 수 있도록 두 가지 방법을 제공한다.</p>
<ul>
<li><p><strong>즉시로딩</strong>: 엔티티를 조회할 때 연관도니 엔티티도 함께 조회한다</p>
<ul>
<li>설정방법: <code>@ManyToOne(fetch = FetchType.EAGER)</code></li>
</ul>
</li>
<li><p><strong>지연로딩</strong>: 연관된 엔티티를 실제 사용할 때 조회</p>
<ul>
<li>설정방법: <code>@ManyToOne(fetch = FetchType.LAZY)</code></li>
</ul>
</li>
</ul>
<h3 id="1-즉시-로딩">(1) 즉시 로딩</h3>
<p>즉시 로딩 사용 시, <code>@ManyToOne</code>의 <code>fetch</code> 속성을 <code>FetchType.EAGER</code>로 설정한다.</p>
<pre><code class="language-java">@Entity
public class Member {
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = &quot;TEAM_ID&quot;)
       private Team team;
}

Member member = em.find(Member.class, &quot;member1&quot;);
Team team = member.getTeam(); //객체 그래프 탐색</code></pre>
<p>회원과 팀을 즉시 로딩으로 설정하였기에, <code>em.find(Member.class, &quot;member1&quot;);</code>로 회원을 조회하는 순간 팀도 함께 조회한다. </p>
<p>이때 쿼리를 2번 실행하지 않고, JPA 구현체는 <strong>즉시 로딩 최적화를 위해 가능하면 조인 쿼리를 사용</strong>한다. 즉, 회원과 팀을 조인해 쿼리 하나로 두 엔티티를 모두 조회한다.</p>
<pre><code class="language-sql">SELECT
    M.MEMBER_ID AS MEMBER_ID,
    M.TEAM_ID AS TEAM_ID,
    M.USERNAME AS USERNAME,
    T.TEAM_ID AS TEAM_ID,
    T.NAME AS NAME
FROM MEMBER M LEFT OUTER JOIN 
     TEAM T ON M.TEAM_ID = T.TEAM_ID
WHERE M.MEMBER_ID=&#39;member1&#39;;</code></pre>
<h3 id="2-지연-로딩">(2) 지연 로딩</h3>
<p>지연 로딩 사용을 위해서는, <code>@ManyToOne</code>의 <code>fetch</code> 속성을 <code>FetchType.LAZY</code>로 지정한다.</p>
<pre><code class="language-java">@Entity
public class Member {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;TEAM_ID&quot;)
    private Team team;
    //...
}</code></pre>
<p>회원과 팀을 지연 로딩으로 설정했기에 <code>em.find(Member.class, &quot;member1&quot;);</code> 호출 시, 회원만 조회하고 팀은 조회하지 않는다. 대신 조회한 회원의 team 멤버변수에 <strong>프록시 객체</strong>를 넣어둔다.</p>
<pre><code class="language-java">Team team = member.getTeam(); //프록시 객체</code></pre>
<p>반환되는 팀 객체는 프록시 객체로, 이 프록시 객체는 실제 사용까지 데이터 로딩을 미루게된다.</p>
<h3 id="3-즉시-로딩-지연-로딩-정리">(3) 즉시 로딩, 지연 로딩 정리</h3>
<p>연관된 엔티티를 처음부터 모두 영속성 컨텍스트에 올려두는 것은 현실적이지 않다. 다만, 필요할 때마다 SQL을 실행해 연관된 엔티티를 지연 로딩하는 것도 최적화 관점에서 보면 좋지 않다고 한다.</p>
<p>예를 들어 애플리케이션 로직에서 회원과 팀 엔티티를 같이 사용하는 경우가 많을 때면, SQL 조인을 사용해 둘을 한 번에 조회하는 것이 더 효율적이다.</p>
<hr>
<h2 id="83-지연-로딩-활용">8.3 지연 로딩 활용</h2>
<blockquote>
<p><strong>사내 주문 관리 시스템</strong></p>
</blockquote>
<ul>
<li>회원은 팀 하나에만 소속할 수 있다. (N:1)</li>
<li>회원은 여러 주문 내역을 가진다. (1:N)</li>
<li>주문내역은 상품정보를 가진다. (N:1)<br> </li>
<li><em>애플리케이션 로직*</em></li>
<li>회원과 연관된 팀은 자주 함께 사용되어, 즉시로딩으로 설정.</li>
<li>회원과 연관된 주문은 가끔 사용되어, 지연로딩으로 설정.</li>
<li>주문과 연관된 상품은 자주 함께 사용되어, 즉시로딩으로 설정.</li>
</ul>
<pre><code class="language-java">@Entity
public class Member {
    @Id
    private String id;
    private String username;
    private Integer age;

    @ManyToOne(fetch = FetchType.EAGER)
    private Team team;

    @OneToMany(mappedBy = &quot;member&quot;, fetch=FetchType.LAZY)
    private List&lt;Order&gt; orders;
}</code></pre>
<p>회원과 팀의 연관관계를 <code>FetchType.EAGER</code>로 설정하여 즉시 로딩되도록 설정한다. 외에 회원과 주문 내역의 경우에는 <code>FetchType.LAZY</code>로 설정해 실제 사용될 때까지 로딩을 지연한다.</p>
<p>회원 조회 시의 SQL은 아래와 같다.</p>
<pre><code class="language-sql">SELECT 
    member.id AS MEMBERID,
    member.age AS AGE,
    member.team_id AS TEAM_ID,
    member.username AS USERNAME,
       team.id AS TEAMID,
    team.name AS NAME
FROM member member LEFT OUTER JOIN
     team team on member.team_id = team1_.ID
WHERE member0_.ID = &#39;member1&#39;;
</code></pre>
<p>회원과 팀이 즉시 로딩으로 설정되었기 때문에, 하이버네이트는 조인 쿼리를 만들어 회원과 팀을 한 번에 조회한다. 반면, 회원과 주문 내역은 지연 로딩으로 설정했기에 결과를 프록시로 조회하게된다. 따라서 위의 SQL에는 전혀 나타나지 않는다. </p>
<p>회원 조회 후 <code>member.getTeam()</code>을 호출하게되면, 이미 로딩된 팀 엔티티를 반환하게 된다.</p>
<h3 id="1-프록시와-컬렉션-래퍼">(1) 프록시와 컬렉션 래퍼</h3>
<p><strong>컬렉션 래퍼</strong>: 하이버네이트는 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하는 것.</p>
<p>엔티티 지연 로딩 시, 프록시 객체를 사용해 지연로딩을 처리하게되지만, 컬렉션의 경우에는 컬렉션 래퍼가 지연 로딩을 처리해주게됨.</p>
<p>컬렉션의 경우에는 <code>member.getOrders()</code>를 호출해도 컬렉션 초기화가 발생하지 않고 <strong>컬렉션에서 <code>member.getOrders().get(0);</code> 처럼 실제 데이터를 조회</strong>할 때에 데이터베이스를 조회해서 초기화하게된다.</p>
<h3 id="2-jpa-기본-패치-정략">(2) JPA 기본 패치 정략</h3>
<p>fetch 속성의 기본 설정값은 아래와 같다.</p>
<ul>
<li><code>@ManyToOne</code>, <code>@OneToOne</code>: 즉시로딩</li>
<li><code>@OneToMany</code>, <code>@ManyToMany</code>: 지연로딩</li>
</ul>
<p>JPA 기본 fetch 전략은 연관된 엔티티가 하나면 즉시로딩, 컬렉션이면 지연로딩을 사용하게된다. 컬렉션을 로딩하는 것은 비용이 많이 들고 잘못할 때, 너무 많은 데이터를 로딩하게 될 수 있기 때문이다.</p>
<p>추천되는 방법은 <strong>모든 연관관계에서 지연로딩을 사용</strong>하는 것이었다. 그리고 추후 애플리케이션 개발이 어느정도 완료되었을 때, 실제 사용하는 상황을 보고 꼭 필요한 상황에 즉시 로딩을 사용하도록 최적화하면 된다.</p>
<h3 id="3-컬렉션에서-fetchtypeeager-사용시-주의점">(3) 컬렉션에서 FetchType.EAGER 사용시 주의점</h3>
<ul>
<li><p>컬렉션을 <strong>하나 이상 즉시 로딩</strong>하는 것은 권장되지 않는다.</p>
<ul>
<li>컬렉션과 조인하는 것은 데이터베이스 테이블로 보면 <strong>일대다 조인</strong>으로 일대다 조인은 결과 데이터가 다쪽에 있는 수만큼 증가된다.</li>
<li>서로 다른 컬렉션 2개 이상을 조인할 때는 너무 많은 데이터를 반환할 수 있어 애플리케이션 성능 저하가 발생할 수 있다.</li>
</ul>
</li>
<li><p>컬렉션 즉시 로딩 때는 항상 <strong>외부 조인</strong>을 사용한다.</p>
</li>
</ul>
<h4 id="fetchtypeeager-설정과-조인-전략-정리">FetchType.EAGER 설정과 조인 전략 정리</h4>
<ul>
<li><p><code>@ManyToOne</code>, <code>@OneToOne</code>:</p>
<ul>
<li><code>optional = false</code>: 내부 조인</li>
<li><code>optional = true</code>: 외부 조인</li>
</ul>
</li>
<li><p><code>@OneToMany</code>, <code>@ManyToMany</code></p>
<ul>
<li><code>optional = false</code>: 외부조인</li>
<li><code>optional = true</code>: 외부조인</li>
</ul>
</li>
</ul>
<hr>
<h2 id="84-영속성-전이-cascade">8.4 영속성 전이: CASCADE</h2>
<p><strong>영속성 전이</strong>: 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속 상태로 만들 때 사용. JPA의 경우 CASCADE 옵션을 통해 영속성 전이를 제공한다.</p>
<pre><code class="language-java">@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;

    @OneToMany(mappedBy = &quot;parent&quot;)
    private List&lt;Child&gt; children = new ArrayList&lt;&gt;();
}

@Entity
public class Child {
    @Id @GeneratdValue
    private Long id;

    @ManyToOne
    private Parent parent;
}</code></pre>
<p>부모 하나에 자식 둘을 저장하는 경우</p>
<pre><code class="language-java">Parent parent = new Parent();
em.persist(parent);

Child child1 = new Child();
child1.setParent(parent);
parent.getChildren().add(child1);
em.persist(child1);

Child child2 = new Child();
child2.setParent(parent);
parent.getChildren().add(child2);
em.persist(child2);</code></pre>
<p><strong>JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태</strong>여야함. 그렇기에 부모 엔티티를 영속 상태로 만들고 자식 엔티티도 각각 영속 상태로 만든다.</p>
<p>이때 영속성 전이를 사용하면 부모만 영속 상태로 만들면, 연관된 자식까지 한 번에 영속 상태로 만들어야함.</p>
<h3 id="1-영속성-전이-저장">(1) 영속성 전이: 저장</h3>
<pre><code class="language-java">@Entity
public class Parent {
    @OneToMany(mappedBy = &quot;parent&quot;, cascade = CascadeType.PERSIST)
    private List&lt;Child&gt; children = new ArrayList&lt;&gt;();
}</code></pre>
<p> <code>cascade = CascadeType.PERSIST</code> 옵션을 통해 부모를 영속화할 때 연관된 자식들도 함께 영속화하는 설정. 해당 옵션을 적용할 때, 부모와 자식 엔티티를 한 번에 영속화할 수 있음.</p>
<h4 id="cascade-저장">CASCADE 저장</h4>
<pre><code class="language-java"> private static void saveWithCascade(EntityManager em) {
     Child child1 = new Child();
    Child child2 = new Child();

    Parent parent = new Parent();
    child1.setParent(parent);
    child2.setParent(parent);
    parent.getChildren().add(child1);
    parent.getChildren().add(chlid2);

    //부모 저장, 연관된 자식들 저장
    em.persist(parent);
 }</code></pre>
<p> 부모만 영속화하면, <code>CascadeType.PERSIST</code>로 설정한 자식 엔티티까지 함게 영속화해서 저장.</p>
<p> 영속성 전이의 경우에는 연관관계 매핑과는 관련이 없다. 단지 <strong>엔티티를 영속화할 때 연관된 엔티티도 같이 영속화하는 편리함을 제공</strong>한다. </p>
<h3 id="2-영속성-전이-삭제">(2) 영속성 전이: 삭제</h3>
<p>부모와 자식 엔티티를 모두 제거할 때는 각각의 엔티티를 하나씩 제거해야한다.</p>
<pre><code class="language-java">Parent findParent = em.find(Parent.class, 1L);
Child findChild1 = em.find(Child.class, 1L);
Child findChild2 = em.find(Child.class, 2L);

em.remove(findChild1);
em.remove(findChild2);
em.remove(findParent);</code></pre>
<p>영속성 전이는 엔티티를 삭제할 때도 사용할 수 있음. <code>CascadeType.REMOVE</code>로 설정하게되면 부모 엔티티만 삭제하면 연관된 자식 엔티티도 함께 삭제.</p>
<p>이 경우 DELETE SQL을 3번 실행해 부모와 연관된 자식을 모두 삭제한다.  삭제 순서는 외래키 제약조건을 고려해 자식을 먼저 삭제한 후에 부모를 삭제하게된다. </p>
<p>만약 <code>CascadeType.REMOVE</code>를 설정하지 않고 코드를 실행하면 부모 엔티티만 삭제된다. 하지만, 데이터베이스의 부모 열을 삭제하면 외래키 제약조건으로 인해 외래키 무결성 예외가 발생한다.</p>
<h3 id="3-cascade의-종류">(3) CASCADE의 종류</h3>
<p>CascadeType은 다양한 옵션이 존재한다.</p>
<pre><code class="language-java">public enum CascadeType {
    ALL,
    PERSIST,
    MERGE,
    REMOVE,
    REFRESH,
    DETACH
}</code></pre>
<p>위의 속성 중 여러 개를 같이 사용할 수 있다.</p>
<hr>
<h2 id="85-고아-객체">8.5 고아 객체</h2>
<p>고아 객체 제거: JPA가 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는 것을 말함.</p>
<p>해당 기능을 통해 ** 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동삭제**되도록할 수 있다.</p>
<pre><code class="language-java">@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;

    @OneToMany (mappedBy = &quot;parent&quot;, orphanRemoval = true)
    private List&lt;Child&gt; children = new ArrayList&lt;&gt;();
}</code></pre>
<p>고아 객체 기능 활성화를 위해서는 컬렉션에 <code>orphanRemoval = true</code>를 설정한다. 이후에는 <strong>컬렉션에서 제거한 엔티티는 자동으로 삭제</strong>된다. 이 기능은 영속성 컨텍스트 플러시할 때 적용되기에 플러시 시점에 DELETE SQL이 실행된다.</p>
<p>모든 자식 엔티티를 제거하기 위해서는 컬렉션을 비우면 된다.</p>
<ul>
<li><p>고아 객체 제거 기능: <strong>참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능</strong>.</p>
<ul>
<li>해당 기능은 <strong>참조하는 곳이 하나</strong>일 때에만 사용해야한다.</li>
<li>삭제한 엔티티를 다른 곳에서도 참조하면 문제가 발생한다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="86-영속성-전이--고아-객체-생명-주기">8.6 영속성 전이 + 고아 객체, 생명 주기</h2>
<p><code>CascadeType.ALL</code> + <code>orphanRemoval</code> = true를 동시에 사용하게되면?</p>
<ul>
<li>일반적으로 엔티티는 <code>EntityManger.persist()</code>를 통해 영속화됨.</li>
<li>이후 <code>EntityManager.remove()</code>를 통해 제거됨
=&gt; 즉, 엔티티 스스로 생명주기를 관리하게된다.</li>
</ul>
<p>만약 위의 두 옵션을 모두 활성화하게되면, <strong>부모 엔티티를 통해 자식의 생명 주기를 관리</strong>할 수 있게된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자바 ORM 표준 JPA 프로그래밍] 6주차 스터디]]></title>
            <link>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-6%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@summeryoung_/%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-6%EC%A3%BC%EC%B0%A8-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Sat, 02 May 2026 16:53:55 GMT</pubDate>
            <description><![CDATA[<h1 id="07장-고급매핑">07장. 고급매핑</h1>
<blockquote>
<h4 id="내용">내용</h4>
</blockquote>
<ul>
<li><strong>상속 관계 매핑</strong>: 객체의 상속 관계를 데이터베이스에서 매핑하는 방법</li>
<li><strong><code>@MappedSuperclass</code></strong>: 등록일, 수정일 같이 여러 엔티티에서 공통으로 사용하는 매핑 정보만 상속받고 싶을 때 사용.</li>
<li><strong>복합키와 식별 관계 매핑</strong>: 데이터베이스의 식별자가 하나 이상일 때 매핑하는 방법 및 데이터베이스 설계의 식별관계와 비식별 관계</li>
<li><strong>조인 테이블</strong>: 외래 키 이외에 연관 테이블을 두어 연관관계를 연결하는 방법. 연결 테이블을 매핑하는 방법을 다룸</li>
<li><strong>엔티티 하나에 여러 테이블 매핑</strong></li>
</ul>
<hr>
<h2 id="71-상속-관계-매핑">7.1 상속 관계 매핑</h2>
<h4 id="슈퍼타입-서브타입-관계">슈퍼타입 서브타입 관계</h4>
<p>관계형 데이터베이스에는 상속이라는 개념이 존재하지 않음. 위의 모델링 기법이 그나마 객체의 상속 개념과 가장 유사하다.</p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/0772aa3e-5eba-4cc8-ada5-2344fe3b73b3/image.png" alt=""></p>
<p>즉, ORM에서의 상속 관계 매핑은 객체의 상속 구조와 데이터베이스 슈퍼타입 서브타입 관계를 매핑하는 것.</p>
<h4 id="물리-모델로-구현하는-3가지-방법">물리 모델로 구현하는 3가지 방법</h4>
<ol>
<li>각각의 테이블로 변환: 슈퍼타입과 서브타입을 모두 테이블로 만든 후 조인을 사용하는 방법. (조인 전략)</li>
<li>통합 테이블로 변환: 테이블 하나를 사용해서 통합. (단일 테이블 전략)</li>
<li>서브타입 테이블로 변환: 서브타입마다 하나의 테이블을 생성. (구현 클래스마다 테이블 전략)</li>
</ol>
<hr>
<h3 id="1-조인-전략">1. 조인 전략</h3>
<p>엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블을 기본 키를 받아 &quot;기본키 + 외래키&quot;로 사용하는 전략. 따라서 조회 시 조인을 자주 사용한다.</p>
<p>해당 전략 사용 시 주의점: 객체는 타입으로 구분이 가능하지만, 테이블은 타입을 구분하지 못하기에 <strong>타입 구분 컬럼 추가</strong> 필요. <code>DTYPE</code> 컬럼을 구분 컬럼으로 사용.</p>
<pre><code class="language-java">//슈퍼타입 테이블
@Entity
@Interitance (strategy = InheritanceType.JOINED)
@DiscriminatorColumn (name = &quot;DTYPE&quot;)
public abstract class Item {
    @Id @GeneratedValue
    @Column (name = &quot;ITEM_ID&quot;)
    private Long id;

    prviate String name;
    private int price;
}

//서브타입 테이블
@Entity
@DiscriminatorValue(&quot;A&quot;)
public class Album extends Item {
    private String artist;
}

//서브타입 테이블
@Entity
@DiscriminatorValue(&quot;M&quot;)
public class Movie extends Item {
    private String director;
    private String actor;
}</code></pre>
<ul>
<li><code>@Inheritance (strategy = InheritanceType.JOINED)</code>: 상속 매핑은 부모 클래스에 <code>@Inheritance</code>를 사용. 매핑 전략 지정에서 위에서는 조인 전략을 사용하므로 <code>Inheritance.JOINED</code> 선택.</li>
<li><code>@DiscriminatorColumn(name=&quot;DTYPE&quot;)</code>: 부모 클래스에 구분 컬럼을 지정. 해당 컬럼으로 자식 테이블을 구분.</li>
<li><code>@DiscriminatorValue(&quot;M&quot;)</code>: 엔티티 저장 시 구분 컬럼에 입력할 값을 지정.</li>
</ul>
<p>기본적으로 자식 테이블을 부모 테이블의 ID 컬럼명을 그대로 사용하게 되는데, 만약 이를 변경하기 위해서는 <code>@PrimaryKeyJoinColumn</code>을 사용.</p>
<pre><code class="language-java">@Entity
@DiscriminatorValue(&quot;B&quot;)
@PrimaryKeyJoinColumn(name = &quot;BOOK_ID&quot;) //ID 재정의
public class Book extends Item {
    private String author;
    private String isbn;
}</code></pre>
<blockquote>
<h3 id="조인전략-정리">조인전략 정리</h3>
</blockquote>
<h4 id="장점">장점:</h4>
<ul>
<li>테이블의 정규화</li>
<li>외래키 참조 무결성 제약조건 활용 가능</li>
<li>저장공간의 효율적 사용<h4 id="단점">단점:</h4>
</li>
<li>조회 시 조인이 많이 사용되어 성능이 저하될 수 있음</li>
<li>조회 쿼리가 복잡함</li>
<li>데이터 등록 시 INSERT SQL을 두 번 실행해야함<h4 id="특징">특징:</h4>
</li>
<li>JPA 표준 명세는 구분 컬럼을 사용하도록 하지만, 하이버네이트 포함 몇몇 구현체는 구분 컬럼 없이도 동작<h4 id="관련-어노테이션">관련 어노테이션:</h4>
</li>
<li><code>@PrimaryKeyJoinColumn</code>, <code>@DiscriminatorColumn</code>, <code>@DiscriminatorValue</code></li>
</ul>
<hr>
<h3 id="2-단일-테이블-전략">2. 단일 테이블 전략</h3>
<p>테이블을 하나만 사용하며, 구분 컬럼으로 어떤 자식 데이터가 저장되었는지 구분. 조회 시 조인을 사용하지 않기 때문에 일반적으로 가장 빠른 편.</p>
<p>자식 엔티티가 매핑한 컬럼은 모두 널(null)을 허용해야한다는 주의점이 존재.</p>
<pre><code class="language-java">@Entity
@Inheritance (strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn (name = &quot;DTYPE&quot;)
public abstract class Item {
    @Id @GeneratedValue
    @Column (name = &quot;ITEM_ID&quot;)
    private Long id;

    private String name;
    private int price;
}

@Entity
@DiscriminatorValue(&quot;A&quot;)
public class Album extends Item {...}

@Entity
@DiscriminatorValue(&quot;M&quot;)
public class Movie extends Item {...}

@Entity
@DiscriminatorValue(&quot;B&quot;)
public class Book extends Item {...}</code></pre>
<p><code>InheritanceType.SINGLE_TABLE</code>로 지정 시 단일 테이블 전략을 사용한다는 의미로 테이블 하나에 모든 것을 통합하기 때문에 <strong>구분 컬럼을 필수</strong>로 사용해야함.</p>
<blockquote>
<h3 id="단일테이블-전략-정리">단일테이블 전략 정리</h3>
</blockquote>
<h4 id="장점-1">장점:</h4>
<ul>
<li>조인이 필요없기에 일반적으로 조회 성능이 빠름</li>
<li>조회 쿼리가 단순함<h4 id="단점-1">단점:</h4>
</li>
<li>자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야함</li>
<li>단일 테이블에 모든 것을 저장하기에 테이블이 커질 수 있음. 따라서 상황에 따라 조회 성능이 떨어질 수 있음<h4 id="특징-1">특징:</h4>
</li>
<li>구분 컬럼을 반드시 사용해야함. <code>@DiscriminatorColumn</code>을 꼭 설정해야함</li>
<li><code>@DiscriminatorValue</code>를 지정하지 않을 시, 기본으로 엔티티 이름을 사용함</li>
</ul>
<hr>
<h3 id="3-구현-클래스마다-테이블-전략">3. 구현 클래스마다 테이블 전략</h3>
<p>자식 엔티티마다 별개의 테이블을 만들고, 자식 테이블 각각에 필요한 컬럼이 모두 존재하는 방식이다.</p>
<pre><code class="language-java">@Entity
@Inheritance (strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
    @Id @GeneratedValue
    @Column(name = &quot;ITEM_ID&quot;)
    private Long id;

    private String name;
    private int price;
}

@Entity
public class Album extends Item {...}

@Entity
public class Movie extends Item {...}

@Entity
public class Book extends Item {...}
</code></pre>
<p><code>InheritanceType.TABLE_PER_CLASS</code>로 지정하여 구현 클래스마다 테이블 전략을 사용. 해당 전략은 자식 엔티티마다 테이블을 만든다. 일반적으로는 추천하지 않는 전략이라고 한다.</p>
<blockquote>
<h3 id="구현-클래스마다-테이블-전략-정리">구현 클래스마다 테이블 전략 정리</h3>
</blockquote>
<h4 id="장점-2">장점:</h4>
<ul>
<li>서브타입을 구분해 처리 시 효과적</li>
<li><code>not null</code> 제약 조건을 사용할 수 있음<h4 id="단점-2">단점:</h4>
</li>
<li>여러 자식 테이블을 함께 조회할 때 성능이 느림. (UNION 사용 필요)</li>
<li>자식 테이블을 통합해서 쿼리하기 어려움<h4 id="특징-2">특징</h4>
</li>
<li>구분 컬럼을 사용하지 않음</li>
</ul>
<hr>
<h2 id="72-mappedsuperclass">7.2 <code>@MappedSuperclass</code></h2>
<h3 id="mappedsuperclass"><code>@MappedSuperclass</code></h3>
<p>앞서 부모클래스와 자식 클래스를 모두 데이터베이스 테이블과 매핑했는데, 만약 부모 클래스의 테이블과 매핑하지 않고, 부모 클래스를 상속받는 자식 클래스에게 매핑정보만 제공할 때는 <code>@MappedSuperclass</code>를 사용하면 됨.</p>
<p>비유하자면 추상클래스와 비슷하고 <code>@Entity</code> 어노테이션이 실제 테이블과 매핑되는 반면, <code>@MappedSuperclass</code>의 경우 실제 테이블과 매핑되지 않는다는 특징이 있다. 즉, 이는 단순히 <strong>매핑 정보를 상속</strong>할 목적으로만 사용됨.</p>
<img src="https://velog.velcdn.com/images/summeryoung_/post/dca4e731-3562-4e4e-96f4-c0b351b480a9/image.png" width=70%>

<pre><code class="language-java">@MappedSuperclass
public abstract class BaseEntity {
    @Id @GeneratedValue
    private Long id;
    private String name;
}

@Entity
public class Member extends BaseEntity {
    private String email;
}

@Entity
public class Seller extends BaseEntity {
    private String shopName;
}</code></pre>
<p><code>BaseEntity</code>에 객체들의 공통 매핑 정보를 정의하고, 자식 엔티티들은 상속을 통해 <code>BaseEntity</code>의 매핑 정보를 물려받는다. </p>
<p>이때 <code>BaseEntity</code>는 테이블과 매핑할 필요 없고, 자식 엔티티에게 공통으로 사용되는 매핑 정보만 제공하면 되기에 <code>@MappedSuperclass</code>를 사용한다.</p>
<h3 id="attributeoverrides-associationoverrides"><code>@AttributeOverrides</code>, <code>@AssociationOverrides</code></h3>
<p>부모로부터 물려받은 매핑 정보를 재정의하기 위해서 <code>@AttributeOverrides</code>를 사용하며, 연관관계를 재정의할 때는 <code>@AssociationOverrides</code>를 사용한다.</p>
<pre><code class="language-java">@Entity
@AttributeOverrides({
    @AttributeOverride(name=&quot;id&quot;, column=@Column(name =&quot;MEMBER_ID&quot;)),
    @AttributeOverride(name=&quot;name&quot;, column=@Column(name=&quot;MEMBER_NAME&quot;))
})</code></pre>
<h4 id="특징-정리">특징 정리</h4>
<ul>
<li>테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용</li>
<li><code>@MappedSuperclass</code>로 지정한 클래스는 엔티티가 아니므로 em.find()나 JPQL에서 사용할 수 없음</li>
<li>해당 클래스를 직접 생성해 사용할 일은 거의 없기에 추상 클래스로 만드는 것이 권장됨.</li>
</ul>
<p>등록일자, 수정일자, 등록자, 수정자 같은 여러 엔티티에서 공통으로 사용하는 속성을 효과적으로 관리할 수 있다.</p>
<hr>
<h2 id="73-복합키와-식별관계-매핑">7.3 복합키와 식별관계 매핑</h2>
<h3 id="1-식별-관계-vs-비식별관계">1. 식별 관계 vs 비식별관계</h3>
<p>두 관계는 <strong>외래키가 기본키에 포함되는지의 여부</strong>에 따라 식별 관계와 비식별관계로 구분할 수 있다.</p>
<h4 id="식별-관계">식별 관계:</h4>
<p>부모 테이블의 기본키를 내려받아 자식 테이블의 기본키+외래키로 사용하는 관계</p>
<h4 id="비식별-관계">비식별 관계:</h4>
<p>부모 테이블의 기본키를 받아서 자식 테이블의 외래키로만 사용하는 관계</p>
<p>이 비식별 관계는 외래키에 널(NULL)을 허용하는지에 따라 다시 비식별 관계와 선택적 비식별 관계로 나뉨.</p>
<ul>
<li><strong>필수적 비식별 관계</strong>: 외래키에 NULL을 허용하지 않는다. 연관관계를 필수적으로 맺어야함</li>
<li><strong>선택적 비식별 관계</strong>: 외래키에 NULL을 허용한다. 연관관계를 맺을지 말지를 선택할 수 있다.</li>
</ul>
<p>최근에는 비식별 관계를 주로 사용하고 반드시 필요한 곳에만 식별관계를 사용하는 추세로 JPA는 둘 모두 지원한다.</p>
<hr>
<h3 id="2-복합키-비식별-관계-매핑">2. 복합키: 비식별 관계 매핑</h3>
<p>기본키를 구성하는 컬럼이 하나일 때</p>
<pre><code class="language-java">@Entity
public class Hello {
    @Id 
    private String id;
}</code></pre>
<p>둘 이상의 컬럼으로 구성된 복합 기본키의 경우, 식별자를 둘 이상 사용하기 위해서는 <strong>식별자 클래스</strong>를 별도로 만들어야한다. </p>
<p>이는 JPA가 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자 키로 사용하는데, 이 식별자 구분을 위해 <code>equals</code>와 <code>hashCode</code>를 사용해 동등성 비교를 하기 때문이다. 식별자 필드가 2개 이상이면 별도 식별자 클래스를 만들고 거기에 이 동등성 비교를 구현해야한다.</p>
<p>이런 복합키를 위해 JPA는 <code>@IdClass</code>와 <code>@EmbeddedId</code> 2가지 방법을 제공한다. <code>@IdClass</code>는 관계형 데이터베이스에 가까운 방법이고, <code>@EmbeddedId</code>가 객체지향에 가까운 방법이라고한다.</p>
<h4 id="idclass"><code>@IdClass</code></h4>
<pre><code class="language-java">@Entity
@IdClass(ParentId.class)
public class Parent {
    @Id 
    @Column (name = &quot;PARENT_ID1&quot;)
    private String id1;

    @Id
    @Column (name = &quot;PARENT_ID2&quot;)
    private String id2;

    private String name;
}</code></pre>
<p>각각의 기본키 컬럼을 <code>@Id</code>로 매핑한 후, <code>@IdClass</code>를 이용해 각 클래스를 식별자 클래스로 지정함.</p>
<pre><code class="language-java">public class ParentId implements Serializable {
    private String id1;
    private String id2;

    public ParentId() {}

    public ParentId(String id1, String id2) {
        this.id1 = id1;
        this.id2 = id2;
    }

    @Override
    public boolean equals (Object o) {...}

    @Override
    public boolean equals hashCode () {...}
}</code></pre>
<p><code>@IdClass</code> 사용 시 식별자 클래스는 아래 조건을 만족해야함.</p>
<ul>
<li>식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야함</li>
<li><code>Serializable</code> 인터페이스를 구현해야함</li>
<li><code>equals</code>, <code>hashCode</code>를 구현해야함</li>
<li>기본 생성자가 있어야함</li>
<li>식별자 클래스는 public이어야함</li>
</ul>
<pre><code class="language-java">Parent parent = new Parent();
parent.setId1(&quot;myId1&quot;);
parent.setId2(&quot;myId2&quot;);
parent.setName(&quot;parentName&quot;);
em.persist(parent);</code></pre>
<p>식별자 클래스가 없어도 <code>em.persist()</code> 호출 시 영속성 컨텍스트에 엔티티를 등록하기 전 내부에서 <code>Parent.id1</code>, <code>Parent.id2</code> 값을 사용해 식별자 클래스를 생성하고 영속성 컨텍스트의 키로 사용하게 됨.</p>
<p>복합키로 조회할 경우 아래와 같이 이루어진다.</p>
<pre><code class="language-java">ParentId parentId = new ParentId(&quot;myId1&quot;, &quot;myId2&quot;);
Parent parent = em.find(Parent.clss, parentId);</code></pre>
<p>식별자 클래스를 통해서 엔티티를 조회할 수 있다.</p>
<p>자식 클래스를 추가하는 코드는 아래와 같다.</p>
<pre><code class="language-java">@Entity
public class Child {
    @Id
    private String id;

    @ManyToOne
    @JoinColumns ({
        @JoinColumn(name = &quot;PARENT_ID1&quot;, referencedColumnName = &quot;PARENT_ID1&quot;),
        @JoinColumn(name = &quot;PARENT_ID2&quot;, referencedColumnName = &quot;PARENT_ID2&quot;)
    })
    private Parent parent;
}</code></pre>
<p>부모 테이블의 기본키 컬럼이 복합키이므로 자식 테이블의 외래키도 복합키가 된다. 즉, 외래키 매핑 시 여러 컬럼을 매핑해야하므로 <code>@JoinColumns</code> 어노테이션을 사용해야하고 각각의 외래키를 <code>@JoinColumn</code>으로 매핑해야한다.</p>
<hr>
<h4 id="embededid"><code>@EmbededId</code></h4>
<pre><code class="language-java">@Entity
public class Parent {
    @EmbeddedId
    private ParentId id;

    private String name;
}</code></pre>
<p>Parent 엔티티에서 식별자 클래스를 직접 사용하고 <code>@EmbeddedId</code> 어노테이션을 적어주면 된다. 식별자 클래스는 아래와 같다.</p>
<pre><code class="language-java">@Embeddable
public class ParentId implements Serializable {
    @Column (name = &quot;PARENT_ID1&quot;)
    private String id1;

    @Column (name = &quot;PARENT_ID2&quot;)
    private String id2;

    //equals와 hashcode 구현
}</code></pre>
<p><code>@EmbeddedId</code>를 적용한 식별자 클래스의 경우에는 식별자 클래스에 기본키를 직접 매핑하게 된다.</p>
<p><code>@EmbeddedId</code>를 적용한 식별자 클래스의 경우 아래 조건을 만족해야한다.</p>
<ul>
<li><code>@Embeddable</code> 어노테이션을 붙여줘야한다</li>
<li><code>Serializable</code> 인터페이스를 구현해야한다</li>
<li>equals, hashcode를 구현해야한다</li>
<li>기본 생성자가 필요하다</li>
<li>식별자 클래스는 public이어야한다</li>
</ul>
<p>엔티티를 저장하는 코드를 보면 아래와 같다</p>
<pre><code class="language-java">Parent parent = new Parent();
ParentId parentId = new ParentId(&quot;myId1&quot;, &quot;myId2&quot;);
parent.setId(parentId);
parent.setName(&quot;parentName&quot;);
em.persist(parent);</code></pre>
<p>엔티티를 조회하는 코드는 역시 parentId (식별자 클래스)를 직접 사용한다.</p>
<pre><code class="language-java">ParentId parentId = new ParentId(&quot;myId1&quot;, &quot;myId2&quot;);
Parent parent = em.find(Parent.class, parentId);</code></pre>
<h3 id="복합-키와-equals-hashcode">복합 키와 <code>equals()</code>, <code>hashCode()</code></h3>
<p>복합키를 사용할 때는 <code>equals()</code>와 <code>hashCode()</code>를 필수적으로 구현해야한다.</p>
<pre><code class="language-java">ParentId id1 = new parentId();
id1.setId1(&quot;myId1&quot;);
id2.setId2(&quot;myId2&quot;);

ParentId id2 = new parentId();
id2.setId2(&quot;myId1&quot;);
id2.setId2(&quot;myId2&quot;);

id1.equals(id2);</code></pre>
<p>위의 경우 id1과 id2 인스턴스 둘 모두 같은 값을 가짐에도 다른 인스턴스이기에 <code>equals()</code>를 사용하였을 때 거짓이 된다. 이는 기본으로 제공되는 <code>equals()</code>는 인스턴스 참조값 비교(동일성 비교)를 하기 때문이다.</p>
<p>영속성 컨텍스트는 엔티티의 식별자를 키로 사용해 엔티티를 관리하기에 동등성 비교가 지켜지지 않으면 예상과 다른 엔티티가 조회되거나 엔티티를 찾을 수 없는 문제가 발생하기에 복합키에서의 <code>equals()</code>와 <code>hashCode()</code> 구현은 필수적이다.</p>
<p>+) 복합키에는 GeneratedValue를 사용할 수 없다. 복합키를 구성하는 여러 컬럼 중 하나에도 역시 마찬가지이다.</p>
<hr>
<h3 id="3-복합키-식별관계-매핑">3. 복합키: 식별관계 매핑</h3>
<p>식별관계에서 자식 테이블은 부모 테이블의 기본키를 포함해 복합키를 구성해야하기에 <code>@IdClass</code>나 <code>@EmbeddedId</code>를 사용해 식별자를 매핑해야함.</p>
<h4 id="idclass와-식별관계"><code>@IdClass</code>와 식별관계</h4>
<pre><code class="language-java">//부모
public class Parent {
    @Id @Column (name = &quot;PARENT_ID&quot;)
    private String id;
    private String name;
}

//자식
@Entity
@IdClass(ChildId.class)
public class Child{
    @Id
    @ManyToOne
    @JoinColumn (name = &quot;PARENT_ID&quot;)
    public Parent parent;

    @Id @Column(name = &quot;CHILD_ID&quot;)
    private String childId;

    private String name;
}

//자식 ID
public class ChildId implements Serializable {
    private String parent; //Child.parent 매핑
    private String childId; //Child.childId 매핑

    //equals, hashCode 매핑
}

//손자
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {
    @Id
    @ManyToOne
    @JoinColumns ({
        @JoinColumn(name = &quot;PARENT_ID&quot;),
        @JoinColumn(name = &quot;CHILD_ID&quot;)
    })
    private Child child;

    @Id @Column(name = &quot;GRANDCHILD_ID&quot;)
    private String id;

    private String name;
}

//손자ID
public class GrandChildId implements Serializable {
    private ChildId child; //GrandChild.child 매핑
    private String id; //GrandChild.id 매핑

    //equals, hashCode 매핑
}</code></pre>
<p>식별 관계는 기본키와 외래키를 같이 매핑해야함. 따라서 식별자 매핑인 <code>@Id</code>와 연관관계 매핑인 <code>@ManyToOne</code>을 함께 사용해야한다.</p>
<pre><code class="language-java">@Id
@ManyToOne
@JoinColumn(name = &quot;PARENT_ID&quot;)
public Parent parent;</code></pre>
<p>Child 엔티티의 parent 필드를 보면 <code>@Id</code>를 통해 기본키로 매핑하면서 동시에 <code>@ManyToOne</code>과 <code>@JoinColumn</code>으로 외래키를 같이 매핑한다.</p>
<hr>
<h4 id="embeddedid와-식별-관계"><code>@EmbeddedId</code>와 식별 관계</h4>
<p><code>@EmbeddedId</code>로 식별 관계를 구성할 땐 <code>@MapsId</code>를 사용한다.</p>
<pre><code class="language-java">//부모
@Entity
public class Parent {
    @Id @Column (name = &quot;PARENT_ID&quot;)
    private String id;

    private String name;
}

//자식
@Entity
public class Child {
    @EmbeddedId
    private ChildId id;

    @MapsId(&quot;parentId&quot;) //ChildId.parentId 매핑
    @ManyToOne
    @JoinColumn (name = &quot;PARENT_ID&quot;)
    public Parent parent;

    private String name;
}

//자식 ID
@Embeddable
public class ChildId implements Serializable {

    private String parentId; //@MapsId(&quot;parentId&quot;)로 매핑

    @Column(name = &quot;CHILD_ID&quot;)
    private String id;

    //equals, hashCode ...
}

//손자
@Entity
public class GrandChild {
    @EmbededId
    private GrandChild id;

    @MapsId(&quot;childId&quot;) //GrandChildId.childId 매핑
    @ManyToOne
    @JoinColumns({
        @JoinColumn(name=&quot;PARENT_ID&quot;),
        @JoinColumn(name=&quot;CHILD_ID&quot;)
    })
    private Child child;

    private String name;
}

//손자ID
@Embeddable
public class GrandChildId implements Serializable {
    private ChildId childId; //@MapsId(&quot;childId&quot;)로 매핑

    @Column(name = &quot;GRANDCHILD_ID&quot;)
    private String id;

    //equals, hashCode...
}</code></pre>
<p><code>@MapsId</code>는 외래키와 매핑한 연관관계를 기본키에도 매핑한다는 의미이다. <code>@MapsId</code>의 속성값은 <code>@EmbeddedId</code>를 사용한 식별자 클래스의 기본키 필드를 지정하면 된다.</p>
<hr>
<h3 id="4-비식별-관계로의-구현">4. 비식별 관계로의 구현</h3>
<pre><code class="language-java">@Entity
public class Parent {
    @Id @GeneratedValue
    @Column (name = &quot;PARENT_ID&quot;)
    private Long id;
    private String name;
}

@Entity
public class Child {
    @Id @GeneratedValue
    private String name;

    @ManyToOne
    @JoinColumn(name = &quot;PARENT_ID&quot;)
    private Parent parent;
}

//손자
@Entity
public class GrandChild {
    @Id @GeneratedValue
    @Column(name = &quot;GRANDCHILD_ID&quot;)
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = &quot;CHILD_ID&quot;)
    private Child child;
}</code></pre>
<p>복합키가 없으니 복합키 클래스를 만들지 않아도 된다.</p>
<hr>
<h3 id="5-일대일-식별-관계">5. 일대일 식별 관계</h3>
<p>일대일 식별관계의 경우, 자식테이블의 기본키 값으로 부모테이블의 기본키값 만을 사용한다. 따라서 부모테이블의 기본키가 복합키가 아니라면, 자식 테이블의 기본키를 복합키로 따로 구성할 필요가 없다.</p>
<pre><code class="language-java">//부모
@Entity
public class Board {
    @Id @GeneratedValue
    @Column(name = &quot;BOARD_ID&quot;)
    private Long id;

    private String title;

    @OneToOne(mappedBy = &quot;board&quot;)
    private BoardDetail boardDetail;
}

//자식
@Entity
public class BoardDetail{
    @Id
    private Long boradId;

    @MapsId //BoardDetail.boardId 매핑
    @OneToOne
    @JoinColumn (name =&quot;BOARD_ID&quot;)
    private Board board;

    private String content;
}</code></pre>
<p>BoardDetail처럼 식별자가 단순히 컬럼 하나일 경우 <code>@MapsId</code>를 사용하고 속성값은 비워두면 된다. 이때 <code>@MapsId</code>는 <code>@Id</code>를 사용해 식별자로 지정한 BoardDetail.boardId와 매핑되게 된다.</p>
<pre><code class="language-java">public void save(){
    Board board = new Board();
    board.setTitle(&quot;제목&quot;)
    em.persist(board);

    BoardDetail boardDetail = new BoardDetail();
    boardDetail.setContent(&quot;내용&quot;);
    boardDetail.setBoard(board);
    em.persist(boardDetail);
}</code></pre>
<hr>
<h3 id="6-식별-비식별-관계의-장단점">6. 식별, 비식별 관계의 장단점</h3>
<p>데이터베이스 설계 관점에서는 비식별 관계를 더 선호한다.</p>
<ul>
<li>식별관계는 부모 테이블이 기본키를 자식 테이블로 전파하면서 <strong>자식 테이블의 기본키 컬럼이 점점 늘어나게된다</strong>. 이는 조인 시 SQL이 복잡해지고, 기본키 인덱스가 불필요하게 커지는 문제를 초래한다.</li>
<li>식별관계는 <strong>2개 이상의 컬럼을 합해 복합 기본키를 만들어야하는 경우가 많다</strong>.</li>
<li>식별 관계 사용시 기본키로 비즈니스 의미가 있는 자연키 컬럼을 조합하는 경우가 많은 반면, 비식별 관계의 기본키는 비즈니스와 전혀 상관없는 <strong>대리키를 주로 사용</strong>한다. <strong>비즈니스 요구사항은 변할 수 있기에 이런 자연키 컬럼들이 자식에, 손자까지 전파되면 변경하기 힘들다는 단점</strong>이 존재한다.</li>
<li>식별관계는 비식별 관계보다 <strong>테이블 구조가 유연하지 못하다</strong>.</li>
</ul>
<p>객체 관계 매핑의 관점에서는 아래 이유로 비식별 관계를 선호한다.</p>
<ul>
<li>일대일 관계 제외, 식별관계는 2개 이상의 컬럼을 묶은 <strong>복합 기본키</strong>를 사용한다. JPA에서 <strong>복합키는 별도의 복합 키 클래스를 만들어서 사용</strong>해야하기에 더 많은 노력이 필요하다.</li>
<li>비식별 관계의 기본키는 주로 대리키를 사용하는데 JPA는 <code>@GeneratedValue</code> 처럼 대리키 생성을 위한 편리한 방법을 제공한다.</li>
</ul>
<p>식별 관계를 사용할 때의 장점도 존재한다. 기본키 인덱스를 활용하기 좋고, 상위 테이블들의 기본키 컬럼을 자식, 손자 테이블들이 가지고 있기에 특정 상황에서는 조인 없이 하위 테이블만으로도 검색을 완료할 수 있다.</p>
<hr>
<h2 id="74-조인-테이블">7.4 조인 테이블</h2>
<p>데이터베이스 테이블의 연관관계 설계 방법 2가지.</p>
<ul>
<li>조인 컬럼 사용 (외래키)</li>
<li>조인 테이블 사용 (테이블 사용)</li>
</ul>
<h4 id="조인-컬럼">조인 컬럼</h4>
<blockquote>
<p>예) 회원과 사물함이 있을 때, 각 테이블에 데이터를 등록했다가, 회원이 원할 때 사물함을 선택할 수 있다.</p>
</blockquote>
<ul>
<li>외래키에 널을 허용하는 선택적 비식별 관계를 택할 경우, 항상 외부 조인을 사용해야한다</li>
<li>실수로 내부 조인을 사용하게 되면 관계가 없는 회원은 조회되지 않는다.</li>
<li>회원과 사물함이 만약 가끔 관계를 맺게되면 외래 키 값 대부분이 null로 저장되는 단점이 있음</li>
</ul>
<h4 id="조인-테이블-사용">조인 테이블 사용</h4>
<ul>
<li><p>조인 테이블의 경우 <strong>연관관계를 관리하는 조인 테이블</strong>을 추가하고, 두 테이블의 외래키를 가지고 연관관계를 관리한다. 즉, 회원과 사물함에는 연관관계를 관리하기 위한 외래키 컬럼이 존재하지 않는다.</p>
</li>
<li><p>회원과 사물함 데이터를 각각 등록하고, 회원이 원할 때 사물함을 선택하면 조인 테이블에만 값을 추가하면 된다.</p>
</li>
<li><p>조인 테이블의 단점은 테이블을 하나 추가해야한다는 점으로 관리해야하는 테이블이 늘어나고, 회원과 사물함 테이블을 조인하기 위해 조인 테이블까지 추가로 조인해야한다는 단점이 있다.</p>
</li>
</ul>
<p>따라서 기본은 조인 컬럼을 사용하고 필요 시에만 조인 테이블을 사용하는 것이 권장된다.</p>
<hr>
<h3 id="1-일대일-조인-테이블">1. 일대일 조인 테이블</h3>
<p>일대일 관계를 만들기 위해서는 조인 테이블의 외래키 컬럼 각각에 총 2개의 유니크 제약조건을 걸어야한다.</p>
<pre><code class="language-java">//부모
@Entity
public class Parent {
    @Id @GeneratedValue
    @Column(name = &quot;PARENT_ID&quot;)
    private Long id;
    private String name;

    @OneToOne
    @JoinTable(name = &quot;PARENT_CHILD&quot;,
        joinColumns = @JoinColumn(name = &quot;PARENT_ID&quot;),
        inverseJoinColumns = @JoinColumn(name=&quot;CHILD_ID&quot;)
        )
    private Child child;
}

//자식
@Entity
public class Child {
    @Id @GeneratedValue
    @Column (name = &quot;CHILD_ID&quot;)
    private Long id;
    private String name;
}</code></pre>
<p>부모 엔티티의 경우 <code>@JoinColumn</code>을 대신해 <code>@JoinTable</code>을 사용함.</p>
<h4 id="jointable의-속성"><code>@JoinTable</code>의 속성</h4>
<ul>
<li>name: 매핑할 조인 테이블의 이름</li>
<li>joinColumns: 현재 엔티티를 참조하는 외래키</li>
<li>inverseJoinColumns: 반대방향 엔티티를 참조하는 외래키</li>
</ul>
<p>양방향으로 매핑하기 위해서는 아래의 코드를 추가하면 된다.</p>
<pre><code class="language-java">public class Child {
    @OneToOne(mappedBy=&quot;child&quot;)
    private Parent parent;
}</code></pre>
<hr>
<h3 id="2-일대다-조인-테이블">2. 일대다 조인 테이블</h3>
<p>일대다 관계를 만들기 위해서는 조인 테이블의 컬럼 중 다와 관련된 커럼인 CHILD_ID에 유니크 제약조건을 걸어야한다. 일대다 단방향 관계로 매핑하면 아래와 같다.</p>
<pre><code class="language-java">//부모
@Entity
public class Parent {
    @Id @GeneratedValue
    @Column(name = &quot;PARENT_ID&quot;)
    private Long id;
    private String name;

    @OneToMany
    @JoinTable(name=&quot;PARENT_CHILD&quot;,
        joinColumns = @JoinColumn(name = &quot;PARENT_ID&quot;),
        inverseJoinColumns = @JoinColumn(name = &quot;CHILD_ID&quot;)
   )
   private List&lt;Child&gt; child = new ArrayList&lt;Chlid&gt;();
}

//자식
@Entity
public class Child {
    @Id @GeneratedValue
    @Column(name = &quot;CHILD_ID&quot;)
    private Long id;
    private String name;
}</code></pre>
<hr>
<h3 id="3-다대일-조인-테이블">3. 다대일 조인 테이블</h3>
<p>다대일의 경우 일대다에서 방향만 반대일 뿐, 조인 테이블의 모양은 일대다와 같다.</p>
<pre><code class="language-java">//부모
@Entity
public class Parent {
    @Id @GeneratedValue
    @Column (name = &quot;PARENT_ID&quot;)
    private Long id;
    private String name;

    @OneToMany(mappedBy = &quot;parent&quot;)
    private List&lt;Child&gt; child = new ArrayList&lt;&gt;();
}

//자식
@Entity
public class Child {
    @Id @GeneratedValue
    @Column(name = &quot;CHILD_ID&quot;)
    private Long id;
    private String name;

    @ManyToOne(optional = false)
    @JoinTable (name = &quot;PARENT_CHILD&quot;,
                joinColumns = @JoinColumn(name =&quot;CHILD_ID&quot;),
                inverseColumns = @JoinColumn(name=&quot;PARENT_ID&quot;)
    private Parent parent;
}</code></pre>
<hr>
<h3 id="4-다대다-조인-테이블">4. 다대다 조인 테이블</h3>
<p>다대다 관계를 만들기 위해서는 조인테이블의 두 컬럼을 합해서 하나의 복합 유니크 제약조건을 걸어야한다. </p>
<pre><code class="language-java">//부모
@Entity
public class Parent {
    @Id @GeneratedValue
    @Column(name = &quot;PARENT_ID&quot;)
    private Long id;
    private String name;

    @ManyToMany
    @JoinTable(name = &quot;PARENT_CHLID&quot;,
            joinColumns = @JoinColumn(name = &quot;PARENT_ID&quot;),
            inverseColumns = @JoinColumn(name = &quot;CHILD_ID&quot;)
       )
    private List&lt;Child&gt; child = new ArrayList&lt;&gt;();
}

//자식
@Entity
public class Child {
    @Id @GeneratedValue
    @Column(name = &quot;CHILD_ID&quot;)
    private Long id;
    private String name;
}</code></pre>
<p>조인 테이블에 다른 컬럼을 추가하게 되면 <code>@JoinTable</code> 전략을 사용할 수 없게된다. 대신 새로운 엔티티를 만들어서 조인 테이블과 매핑해야한다.</p>
<hr>
<h2 id="75-엔티티-하나에-여러-테이블-매핑">7.5 엔티티 하나에 여러 테이블 매핑</h2>
<p><code>@SecondaryTable</code>을 사용하면 하나의 엔티티에 여러 테이블을 매핑할 수 있다. (다만, 잘 사용하지는 않는다고 한다.</p>
<pre><code class="language-java">@Entity
@Table(name =&quot;BOARD&quot;)
@SecondaryTable (name = &quot;BOARD_DETAIL&quot;,
    pkJoinColumns = @PrimaryKeyJoinColumn(name = &quot;BOARD_DETAIL_ID&quot;))
public class Board{
    @Id @GeneratedValue 
    @Column (name = &quot;BOARD_ID&quot;)
    private Long id;

    private String title;

    @Column (table = &quot;BOARD_DETAIL&quot;)
    private String content;
}</code></pre>
<p>Board 엔티티의 경우 <code>@Table</code>을 통해 BOARD 테이블과 매핑한 후, <code>@SecondaryTable</code>을 통해 BOARD_DETAIL 테이블을 추가로 매핑한다.</p>
<h4 id="secondarytable-속성"><code>@SecondaryTable</code> 속성</h4>
<ul>
<li><code>.name</code>: 매핑할 다른 테이블의 이름.</li>
<li><code>.pkJoinColumns</code>: 매핑할 다른 테이블의 기본키 컬럼 속성.</li>
</ul>
<p>다만 최적화를 위해서는, 여러 테이블을 하나의 엔티티에 매핑하는 것보다는 테이블 당 엔티티를 각각 만들어 일대일 매핑하는 것이 권장된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 6주차 내용 정리]]></title>
            <link>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-6%EC%A3%BC%EC%B0%A8-%EB%82%B4%EC%9A%A9-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-6%EC%A3%BC%EC%B0%A8-%EB%82%B4%EC%9A%A9-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sat, 11 Apr 2026 03:04:59 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이론 정리 및 실습을 하나의 파일에 정리하였습니다. </p>
</blockquote>
<h1 id="3-뷰">3. 뷰</h1>
<h2 id="3-1-뷰view의-생성">3-1. 뷰(View)의 생성</h2>
<p><strong>뷰(view)</strong>: 하나 이상의 테이블을 합해 만드는 <strong>가상의 테이블</strong></p>
<ul>
<li>실제 데이터는 저장하지 않고, SELECT 쿼리의 결과에 이름을 붙여 재사용하는 객체<pre><code class="language-sql">CREATE VIEW &lt;뷰의 이름&gt; [열이름...]
AS &lt;SELECT문...&gt;</code></pre>
</li>
</ul>
<h4 id="특징">특징</h4>
<ul>
<li>데이터 보안- 특정 컬럼만 노출하도록 설정 가능</li>
<li>재사용성: 복잡한 쿼리를 뷰를 통해 재사용하며 단순화 가능</li>
<li>독립성: 기본 테이블의 구조를 변경해도 뷰를 통해 동일한 인터페이스를 제공</li>
<li>읽기 전용이 기본이지만, 일부의 경우에는 테이블 업데이트 역시 가능</li>
</ul>
<p>예) 뷰의 생성</p>
<pre><code class="language-sql">CREATE VIEW view_book
AS SELECT *
   FROM book
   WHERE bookname LIKE &#39;%축구%&#39;;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/f37294c0-bd18-416b-8a1c-6135616be174/image.png" alt="">
schemas 아래에 있는 view에서 생성된 뷰를 확인할 수 있다.</p>
<p>뷰의 생성 후에는 아래처럼 설정한 뷰의 이름을 통해 해당 뷰를 사용할 수 있음</p>
<pre><code class="language-sql">CREATE VIEW order_books
AS SELECT B.bookid, B.bookname, B.price, B.publisher
   FROM book B JOIN
        orders O ON B.bookid =O.bookid;

SELECT publisher, SUM(price) AS &#39;출판사별 수익&#39;
FROM order_books 
GROUP BY publisher;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/ba0dc832-7030-41e0-89d4-c17fdff70222/image.png" alt=""></p>
<h3 id="뷰의-장점-및-특징">뷰의 장점 및 특징</h3>
<h4 id="장점">장점</h4>
<ul>
<li>편리성 및 재사용성: 자주 사용되는 복잡한 질의를 뷰로 미리 정의해두고 사용할 수 있음</li>
<li>보안성: 사용자별로 필요한 데이터만 선별하여 보여줄 수 있고, 중요한 질의의 경우, 질의 내용을 암호화하는 것도 가능
예) 개인정보(주민번호)나 급여, 건강 같은 민감 정보를 제외한 뷰를 생성해 사용</li>
<li>독립성: 원본 테이블의 구조가 변해도 응용(사용)에 영향을 주지 않도록함</li>
<li><blockquote>
<p>논리적 데이터 독립성 제공 방법</p>
</blockquote>
</li>
</ul>
<h4 id="특징-1">특징</h4>
<ul>
<li><strong>원본 데이터 값에 따라 같이</strong> 뷰의 값이 변함</li>
<li>독립적인 인덱스 생성이 어려움</li>
<li>삽입/삭제/갱신 연산에 많은 제약 존재 (읽기 전용이 대다수)</li>
</ul>
<hr>
<h2 id="3-2-뷰의-수정">3-2. 뷰의 수정</h2>
<h3 id="replace">REPLACE</h3>
<p>뷰의 수정을 위해서는 <code>CREATE VIEW</code> 문에 <code>REPLACE</code> 명령을 추가.</p>
<pre><code class="language-sql">CREATE OR REPLACE VIEW &lt;뷰 이름&gt;
AS &lt;SELECT 문&gt;</code></pre>
<p>생성하거나 수정한다는 의미.</p>
<p>예) 앞에서 사용한 뷰에서 주문고객 정보까지 뷰에 포함하도록 수정</p>
<pre><code class="language-sql">CREATE OR REPLACE VIEW order_books
AS SELECT B.bookid, B.bookname, B.price, B.publisher, C.custid
   FROM book B JOIN
        orders O ON B.bookid = O.bookid JOIN
        customer C ON C.custid = O.custid;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/7c654688-7650-4faf-9ae7-24f35a029469/image.png" alt=""></p>
<h3 id="drop">DROP</h3>
<p>뷰를 삭제하기 위해서는 <code>DROP</code> 명령어를 사용함</p>
<pre><code class="language-sql">DROP VIEW &lt;뷰 이름&gt; </code></pre>
<p>예) </p>
<pre><code class="language-sql">DROP VIEW order_books;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/dac2c4ed-97d4-4e17-9635-1f8bc4068f07/image.png" alt="">
왼쪽의 Views에서 order_books가 사라진 것을 볼 수 있다.</p>
<hr>
<h2 id="3-3-뷰의-활용">3-3. 뷰의 활용</h2>
<blockquote>
<p>일반적으로 <strong>뷰는 읽기 전용</strong>이지만,. <strong>DISTINCT, GROUP BY</strong> 등의 제약이 없다면 변경이 가능하다</p>
</blockquote>
<h3 id="뷰의-활용-insert-update-delete-문">뷰의 활용: INSERT, UPDATE, DELETE 문</h3>
<ul>
<li>뷰에 대한 삽입/수정/삭제 연산은 제한적으로 수행된다.<ul>
<li>실제로 기본 테이블에 연산이 수행되니, 결과적으로 기본 테이블이 변경되는 것</li>
<li>변경 가능한 뷰와 불가능한 뷰가 존재한다</li>
</ul>
</li>
</ul>
<blockquote>
<h4 id="변경이-불가능한-뷰의-특징">변경이 불가능한 뷰의 특징</h4>
</blockquote>
<ul>
<li>기본 테이블의 <strong>기본키를 구성하는 속성이 포함되어 있지 않은</strong> 뷰</li>
<li>기본 테이블에서 <strong>NOT NULL로 지정된 속성이 포함되어있지 않은</strong> 뷰</li>
<li>기존 테이블에 있던 내용이 아닌 <strong>집계함수로 새로 계산된 내용을 포함</strong>하는 뷰</li>
<li>DISTINCT 키워드를 포함해 정의한 뷰</li>
<li>GROUP BY 절을 포함해 정의한 뷰</li>
<li>여러 개의 테이블을 조인해 정의한 뷰는 변경이 불가능한 경우가 많음.</li>
</ul>
<p>예) 변경이 불가능한 뷰의 예</p>
<img src="https://velog.velcdn.com/images/summeryoung_/post/e9306cc2-0c8e-4cc6-b61d-35a4197a521d/image.png" width=60%>

<p>View1</p>
<pre><code class="language-sql">CREATE VIEW view1
AS SELECT 제품번호, 재고량, 제조업체
   FROM 제품;
</code></pre>
<p>기본 테이블의 기본키인 제품번호를 포함하고 있기에 변경 가능. 
(기본키 속성은 NOT NULL)</p>
<p>View2</p>
<pre><code class="language-sql">CREATE VIEW view2
AS SELECT 제품명, 재고량, 제조업체
   FROM 제품;</code></pre>
<p>기본 테이블의 기본 키를 구성하는 속성이 포함되어 있지 않아 변경이 불가능함.
만약, 변경을 허용하는 경우 기본 키 속성의 값이 NULL이 되기에 안됨.</p>
<h4 id="뷰에-데이터-삽입하기">뷰에 데이터 삽입하기</h4>
<pre><code class="language-sql">INSERT INTO view1 VALUES (&#39;p08&#39;, 1000, &#39;신선식품&#39;);</code></pre>
<p>INSERT문은 기존 테이블에 작성하듯이 작성한다.</p>
<p>이 연산은 실제로는 <strong>기본 테이블(제품 테이블)</strong>에 수행 되기에 새로운 제품에 대한 튜플이 제품 테이블에 삽입된다.</p>
<p>** 슬라이드 60p. 실습12**
(1) 판매가격이 20,000원 이상인 도서의 도서번호, 도서이름, 고객이름, 출판사, 판매가격을 보여주는 highorders 뷰 생성하기</p>
<pre><code class="language-sql">CREATE VIEW highorders
AS SELECT B.bookid, B.bookname, C.name, B.publisher, B.price
   FROM book B JOIN
        orders O ON B.bookid = O.bookid JOIN
        customer C ON C.custid = O.custid
   WHERE B.price &gt;= 20000;</code></pre>
<p>(2) 생성한 뷰를 이용하여 판매된 도서의 이름과 고객의 이름을 출력하는 SQL문 작성하기</p>
<pre><code class="language-sql">SELECT bookname, name
FROM highorders;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/c92b031a-0625-4217-af5f-9a1f9dcecbcf/image.png" alt=""></p>
<p>(3) highorders 뷰를 변경하고자한다. 판매가격 속성을 삭제하는 명령을 수행하시오. 삭제 후 (2)번 SQL문 재수행.</p>
<pre><code class="language-sql">CREATE OR REPLACE VIEW highorders
AS SELECT B.bookid, B.bookname, C.name, B.publisher
   FROM book B JOIN
        orders O ON B.bookid = O.bookid JOIN
        customer C ON C.custid = O.custid
   WHERE B.price &gt;= 20000;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/c1aa0be9-9a14-4956-ac7f-5c4aaa3d533e/image.png" alt=""></p>
<hr>
<h1 id="4-인덱스">4. 인덱스</h1>
<h2 id="4-1-데이터베이스의-물리적-저장">4-1. 데이터베이스의 물리적 저장</h2>
<p>데이터베이스의 경우 각 DBMS 만의 고유한 방식으로 테이블에 저장되는 데이터를 저장함.</p>
<img src="https://velog.velcdn.com/images/summeryoung_/post/d4cbdbae-cffc-4220-b6b5-2a4a28a13d7b/image.png" width=60%>

<p>DBMS가 하드디스크에 데이터를 저장하고 읽어오면 속도가 느리기에, 주기억장치에 사용하는 공간 중 일부를 버퍼 풀로 만들어 사용함. 데이터 검색 시 이 버퍼 풀에 저장된 데이터를 우선 읽어들여 작업을 진행함.</p>
<p><code>SHOW VARIABLES LIKE &#39;datadir&#39;</code>를 통해 데이터베이스가 저장된 위치를 알아볼 수 있음</p>
<hr>
<h2 id="4-2-인덱스와-b-tree">4-2. 인덱스와 B-tree</h2>
<h3 id="인덱스">인덱스</h3>
<p>데이터를 쉽고 빠르게 찾을 수 있도록 만든 데이터 구조로, 일반적인 RDBMS의 인덱스는 대부분 B-tree(B+Tree) 구조로 이루어져 있음.</p>
<h3 id="b-tree">B-tree</h3>
<ul>
<li><strong>데이터 검색 시간</strong>을 단축하기 위한 자료구조</li>
<li>B-tree의 각 <strong>노드는 키값과 포인터</strong>를 가짐</li>
<li>루트노드, 내부노드, 리프노드로 구성</li>
<li>리프노드가 같은 레벨에 존재하는 균형 트리</li>
<li>리프노드에는 해당 데이터의 저장 위치에 대응하는 정보가 있어 빠르게 검색할 수 있음.</li>
<li>B Treed와 B-Tree는 balanced tree.</li>
</ul>
<img src="https://velog.velcdn.com/images/summeryoung_/post/79524279-9e38-4a32-aa0b-2157eb2fc782/image.png" width=80%>

<ul>
<li>B-tree에서의 검색: 루트 노드에서부터 값을 비교 -&gt; 중간 단계(내부 노드)에서 해당 노드를 찾음 -&gt; 최종적으로 마지막 레벨인 리프 노드에 도달</li>
</ul>
<h4 id="인덱스의-특징">인덱스의 특징</h4>
<ul>
<li>인덱스는 테이블에서 한 개 이상의 속성을 이용해 생성</li>
<li><strong>빠른 검색</strong>과 효율적인 레코드 접근이 가능</li>
<li>순서대로 정렬된 속성과 데이터의 위치만 보유하기에 테이블보다 작은 공간 차지</li>
<li>저장된 값들은 테이블의 부분집합이 됨</li>
<li>데이터에서 수정, 삭제 등의 변경이 발생하면 인덱스를 재구성해야함.</li>
</ul>
<hr>
<h2 id="4-3-mysql의-인덱스">4-3. MySQL의 인덱스</h2>
<p>MySQL의 인덱스는 <strong>클러스터 인덱스와 보조 인덱스</strong>로 나뉨</p>
<h3 id="클러스터-인덱스">클러스터 인덱스</h3>
<ul>
<li>테이블 자체가 인덱스 구조로 되어있어, 데이터가 인덱스 키 순서대로 물리적 저장됨</li>
<li>기본키 생성 시 자동으로 생성</li>
</ul>
<h4 id="특징-2">특징</h4>
<ul>
<li><strong>테이블 당 하나만 존재</strong></li>
<li>키 값이 정렬되어 특정값, 범위 검색에 유리</li>
<li>기본키 기반 검색이 매우 빠름</li>
<li>검색 성능은 뛰어나지만, 키 변경/삽입 시 성능 부담</li>
</ul>
<h3 id="보조-인덱스">보조 인덱스</h3>
<ul>
<li>클러스터 인덱스가 아닌 모든 인덱스</li>
<li>InnoDB에서는 PK(기본키)가 클러스터 인덱스이고, 나머지는 모두 보조 인덱스임.</li>
</ul>
<h4 id="특징-3">특징</h4>
<ul>
<li><p><strong>데이터 자체를 정렬하지 않고, 테이블 당 여러 개를 만들 수 있음</strong></p>
</li>
<li><p>인덱스 키와 해당 레코드의 기본키값을 저장해 검색될 수 있게 함</p>
<ul>
<li>즉, 인덱스의 리프 노드 = 테이블 상 데이터 위치를 지정하는 row id</li>
</ul>
</li>
</ul>
<h3 id="mysql-인덱스">MySQL 인덱스</h3>
<p>클러스터 인덱스와 보조 인덱스를 동시에 사용하는 검색.</p>
<p>예) 기본키인 <code>bookid</code>는 클러스터 인덱스, 외에 추가적으로 <code>bookname</code>을 보조 인덱스로 설정</p>
<h4 id="비교-정리">비교 정리</h4>
<table>
<thead>
<tr>
<th align="center">인덱스 명칭</th>
<th align="center">설명/생성 예</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>클러스터 인덱스</strong></td>
<td align="center">- <strong>기본적인 인덱스</strong>. 테이블 생성 시 기본 키 지정하면, 기본 키에 대해 클러스터 인덱스를 생성. <br> - 기본키 지정하지 않을 시에는 먼저 나오는 UNIQUE 속성에 대해 생성 <br> - 기본키/UNIQUE 모두 없을 시에는 MySQL 자체 생성 행번호로 생성 <br> - <strong>테이블 당 1개만 생성</strong></td>
</tr>
<tr>
<td align="center"><strong>보조 인덱스</strong></td>
<td align="center">- 클러스터 인덱스 외 모든 인덱스. 각 레코도는 보조 인덱스의 속성과 기본키 속성값을 가짐 <br> - 보조 인덱스 검색 -&gt; 기본 키 속성을 찾음 -&gt; 이후 클러스터 인덱스로 가서 해당 레코드를 찾는 방식. <br> -<strong>테이블 당 여러 개를 생성할 수 있음</strong></td>
</tr>
</tbody></table>
<hr>
<h2 id="4-4-인덱스의-생성">4-4. 인덱스의 생성</h2>
<p>의미없이 인덱스를 생성할 경우에는 오히려 검색속도가 더 느려지고 공간이 낭비됨</p>
<blockquote>
<h4 id="인덱스-생성-전-고려사항">인덱스 생성 전 고려사항</h4>
</blockquote>
<ul>
<li>인덱스는 <strong>WHERE절</strong>에서  자주 사용되는 속성이어야함</li>
<li>인덱스는 <strong>JOIN</strong>에 자주 사용되는 속성이어야함</li>
<li>단일 테이블에 인덱스가 많으면 속도가 느려질 수 있음 (테이블 당 4-5개 권장)</li>
<li>속성이 가공되는 경우에는 사용X
  예) YEAR(birth) = 2026</li>
<li>속성의 선택도가 낮을 때 유리 ( 즉, 속성의 모든 값이 다른 경우)
  예) 주민번호 - 선택도가 낮음 (고유한 값, 중복 없음), <pre><code>  성별 - 선택도 높음 (중복 많음)</code></pre></li>
</ul>
<h3 id="인덱스-생성-방법">인덱스 생성 방법</h3>
<pre><code class="language-sql">CREATE [UNIQUE] INDEX [인덱스 이름]
ON &lt;테이브 이름&gt; (컬럼 [ASC|DESC], ...)</code></pre>
<ul>
<li>UNIQUE: 테이블 속성값에 대해 중복이 없는 유일한 인덱스를 생성하는 것</li>
<li>ASC |DESC: 컬럼 값의 정렬 방식 의미.</li>
</ul>
<p>예)</p>
<pre><code class="language-sql">CREATE INDEX ix_Book ON book(publisher, price);</code></pre>
<h4 id="인덱스-미사용">인덱스 미사용</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/e725d16b-ca6a-4f82-8ebf-2cb9afbb339d/image.png" alt=""></p>
<h4 id="인덱스-사용">인덱스 사용</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/c6676c08-333d-454e-9933-b356a416ed30/image.png" alt=""></p>
<hr>
<h2 id="4-5-인덱스-재구성과-삭제">4-5. 인덱스 재구성과 삭제</h2>
<h3 id="인덱스--재구성">인덱스  재구성</h3>
<ul>
<li><p>B-tree 인덱스의 경우 데이터 수정/삭제/삽입이 잦으면 노드의 갱신이 주기적으로 일어나 단편화 현상이 일어남</p>
</li>
<li><p>)<strong>단편화(Fragmentation)</strong>: 데이터가 저장된 공간이 효율적으로 채워지지 않고 군데군데 빈틈(구멍)이 생겨 성능이 떨어지는 현상</p>
</li>
<li><p>이 경우, ANALYZE 문법을 통해 인덱스를 다시 생성해줌</p>
</li>
</ul>
<pre><code class="language-sql">ANALYZE TALBE book;</code></pre>
<h3 id="인덱스-삭제">인덱스 삭제</h3>
<pre><code class="language-sql">DROP INDEX ix_Book ON book;</code></pre>
<hr>
<h1 id="1-데이터베이스-프로그래밍의-개념">1. 데이터베이스 프로그래밍의 개념</h1>
<h3 id="데이터베이스-프로그래밍">데이터베이스 프로그래밍</h3>
<ul>
<li>DBMS에 데이터를 정의하고 저장된 데이터를 읽어와 데이터를 변경하는 프로그램을 작성하는 과정</li>
<li>데이터베이스 언어인 SQL을 포함하는 점이 일반 프로그래밍과의 차이점.</li>
</ul>
<h3 id="방법">방법</h3>
<ul>
<li>SQL 전용 언어 사용</li>
<li>일반 프로그래밍 언어에 SQL 삽입해 사용하기</li>
<li>웹 프로그래밍 언어에 SQL 삽입해 사용하기</li>
<li>4GL: 데이터베이스 관리 가능 비주얼 프로그래밍 기능을 갖춘 GUI 기반 소프트웨어 개발 도구 사용</li>
</ul>
<h4 id="데이터베이스-응용-시스템---하드웨어---운영체제---dbms--프로그램-환경으로-계층화">데이터베이스 응용 시스템 -&gt; &#39;하드웨어 - 운영체제 - DBMS -프로그램 환경&#39;으로 계층화</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/866fbfab-8360-47f2-ab99-2b4bbb21dd26/image.png" alt=""></p>
<hr>
<h1 id="2-저장-프로그램">2. 저장 프로그램</h1>
<h2 id="2-1-저장-프로그램">2-1. 저장 프로그램</h2>
<h3 id="저장-프로그램">저장 프로그램</h3>
<p>프로그램 로직을 <strong>프로시저</strong>로 구현해 객체 형태로 사용</p>
<ul>
<li>프로시저가 정의된 후 MySQL(DBMS)에 저장되기에 저장 프로그램이라 함</li>
<li>MySQL에서 저장프로그램을 정의하는과정:
  프로그램 정의 -&gt; 실행 -&gt; 실행 결과 -&gt; 개체확인</li>
</ul>
<h3 id="create-procedure">CREATE PROCEDURE</h3>
<ul>
<li>프로시저는 <strong>선언부와 실행부(BEGIN-END)</strong>로 구성</li>
<li>선언부: 변수와 매개변수 선언, 실행부: 프로그램 로직 구현</li>
<li>매개변수: 저장 프로시저가 호출될 때 해당 프로시저에 전달되는 값</li>
<li>변수: 저장 프로시저/트리거 내에서 사용되는 값</li>
<li>제어문 사용 가능.</li>
</ul>
<blockquote>
<h4 id="저장프로그램의-제어문">저장프로그램의 제어문</h4>
</blockquote>
<ul>
<li><code>DELIMITER</code>: 구문 종료 기호를 설정</li>
<li><code>BEGIN-END</code>: 프로그램문을 블록화. 중첩 가능</li>
<li><code>IF-ELSE</code>: 조건의 검삭 결과에 따라 문장 실행</li>
<li><code>RETURN</code>: 프로시저 종료 및 상태값 반환.</li>
<li><code>LOOP</code>: LEAVE 문을 만나기 전까지 LOOP 반복</li>
<li><code>WHILE</code>: 조건이 참일 경우 WHILE문읠 블록 실행</li>
<li><code>REPEAT</code>: 조건이 참일 경우 REPEAT 문의 블록 실행</li>
</ul>
<h3 id="프로시저procedure-실습">프로시저(PROCEDURE) 실습</h3>
<ul>
<li>DELIMITER 뒤에 오는 기호는 자유롭게 지정 가능 
  예: $$, //, ### 등</li>
</ul>
<h4 id="프로시저-생성">프로시저 생성</h4>
<pre><code class="language-sql">DELIMITER $$

CREATE PROCEDURE getAge(
    -- 선언부
    IN p_id INt,
    OUT p_name VARCHAR(50),
    OUT p_age INT
)

-- 실행부
BEGIN
    SELECT 이름, 나이 INTO p_name, p_age -- SELECT 후, 해당 값 전달 위치 지정해줘야함
    FROM person
    WHERE id = p_id; -- 쿼리의 마지막이니 ;(세미콜론) 필요
END $$ 

DELIMITER ;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/2ac3482a-5dc1-42e2-a9b5-a00ba9c7f67c/image.png" alt=""></p>
<h4 id="생성한-프로시저-사용">생성한 프로시저 사용</h4>
<pre><code class="language-sql">CALL getAge(2, @NAME, @age);
SELECT @NAME, @age;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/d42f62e8-56e9-40be-bd93-55de19274b2e/image.png" alt=""></p>
<hr>
<h2 id="2-2-트리거">2-2. 트리거</h2>
<h3 id="트리거">트리거</h3>
<p>테이블에 대한 <strong>특정 동작</strong>(INSERT, UPDATE, DELETE)이 수행될 때 <strong>자동으로 실행되는 저장된 프로그램</strong></p>
<h4 id="사용목적">사용목적</h4>
<ul>
<li><strong>데이터 무결성 유지</strong>: 잘못된 값 입력 방지, 자동 검증</li>
<li>자동 처리: 로그 기록, 변경 이력 관리</li>
<li>비즈니스 규칙 적용: 특정 조건 만족 시 자동 계산, 알림 처리</li>
<li>연관 테이블 동기화: 다른 테이블에 자동 반영</li>
</ul>
<h4 id="주의사항">주의사항</h4>
<ul>
<li>트리거는 자동 실행되기에 디버깅이 어렵고, 잘못 작성 시 성능 저하 우려</li>
<li>너무 많은 로직을 트리거에 넣으면 관리가 어려워짐</li>
<li>트리거는 트랜잭션과 밀접한 관련이 있기에 롤백 시 함께 취소됨.</li>
</ul>
<h3 id="트리거-실습">트리거 실습</h3>
<p>예) person 테이블에 새로운 행이 삽입될 때, 같은 이름이 이미 존재하면 로그 테이블에 기록하는 트리거 생성</p>
<pre><code class="language-sql">DELIMITER $$
CREATE TRIGGER check_duplicate_name
BEFORE INSERT ON person -- person 테이블에 삽입하기 전에
FOR EACH ROW
BEGIN
    DECLARE cnt INT;

    SELECT COUNT(*) INTO cnt
    FROM person
    WHERE 이름 = NEW.이름;

    IF cnt &gt; 0 THEN
        INSERT INTO person_alert (이름, action_time, message)
        VALUES (NEW.이름, NOW(), &#39;동명이인&#39;);
    END IF;

END $$
DELIMITER ;</code></pre>
<img src="https://velog.velcdn.com/images/summeryoung_/post/6e181bd9-4829-4233-9dc8-62c3455b4de9/image.png" width=50%>

<p>MySQLWorkbench의 경우 트리거를 생성한 테이블 아래에 생성된 트리거가 보인다.</p>
<h4 id="테스트-이름이-중복인-튜플-넣기">테스트: 이름이 중복인 튜플 넣기</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/76849c5d-7194-4b69-be70-6547eecc8e42/image.png" alt=""></p>
<hr>
<h2 id="2-3-사용자-정의-함수">2-3. 사용자 정의 함수</h2>
<h3 id="사용자-정의-함수">사용자 정의 함수</h3>
<p>개발자가 직접 작성하여 SQL 내에서 호출할 수 있는 함수.</p>
<ul>
<li>기본 제공 함수로 해결하기 어려운 로직을 캡슐화할 때 유용</li>
<li>SQL에서 자주 쓰는 계산이나 로직을 자신이 만든 함수로 등록해두고 필요할 때 호출</li>
</ul>
<h4 id="종류">종류</h4>
<ul>
<li>스칼라 함수: MySQL에서 일반적</li>
<li>테이블 반환 함수</li>
<li>인라인 함수</li>
</ul>
<h4 id="장점-1">장점</h4>
<ul>
<li>SQL 코드 간결화</li>
<li>유지보수에 용이</li>
<li>재사용성 증가</li>
</ul>
<h4 id="단점">단점</h4>
<ul>
<li>잘못 사용 시 성능 저하 가능</li>
</ul>
<h3 id="사용자-정의-함수-실습">사용자 정의 함수 실습</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/cabe8874-2f41-41de-82c6-40bccbb0146e/image.png" alt=""></p>
<pre><code class="language-sql">DELIMITER //
CREATE FUNCTION fnGetAge (birthdate DATE)
RETURNS INT
DETERMINISTIC
BEGIN
    DECLARE age INT;
    RETURN TIMESTAMPDIFF(YEAR, birthdate, CURDATE());
END //
DELIMITER ;

SELECT fnGetAge(&#39;2004-08-13&#39;) AS age;</code></pre>
<hr>
<h2 id="2-4-저장-프로그램의-특징">2-4. 저장 프로그램의 특징</h2>
<h4 id="프로시저">프로시저</h4>
<p>여러 SQL문의 묶음으로 단순 반복 업무에 권장. 복잡한 로직을 DB에 몰아넣으면 안됨.</p>
<h4 id="트리거-1">트리거</h4>
<p>특정 이벤트가 발생할 때 자동으로 실행. 디버깅, 예측 불가능한 성능 저하 문제 존재.</p>
<h4 id="사용자-정의-함수-1">사용자 정의 함수</h4>
<p>입력을 받아 단일값 또는 테이블 반환. 복잡한 연산을 함수로 감싸면 쿼리 최적화를 방해할 수 있음.</p>
<h3 id="비교">비교</h3>
<table>
<thead>
<tr>
<th align="center">구분</th>
<th align="center">프로시저</th>
<th align="center">트리거</th>
<th align="center">사용자 정의 함수</th>
</tr>
</thead>
<tbody><tr>
<td align="center">정의 방법</td>
<td align="center"><code>CREATE PROCEDURE</code></td>
<td align="center"><code>CREATE TRIGGER</code></td>
<td align="center"><code>CREATE FUNCTION</code></td>
</tr>
<tr>
<td align="center">호출 방법</td>
<td align="center"><code>CALL</code> 문으로 직접 호출</td>
<td align="center"><code>INSERT</code>, <code>DELETE</code>, <code>UPDATE</code> 문 실행 시 자동 실행</td>
<td align="center"><code>SELECT</code> 문에 포함됨</td>
</tr>
<tr>
<td align="center">기능 차이</td>
<td align="center">SQL문으로 할 수 없는 복잡한 로직 수행</td>
<td align="center">기본값 제공, 데이터 제약 준수, SQL 뷰의 수정, 참조무결성 작업 수행</td>
<td align="center">속성값을 가공해 반환, SQL문 내에서 직접 사용</td>
</tr>
</tbody></table>
<br>

<h3 id="실제-사용-예시">실제 사용 예시</h3>
<h4 id="트리거-2">트리거</h4>
<ul>
<li>히스토리(감사) 로그 기록: 고객 등급/결제 상태 변경 시 로그 테이블에 기록</li>
<li>통계 데이터 실시간 갱신: 게시글 증가 시, 총 게시글 수 증가</li>
</ul>
<h4 id="프로시저-1">프로시저</h4>
<ul>
<li>복잡한 주문/결제 처리: 주문 내역 생성 -&gt; 재고 차감 -&gt; 포인트 적립 -&gt; 쿠폰 사용 처리&#39;</li>
<li>배치 데이터 처리: 매일 새벽 2시, 유효기간 지난 미사용 쿠폰 일괄 만료 상태 변경</li>
</ul>
<h4 id="함수">함수</h4>
<ul>
<li>비즈니스 로직 계산: 상품 가격&amp;고객 등급 -&gt; 최종 할인 가격 반환</li>
<li>데이터 포맷팅/마스킹: 01012345678 입력시 010-1234-5678로 변환 또는 이름 홍*동으로 마스킹</li>
</ul>
<hr>
<h1 id="3-데이터베이스-연동-프로그래밍">3. 데이터베이스 연동 프로그래밍</h1>
<h4 id="java-주요-클래스">Java 주요 클래스</h4>
<ul>
<li><p><code>DriverManager</code>: </p>
<ul>
<li><code>getConnection()</code></li>
<li>DBMS 드라이버를 로드하고 연결 객체 생성</li>
</ul>
</li>
<li><p><code>Connection</code>: </p>
<ul>
<li><code>createStatement()</code>, <code>prepareStatement()</code>, <code>close()</code></li>
<li>특정 데이터베이스와 연결 세션을 관리. SQL 실행 객체 생성</li>
</ul>
</li>
<li><p><code>Statement</code>: </p>
<ul>
<li><code>executeQuery()</code>, <code>executeUpdate()</code></li>
<li>SQL문을 DB에 전달. 캐싱되지 않기에 정적 SQL 실행 시 주로 사용.</li>
</ul>
</li>
<li><p><code>PreparedStatement</code>: </p>
<ul>
<li><code>set...()</code>, <code>executeQuery()</code>, <code>executeUpdate()</code> </li>
<li>SQL을 미리 컴파일해 성능을 높이고, 매개변수를 사용해 보안 강화(SQL Injection 방지)</li>
</ul>
</li>
<li><p><code>ResultSet</code></p>
<ul>
<li><code>next()</code>, <code>get...()</code>, <code>close()</code></li>
<li>SELECT 쿼리 실행 결과를 테이블 형태로 저장, 커서를 이동시켜 데이터를 한 줄씩 조회</li>
</ul>
</li>
<li><p><code>SQLException</code></p>
<ul>
<li><code>getMessage()</code>, <code>getErrorCode()</code></li>
<li>DB 연동 과정에서 발생하는 예외상황에 대한 정보 제공</li>
</ul>
</li>
</ul>
<h3 id="연동">연동</h3>
<h4 id="연동-순서">연동 순서</h4>
<p>드라이버 로드 -&gt; Connection 생성 -&gt; Statement 생성 및 실행 -&gt; ResultSet 처리 -&gt; 자원 반납.</p>
<h4 id="preparedstatment-권장">PreparedStatment 권장</h4>
<p>보안, 가독성, 성능의 이유로 권장됨</p>
<h4 id="자원-반납try-with-resources">자원 반납(Try-with-resources)</h4>
<p><code>close()</code>를 일일이 호출하지 않아도 자동으로 자원을 닫아주는 구문.</p>
<hr>
<h2 id="solid-원칙">SOLID 원칙</h2>
<h3 id="solid란">SOLID란?</h3>
<p>SOLID  원칙이란 변화에 강하고 재사용에 유리한 클래스 구조를 만드는 법칙이자 원칙이라고한다. 앞에서 정리한 클린코드를 위해서 지켜야하는 법칙과도 같다. 이 원칙을 지키면서 코딩하면, 유지보수 및 확장이 편리하며 재사용성이 높고 테스트가 쉬운 코드를 작성할 수 있다.</p>
<table>
<thead>
<tr>
<th align="center"></th>
<th align="center"></th>
<th align="center"></th>
</tr>
</thead>
<tbody><tr>
<td align="center">S</td>
<td align="center">Single Responsibility</td>
<td align="center">단일 책임 원칙</td>
</tr>
<tr>
<td align="center">O</td>
<td align="center">Open/Closed</td>
<td align="center">개방-폐쇄 원칙</td>
</tr>
<tr>
<td align="center">L</td>
<td align="center">Liskov Substitution</td>
<td align="center">리스코프 치환 원칙</td>
</tr>
<tr>
<td align="center">I</td>
<td align="center">Interface Segregation</td>
<td align="center">인터페이스 분리 원칙</td>
</tr>
<tr>
<td align="center">D</td>
<td align="center">Dependency Inversion</td>
<td align="center">의존성 역전 원칙</td>
</tr>
</tbody></table>
<h3 id="jdbc에서의-solid">JDBC에서의 SOLID</h3>
<h4 id="srp단일-책임-원칙">SRP(단일 책임 원칙)</h4>
<p>하나의 클래스는 하나의 책임만을 가져야함.
DAO, DTO, View, Controller로 분리해야함</p>
<h4 id="ocp-개방-폐쇄-원칙">OCP (개방-폐쇄 원칙)</h4>
<p>확장에는 열려있고, 변경에는 닫혀있어야함
인터페이스 기반 DAO</p>
<h4 id="lsp-리스코프-치환-원칙">LSP (리스코프 치환 원칙)</h4>
<p>자식 클래스는 부모 클래스를 대체할 수 있어야함.
다형성 활용.</p>
<h4 id="isp-인터페이스-분리-원칙">ISP (인터페이스 분리 원칙)</h4>
<p>불필요한 인터페이스 의존은 피해야함.
역할별 인터페이스, DAO 인터페이스 정의</p>
<h4 id="dip-의존성-역전-원칙">DIP (의존성 역전 원칙)</h4>
<p>고수준 모듈은 저수준 모듈에 의존하면 안됨.
Service -&gt; DAO 인터페이스 주입.</p>
<h2 id="dao와-dto">DAO와 DTO</h2>
<p>View(입출력), Controller(흐름 제어), Service(비즈니스 로직 처리)와 함께 사용됨.
DAO에서 DB와 관련된 작업이 처리되거나 테이블의 데이터와 매핑되며, DTO는 이런 데이터를 전달하거나 받을 때 사용함.</p>
<h3 id="daomodel">DAO(Model)</h3>
<p>DB 접근 전담 CRUD 처리 클래스(JDBC를 사용하여 SQL 실행)로 DAO 인터페이스를 도입할수도 있음.
DB와 직접 통신한다.
예) 학생 정보 객체 </p>
<h3 id="dtodata-transter-object">DTO(Data Transter Object)</h3>
<p>데이터를 담아 다른계층으로 전달. 가볍고 단순해야 함.</p>
<hr>
<h2 id="jdbc-실습">JDBC 실습</h2>
<h3 id="jdbc">JDBC</h3>
<p>: 자바 애플리케이션이 데이터베이스에 연결하고 SQL문을 실행할 수 있도록 제공됟는 표준 API</p>
<p>과거 DB마다 접속 방법이 달랐으나, JDBC라는 통일된 인터페이스와 각 DB 제조사의 JDBC 드라이버 제공으로 <strong>JDBC 드라이버만 교체</strong>하면 자바 코드의 변경 없이 다양한 종류의 데이터베이스와 통신이 가능해짐.</p>
<h3 id="1-driver-라이브러리-추가">1. Driver 라이브러리 추가</h3>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/df172217-dba0-4c5a-b149-53cfa52f8891/image.png" alt=""></p>
<h3 id="2-데이터-베이스-연결">2. 데이터 베이스 연결</h3>
<pre><code class="language-java">String url = &quot;jdbc:mysql://127.0.0.1:3307/madangdb&quot;;
String username = &quot;&quot;;
String password = &quot;&quot;;

try (Connection connection = DriverManager.getConnection(url, username, password)) {
    ...
} catch (SQLException e) {
    System.out.println(&quot;DB연결 실패&quot;);
}
</code></pre>
<h3 id="3-sql문-실행">3. SQL문 실행</h3>
<ul>
<li>Statement/PreparedStatement</li>
<li>Connection 객체로부터 SQL을 실행할 수 있는 <code>Statement</code> 또는 <code>PreparedStatement</code> 객체를 생성.</li>
<li><code>statement</code>: 정적인 SQL문 실행에 사용</li>
<li><code>preparedStatement</code>: 성능과 보안 때문에 사용 권장.</li>
</ul>
<h3 id="4-결과처리-resultset">4. 결과처리 (ResultSet)</h3>
<ul>
<li>SELECT문을 실행한 후 반환되는 <code>ResultSet</code> 객체는 조회된 데이터의 집합</li>
<li><code>next()</code> 메소드를 호출해 한 행씩 이동하며 데이터를 읽어옴.</li>
</ul>
<hr>
<h2 id="jdbc-실습-결과">JDBC 실습 결과</h2>
<p>madangdb에서 만든 book, customer 데이터베이스에서 select 및 insert 쿼리 실행.</p>
<pre><code class="language-java">import java.sql.*;

public class Main {
    public static void main(String[] args) {
        String url = &quot;jdbc:mysql://127.0.0.1:3307/madangdb&quot;;
        String username = &quot;&quot;;
        String password = &quot;&quot;;

        try (Connection connection = DriverManager.getConnection(url, username, password)) {
            String selectSql = &quot;SELECT * FROM book&quot;;

            try (Statement stmt = connection.createStatement();
            ResultSet rs = stmt.executeQuery(selectSql)) {
                while (rs.next()) {
                    int bookid = rs.getInt(&quot;bookid&quot;);
                    String bookname = rs.getString(&quot;bookname&quot;);
                    String publisher = rs.getString(&quot;publisher&quot;);
                    int price = rs.getInt(&quot;price&quot;);

                    String result = &quot;bookid: &quot;+ bookid;
                    result += &quot; bookname: &quot;+ bookname;
                    result += &quot; publisher: &quot;+ publisher;
                    result += &quot; price: &quot;+price;

                    System.out.println(result);
                }
            }

            String insertSql = &quot;INSERT INTO customer (custid, name, address, phone) VALUES(?,?,?,?)&quot;;
            try (PreparedStatement pstmt = connection.prepareStatement(insertSql)) {
                pstmt.setInt(1,13);
                pstmt.setString(2, &quot;김이화&quot;);
                pstmt.setString(3, &quot;대한민국 서울&quot;);
                pstmt.setString(4,&quot;010-1111-2222&quot;);
                pstmt.executeUpdate();
            }
        } catch (SQLException e){
            System.out.println(&quot;연결 오류&quot;);
        }
    }
}</code></pre>
<h4 id="실습코드-사진">실습코드 사진</h4>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/63e59f3f-9ee1-43ef-b68d-a8761fab1fbd/image.png" alt=""></p>
<h3 id="실습-결과">실습 결과</h3>
<h4 id="select-쿼리">select 쿼리</h4>
<img src="https://velog.velcdn.com/images/summeryoung_/post/5e12c2e0-7f17-48b9-abf8-014b3aa7f0f6/image.png" width=80%>

<h4 id="insert-쿼리">insert 쿼리</h4>
<pre><code class="language-java">pstmt.setInt(1,13);
                pstmt.setString(2, &quot;김이화&quot;);
                pstmt.setString(3, &quot;대한민국 서울&quot;);
                pstmt.setString(4,&quot;010-1111-2222&quot;);
                pstmt.executeUpdate();</code></pre>
<img src="https://velog.velcdn.com/images/summeryoung_/post/bac2cbe9-1105-4c7a-969f-11c0b7d38000/image.png" width=70%>

]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 5주차 SQL 문제 풀이]]></title>
            <link>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-5%EC%A3%BC%EC%B0%A8-SQL-%EB%AC%B8%EC%A0%9C-%ED%92%80%EC%9D%B4</link>
            <guid>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-5%EC%A3%BC%EC%B0%A8-SQL-%EB%AC%B8%EC%A0%9C-%ED%92%80%EC%9D%B4</guid>
            <pubDate>Sat, 04 Apr 2026 09:20:27 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h4 id="q1-클래스-문제1-거래-상태에-따른-출력-및-날짜-비교">Q1. 클래스 문제1: 거래 상태에 따른 출력 및 날짜 비교</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/8e5fdbee-3409-4c33-9e32-c2aed313e49d/image.png" alt=""></p>
<pre><code class="language-sql">SELECT board_id, writer_id, title, price,
       CASE status
        WHEN &#39;SALE&#39; THEN &#39;판매중&#39;
        WHEN &#39;RESERVED&#39; THEN &#39;예약중&#39;
        WHEN &#39;DONE&#39; THEN &#39;거래완료&#39;
        ELSE status
       END AS STATUS
FROM used_goods_board
WHERE created_date = &#39;2022-10-05&#39;
ORDER BY board_id DESC;</code></pre>
<p>중고 거래 게시글 중에서 생성일자를 기준으로 한 번 거르고, 거래 상태에 따라 판매중/예약중/거래완료를 표기하는 문제였다.</p>
<p>날짜를 비교하는 부분은 날짜 그대로 비교하는게 성능적으로 더 좋다기에 <code>created_date = &#39;2022-10-05&#39;</code>와 같이 날짜 그대로 두고 비교하였다.</p>
<p>또 거래 상태가 SALE/RESERVED/DONE일 때에 따라 다른 식으로 표기해줘야하는 부분은 <code>CASE WHEN</code>을 사용했다. <code>WHEN</code> 뒤에 조건식을 써도 되지만, 단일값의 경우가 동일한지 (<code>=</code> 이 연산 비교) 에는 C언어나 다른 언어의 <code>switch</code>문처럼 CASE에 속성을 적고, WHEN 뒤에 값을 적어주면 되기에 그렇게 작성해주었다. </p>
<hr>
<blockquote>
<h4 id="q2-클래스-문제2-10월-생성된-글-및-댓글-조회">Q2. 클래스 문제2: 10월 생성된 글 및 댓글 조회</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/7134880d-ba94-4e51-8300-ad7c41d2cc58/image.png" alt=""></p>
<pre><code class="language-sql">SELECT B.title, B.board_id, 
       R.reply_id, R.writer_id, R.contents, 
       DATE_FORMAT(R.created_date, &#39;%Y-%m-%d&#39;)
FROM used_goods_board B JOIN
     used_goods_reply R ON
     B.board_id = R.board_id
WHERE MONTH(B.created_date) = &#39;10&#39;
ORDER BY R.created_date ASC, B.title;</code></pre>
<p>댓글 테이블과 게시글 테이블이 있었고, 댓글 테이블에서 게시글 id를 외래키로 가지면서 관계를 맺고 있는 식이었다.</p>
<p>10월에 만들어진 게시글과 그 댓글 등의 정보를 조회해서 출력하는 문제여서 일단은 <code>MONTH()</code>를 써서 비교했는데...? 이게 연도 조건이 있었던걸로 기억하는데 이렇게 풀면 연도는 사실 상관없이 10월에 만든 글이면 다 출력될텐데...? 어떻게 통과가 된 것 같다.</p>
<p>실제로는 <code>WHERE MONTH(B.created_date) = &#39;10&#39;</code> 이런식으로 쓰지 않고 그래서
<code>B.created_date BEWTEEN &#39;2022-10-01&#39; AND &#39;2022-10-31&#39;</code> 이렇게 쓰는게 더 좋을 것 같다. 또 찾아보니까, <code>B.created_date LIKE &#39;2022-10%&#39;</code> 이렇게 써도 10월인 날짜들만 걸러지는 것 같은데 이건 문자열로 비교하는거라 성능이 별로...다.</p>
<hr>
<blockquote>
<h4 id="q3-클래스-문제3">Q3. 클래스 문제3:</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/6c19d450-30a2-45d1-bb33-e9449c779fa5/image.png" alt=""></p>
<pre><code class="language-sql">-- 코드를 입력하세요
SELECT B.book_id, A.author_name, 
       DATE_FORMAT(B.published_date, &#39;%Y-%m-%d&#39;) AS PUBLISHED_DATE
FROM book B JOIN
     author A ON
     B.author_id = A.author_id
WHERE B.category =  &#39;경제&#39;
ORDER BY B.published_date;</code></pre>
<p>크게 복잡한건 없었고, 테이블 두 개를 JOIN으로 잘 연결한 후에 카테고리가 경제인 것들만 남기고, 날짜 형식만 요구대로 잘 조정해주었다.</p>
<p>날짜 형식은 <code>DATE_FORMAT()</code> 사용해서 연월일만 출력하도록 조정해주었다. </p>
<hr>
<blockquote>
<h4 id="q4-클래스-문제4-상품의-코드-앞-자리-두-개를-기준으로-분류">Q4. 클래스 문제4: 상품의 코드 앞 자리 두 개를 기준으로 분류</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/5175eda2-247e-40e5-86b9-7372b721a1c2/image.png" alt=""></p>
<pre><code class="language-sql">SELECT LEFT(product_code,2) AS category, 
       COUNT(DISTINCT product_id) AS product
FROM product P
GROUP BY category
ORDER BY category;</code></pre>
<p>처음에는 순간 앞 자리 두 개로 어떻게 분류해야하지? 했고, 약간 테이블을 하나 만들거나 서브 쿼리를 만들어야하나 생각했는데, <code>LEFT()</code>를 써서 분리하고 이걸 기준으로 그루핑하고, 정렬하면 되는 것이었다. 정렬은 SELECT 후에 진행되니 SELECT에서 만든 별칭으로 정렬해도 되고.</p>
<p><code>LEFT(속성,2)</code> 이렇게 해서 우선 상품 코드의 앞 두 개를 카테고리로 SELECT에서 정의해주었다.</p>
<p>그리고 위에서는 GROUP BY에 별칭 사용하긴 했는데 MySQL이나 MariaDB에서는 이렇게 GROUP BY에서 SELECT에서 사용한 별칭을 사용하는걸 허용하는데 Oracle에서는 허용하지 않는다고 하니 만약에 표준적으로 사용해야하는걸 생각하면</p>
<pre><code class="language-sql">GROUP BY LEFT(product_code,2)</code></pre>
<p>이렇게 적어야할 것 같았는데, Oracle에서는 LEFT도 없다고 하니? 그것조차 SUBSTR으로 바꾸어 주어야할 것 같다...</p>
<hr>
<blockquote>
<h4 id="q5-클래스-문제5-상품-출고-날짜를-기준으로-출력">Q5. 클래스 문제5: 상품 출고 날짜를 기준으로 출력</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/daa05c23-c4c9-4880-9242-79af79684b11/image.png" alt=""></p>
<pre><code class="language-sql">SELECT order_id, product_id, COALESCE(out_date,&#39; &#39;) AS out_date,
        CASE
        WHEN out_date &lt;= &#39;2022-05-01&#39; THEN &#39;출고완료&#39;
        WHEN out_date IS NULL THEN &#39;출고미정&#39;
        ELSE &#39;출고대기&#39;
        END AS &#39;출고여부&#39;
FROM food_order
ORDER BY order_id;</code></pre>
<p>날짜 기준으로 출고 여부를 잘 출력해줘야하는 문제였는데, 일단 처음에 위에처럼 쿼리를 작성해서 여러번 틀렸다.</p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/e985a20c-3687-4335-934f-4b1ecccdea44/image.png" alt=""></p>
<pre><code class="language-sql">SELECT order_id, product_id, DATE_FORMAT(out_date, &#39;%Y-%m-%d&#39;) AS out_date,
        CASE
        WHEN out_date &lt;= &#39;2022-05-01&#39; THEN &#39;출고완료&#39;
        WHEN out_date IS NULL THEN &#39;출고미정&#39;
        WHEN out_date &gt; &#39;2022-05-01&#39; THEN &#39;출고대기&#39;
        END AS &#39;출고여부&#39;
FROM food_order
ORDER BY order_id;</code></pre>
<p>코드를 바꿔보면서 위처럼 작성해야하는걸 깨달았는데. 일단 CASE WHEN으로 구분하는건 괜찮았고, NULL일 때 출고 미정인 부분들은 틀린게 없었는데 <code>out_date</code> 출력할 때 문제가 있었다.</p>
<ol>
<li>널 값을 굳이 다른 값으로 채울 필요가 없었다.</li>
<li>날짜 형식 지정을 잘 해줘야했다.</li>
</ol>
<p>처음에는 <code>COALESCE</code>를 사용해서 널인 경우에는 공백을 출력하도록 해줘야할 것 같아 그렇게 했는데 이렇게 하면 DATE_FORMAT을 하지 않아도 한 것처럼 연월일이 출력되어서 계속 그대로 돌렸는데, 일단 포맷 형식을 지정해줘야했었고, 공백을 출력하게 지정하지 않아도 됐다. 널은 왠지 널이라고 쓰여있어야할 것 같아서 그냥 공백이길래 &#39; &#39; 이걸 지정해줘서 문제였다.</p>
<p>다른게 문제인줄 알고 바꾸다가 CASE절도 바꾸어줬는데 마지막에 출고 대기는 그냥 전처럼 ELSE를 쓰는게 나을 것 같기도하다.</p>
<hr>
<blockquote>
<h4 id="q6-음식-종류별-즐겨찾기가-가장-많은-식당-출력">Q6. 음식 종류별 즐겨찾기가 가장 많은 식당 출력</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/f127983d-d0e5-4e62-9dfa-f8b1139d307f/image.png" alt=""></p>
<pre><code class="language-sql">SELECT food_type, rest_id, rest_name, favorites
FROM rest_info R1
WHERE favorites &gt;= (SELECT MAX(favorites)
                 FROM rest_info R2
                 WHERE R1.food_type = R2.food_type)
ORDER BY food_type DESC;</code></pre>
<p>이 문제가 음식 종류별로 이제 즐겨찾기가 가장 많은 행(식당)을 찾고 -&gt; 그 행(식당)의 정보를 출력하는 거였는데 이 즐겨찾기가 가장 많은 행은 <code>MAX()</code>로 찾는다해도, 그것과 비교를하려면 임시 테이블을 둬야할지 서브쿼리를 써야할 지 고민했다. 그리고 윈도우 함수 써서 rank 매기는 것도 방법일 것 같았다. (근데 서브쿼리가 훨씬 나을 것 같았다. 행 몇 개 두는 테이블을 만들 필요는 없을 것 같았다.)</p>
<p>WHERE절에 서브쿼리를 두었는데 (중첩질의) 처음에는 어떻게 <strong>조건에 부합하는 행(종류별 즐겨찾기가 가장 많은 그 행)만 뽑아낼 수 있을까</strong> 고민을 많이 했다.</p>
<p>상관질의 사용해서 상위 쿼리의 음식 종류와 하위 쿼리 음식 종류가 같은 것들로 1차로 조건 설정을 해주고, 그 안에서 이제 즐겨찾기 수를 토대로 걸러주면 됐다. 처음에는 <code>ALL</code>을 써서 했는데 없이 비교문만 사용해도 잘 돌아갔다.</p>
<p>하위 쿼리에서 해당 음식 종류의 최대 즐겨찾기 수를 찾고 그것과 상위 쿼리의 즐겨찾기 수를 행마다 비교하며 각 음식 종류의 즐겨찾기 수가 가장 많은 음식점 정보를 잘 출력하면 됐다.</p>
<hr>
<blockquote>
<h4 id="q7-클래스-문제7-렌트카-대여-가능-여부-출력">Q7. 클래스 문제7: 렌트카 대여 가능 여부 출력</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/899aa9d7-3538-4013-af31-4a3bd03ec559/image.png" alt=""></p>
<pre><code class="language-sql">SELECT car_id,
        MAX(CASE 
            WHEN &#39;2022-10-16&#39; BETWEEN start_date AND end_date
            THEN &#39;대여중&#39;
            ELSE &#39;대여 가능&#39;
        END) AS availability
FROM car_rental_company_rental_history
GROUP BY car_id
ORDER BY car_id DESC;</code></pre>
<p>차량 대여 기록이 엄청 많고, 빌렸던걸 또 빌리고 막 하는 기록들이 많아서 막상 테이블을 출력해보면 동일한 아이디가 대여중/대여 가능이 번갈아서 막 여러 번 나왔었다.</p>
<p>이걸 동일한 ID의 여러 기록들/값들 중에 어떻게 걸러내야하지? 라는 생각이 들었다. 그루핑하자는 생각이 들었는데 이 경우에는 대여중/대여 가능을 어떻게 선택할 조건을 설정할 수 없으니 랜덤하게 나와버린다.</p>
<p><code>car_id</code>를 기준으로 그룹화를 하고, SELECT절에서 CASE WHEN을 사용해 날짜 기준으로 조건의 날짜가 start_date나 end_date 사이에 있으면 대여중, 아니면 대여 가능을 반환하도록 했는데, 위의 문제는 해결하지 못했다.</p>
<p>이거 관련해서 AI한테 물어봤을 때는 &#39;대여중&#39; &#39;대여 가능&#39; 중에서 대여 중이 MAX를 썼을 때 더 크니까(뒤) 그렇게 하면 그루핑 후에 대여중+대여가능이 모두 뜰 경우 대여중으로 설정된다고 한다. 그래서 기존에 썼던 코드에서 SELECT의 CASE WHEN을 MAX로 감쌌는데 잘 돌아갔다.</p>
<p>약간 꼼수같은 방법이라 다른 사람들 풀이가 궁금해서 찾아봤는데 CASE WHEN 안에서 또 SELECT를 사용해서 IN으로 이제 하나라도 start_date와 end_date 사이에 해당 날짜가 끼여있는지 확인하면 해결하면 되는 것 같았다.</p>
<p>아래는 다시 풀었을 때...인데 의외로 또 간단하다.</p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/1192f3e1-d180-44f3-87ec-1b5264000d25/image.png" alt=""></p>
<pre><code class="language-sql">SELECT car_id,
       CASE
            WHEN car_id IN (SELECT car_id
                            FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY
                            WHERE &#39;2022-10-16&#39; 
                                   BETWEEN start_date AND end_date )
            THEN &#39;대여중&#39;
            ELSE &#39;대여 가능&#39;
       END AS availability
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY C
GROUP BY car_id
ORDER BY car_id DESC;</code></pre>
<hr>
<blockquote>
<h4 id="q8-클래스-문제8-1월-판매된-작가별-카테고리별-총액-출력">Q8. 클래스 문제8: 1월 판매된 작가별-카테고리별 총액 출력</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/0c39f65b-3c01-41bd-b11f-1e85928ef034/image.png" alt=""></p>
<pre><code class="language-sql">WITH sales AS (
    SELECT A.author_id, A.author_name,
       B.category,
       SUM(S.sales)*B.price AS total_sales
    FROM book_sales S JOIN
     book B ON S.book_id = B.book_id JOIN
     author A ON A.author_id = B.author_id
    WHERE MONTH(S.sales_date) = &#39;1&#39;
    GROUP BY B.category, S.book_id
)

SELECT author_id, author_name,
       category,
       SUM(total_sales) AS total_sales
FROM sales
GROUP BY author_id, category
ORDER BY author_id, category DESC;</code></pre>
<p>위에서는 CTE 사용해서 풀어보긴했는데 사실 안써도 풀 수 있는 문제였다.</p>
<p>일단은 작가와 카테고리 별로 묶어서 총 판매액을 출력하면 되는 문제였는데 그룹화를 어떤 기준으로 할 지 고민했었다.</p>
<p>이 문제에서도 위에처럼 일단 월 설정하는거는 다시해야할 것 같다.</p>
<p>일단은 위에 문제를 풀 때는 카테고리별로 묶고, 그 안에서 책의 아이디 별로 묶어서 해당 책이 얼마나 팔렸는지를 먼저 기록하고, 그걸 토대로 다시 작가 아이디, 카테고리별로 묶었는데 이거를 사실 그냥 <code>GROUP BY author_id, category, book_id</code>로 하면 됐다.</p>
<p>근데 이걸 처음에 했다가 위처럼 테이블을 하나 더 만든 이유는 <code>SUM()</code>을 사용해서 계산할 때, <code>book_id</code> 별로 다른 가격을 반영해서 총액 계산하는게 어려워서 였다.</p>
<p><code>SUM(S.sales)</code> 이렇게 해서 판매한 개수를 얻어오고 이 뒤에 곱셈을 해서 다른 가격 반영하는게 쉽지 않았는데 <code>SUM(S.sales*B.price)</code> 이런식으로? <code>SUM()</code> 안에 하나가 아니라 수식을 넣어도 되는걸 다 풀고 찾아보다 알았다....</p>
<p>다시 풀게 되면 아래처럼 풀 것 같다.
<img src="https://velog.velcdn.com/images/summeryoung_/post/a70fb3e5-5d10-47b4-9482-2fe35c10937c/image.png" alt=""></p>
<pre><code class="language-sql">SELECT A.author_id, A.author_name, B.category,
       SUM(S.sales * B.price) AS total_sales
FROM book B JOIN
     book_sales S ON B.book_id = S.book_id JOIN
     author A ON B.author_id = A.author_id
WHERE S.sales_date BETWEEN &#39;2022-01-01&#39; AND &#39;2022-01-31&#39;
GROUP BY A.author_id, B.category
ORDER BY author_id, category DESC;</code></pre>
<p>약간 SUM을 하게되면, 그룹한것들을 총 합계를 구하는거니까 <code>SUM(S.sales*B.price)</code>를 하게 되면 이제 행별로 각각 곱한 후에 -&gt; 작가&amp;카테고리별로 묶었으니 그 기준으로 다 합하고 행 줄이기. 이런식으로 진행되는 것 같다.</p>
<hr>
<blockquote>
<h4 id="q9-클래스-문제9-게시글의-수가-3개-이상인-고객-정보-출력">Q9. 클래스 문제9: 게시글의 수가 3개 이상인 고객 정보 출력</h4>
</blockquote>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/78953fdc-98eb-4480-a0fb-2cf192593dd4/image.png" alt=""></p>
<pre><code class="language-sql">WITH users AS (
    SELECT U.user_id AS user_id
    FROM used_goods_board B JOIN
        used_goods_user U ON
        B.writer_id = U.user_id
    GROUP BY U.user_id
    HAVING COUNT(DISTINCT B.board_id) &gt;= 3)

SELECT U.user_id, U.nickname, 
       CONCAT(U.city,&#39; &#39;,U.street_address1,&#39; &#39;, U.street_address2) AS &#39;전체주소&#39;,
       CONCAT(LEFT(TLNO,3),&#39;-&#39;,SUBSTR(TLNO,4,4),&#39;-&#39;,RIGHT(TLNO,4)) AS &#39;전화번호&#39;
FROM used_goods_user U LEFT JOIN
     users ON 
     U.user_id = users.user_id
WHERE users.user_id IS NOT NULL
ORDER BY U.user_id DESC;</code></pre>
<p>위에도 CTE 사용하기는 하는데, 사실 그럴 필요는 없었을 것 같긴하다. 그냥 분리하면 아래를 더 간단하게 쓸 수 있을 것 같았다...?</p>
<p>일단 위에서 3번 이상 게시글을 올린 사용자의 아이디만 테이블에 남겼다. 그룹화하고, <code>HAVING</code> 사용해서 이 부분은 풀었다.</p>
<p>그 후에 그렇게 만든 테이블을 기존 테이블하고 조인한 뒤에 널값 아닌 사용자의 정보들만 출력했다. 사실 CTE 만드는건 불필요한데 이번주 내용에서 배운 거라 사용하고 조인도 사용해봤다. </p>
<p>실제로 다시 풀게되면 그냥 바로 GROUP BY -&gt; HAVING 쓴 뒤에 SELECT에서 조건대로 처리할 것 같다. 아래처럼. </p>
<p>문제에서 신경써서 처리할 부분은 <code>CONCAT()</code> 써서 요구조건대로 문자열 만드는 부분이었다. 전화 번호 사이에 &#39;-&#39; 넣는 것이나 주소 연결하는 것만 잘 처리해주었다. (시키는대로 하면 되는데 그 조건이 은근 자잘해서 귀찮고 또 완전 안똑같으면 틀려서 몇 번 다시 풀었다.)</p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/ad23e531-fdc5-426b-8c6b-35ea54010307/image.png" alt=""></p>
<pre><code class="language-sql">SELECT U.user_id, U.nickname,
       CONCAT(U.city, &#39; &#39;, U.street_address1, &#39; &#39;, U.street_address2) AS &#39;전체주소&#39;,
       CONCAT(LEFT(U.tlno,3), &#39;-&#39;, SUBSTR(U.tlno,4,4), &#39;-&#39;, RIGHT(U.tlno,4)) AS &#39;전화번호&#39;
FROM used_goods_board B JOIN
     used_goods_user U ON B.writer_id = U.user_id
GROUP BY B.writer_id
HAVING COUNT(DISTINCT B.board_id) &gt;= 3
ORDER BY U.user_id DESC;</code></pre>
<p>CTE 만들지 않고 풀면 위와 같다. 이게 더 좋은 방법인 것 같긴하다, 위는 그냥 억지로 쓴 거...같고? 주소 연결할 때 공백을 빼먹어서 몇 번 다시 풀었었다....</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 5주차 내용 정리]]></title>
            <link>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-5%EC%A3%BC%EC%B0%A8-%EB%82%B4%EC%9A%A9-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@summeryoung_/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-5%EC%A3%BC%EC%B0%A8-%EB%82%B4%EC%9A%A9-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sat, 04 Apr 2026 05:36:17 GMT</pubDate>
            <description><![CDATA[<h1 id="1-내장함수-null-비교문">1. 내장함수, NULL, 비교문</h1>
<h2 id="1-1-sql-내장함수">1-1. SQL 내장함수</h2>
<ul>
<li><strong>상수나 속성 이름</strong>을 입력값으로 받아 <strong>단일 값</strong>을 결과로 반환</li>
<li>모든 내장함수는 사용 시 유효한 입력값을 받아야함</li>
<li><strong>SELECT절, WHERE절, UPDATE절</strong>에서 모두 사용 가능</li>
</ul>
<hr>
<h3 id="1-숫자-함수">(1) 숫자 함수</h3>
<ul>
<li>SQL문에서 수학의 기본적인 사칙 연산자와 나머지 연산자 기호를 그대로 사용</li>
<li>MySQL은 이러한 연산자 중 사용 빈도가 높은 것을 <strong>내장 함수</strong> 형태로 제공</li>
</ul>
<blockquote>
<ul>
<li><strong>ABS(숫자)</strong>: 숫자의 절댓값을 계산</li>
</ul>
</blockquote>
<ul>
<li><strong>CEIL(숫자), FLOOR(숫자)</strong>: 올림/내림</li>
<li><strong>ROUND(숫자, m)</strong>: 숫자의 반올림. m은 기준수.</li>
</ul>
<h4 id="abs-함수">ABS 함수</h4>
<ul>
<li>절댓값 구하는 함수<pre><code class="language-sql">SELECT ABS(-78), ABS(78);</code></pre>
<img src="https://velog.velcdn.com/images/summeryoung_/post/f2bd879e-35e9-43c5-a5a6-c8e97c6256e0/image.png" width=80%>

</li>
</ul>
<hr>
<h4 id="round-함수">ROUND 함수</h4>
<ul>
<li>반올림한 값을 구하는 함수<pre><code class="language-sql">SELECT ROUND(4.12345, 3);</code></pre>
<img src="https://velog.velcdn.com/images/summeryoung_/post/e5b79ce9-1425-4677-97a6-74fe6fe4aafa/image.png" alt=""></li>
</ul>
<h4 id="숫자함수의-연산">숫자함수의 연산</h4>
<ul>
<li>숫자함수에는 <strong>직접 숫자를 입력</strong>하거나 <strong>열 이름을 사용</strong>할 수 있음</li>
<li>여러 함수를 <strong>복합적으로 사용</strong>할 수도 있음</li>
</ul>
<pre><code class="language-sql">SELECT custid &#39;고객번호&#39;, ROUND(SUM(saleprice)/COUNT(*), -2) &#39;평균금액&#39;
FROM orders
GROUP BY custid;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/8eb02322-5261-4695-a999-3012a88dcf37/image.png" alt=""></p>
<hr>
<h3 id="2-문자함수">(2) 문자함수</h3>
<blockquote>
<h4 id="문자값-반환-함수">문자값 반환 함수</h4>
</blockquote>
<ul>
<li><strong>CONCAT(s1, s2)</strong>: 두 문자열을 연결</li>
<li><strong>LOWER(s), UPPER(s)</strong>: 대상 문자열을 모두 소문자로/대문자로 변환</li>
<li><strong>SUBSTR(s,n,k)</strong>: 대상 문자열을 지정된 자리에서부터, 지정된 길이만큼 잘라서 반환</li>
<li><strong>TRIM(c FROM s)</strong>: 대상 문자열의 양쪽에서 지정된 문자를 삭제. (문자열만 넣을 시 기본으로 공백 제거)</li>
</ul>
<blockquote>
<h4 id="숫자값-반환-함수">숫자값 반환 함수</h4>
</blockquote>
<ul>
<li><strong>LENGTH(s)</strong>: 대상 문자열의 바이트를 반환 (알파벳은 1바이트, 한글은 3바이트)</li>
<li><strong>CHAR_LENGTH(s)</strong>: 문자열의 문자 수를 반환</li>
</ul>
<h4 id="replace-함수">REPLACE 함수</h4>
<ul>
<li>문자열을 치환하는 함수<pre><code class="language-sql">SELECT bookid, REPLACE(bookname, &#39;야구&#39;, &#39;농구&#39;) bookname, publisher, price
FROM book;</code></pre>
<img src="https://velog.velcdn.com/images/summeryoung_/post/f0860463-da75-4628-9d3f-9376c3c0c301/image.png" alt=""></li>
</ul>
<h4 id="length-char_length-함수">LENGTH, CHAR_LENGTH 함수</h4>
<ul>
<li><code>LENGTH()</code>: 바이트 수를 가져오는 함수</li>
<li><code>CHAR_LENGTH()</code>: 문자의 수를 가져오는 함수</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/25395853-9428-4f7e-b317-604f7dc6eb05/image.png" alt=""></p>
<h4 id="이름-가리기masking-실습">이름 가리기(masking) 실습</h4>
<pre><code class="language-sql">SELECT CONCAT(LEFT(name,1), &#39;*&#39;,RIGHT(name,1)) AS marked_name
FROM customer;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/304f4660-cbd3-472e-b5ac-6021e6c33877/image.png" alt=""></p>
<pre><code class="language-java">SELECT
    CASE
        WHEN CHAR_LENGTH(name) = 2
            THEN CONCAT(LEFT(name,1),&#39;*&#39;)
        WHEN CHAR_LENGTH(name) &gt; 2
            THEN CONCAT(LEFT(name,1), REPEAT(&#39;*&#39;,CHAR_LENGTH(name)-2), RIGHT(name,1))
        ELSE name
    END
FROM customer;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/0c58796f-6269-46c9-a45d-38245370781a/image.png" alt=""></p>
<hr>
<h3 id="3-날짜시간-함수">(3) 날짜/시간 함수</h3>
<ul>
<li>날짜와 시간 부분을 나타내는 <strong><code>format</code></strong>으로 표기</li>
<li><code>format</code>은 날짜 형식 지정자로 날짜와 시간 부분을 표기하기 위해 특별한 규칙을 가짐.</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li><code>STR_TO_DATE(string format)</code>: 문자열 데이터를 날짜형으로 반환</li>
<li><code>DATE_FORMAT(date format)</code>: 날짜형 데이터를 문자열로 반환</li>
<li><code>ADDDATE(date interval)</code>: DATE형의 날짜에서 INTERVAL 지정한 시간만큼 더함</li>
<li><code>DATE(date)</code>: DATE형의 날짜 부분을 반환</li>
<li><code>DATEDIFF(date1, date2)</code>: DATE형의 date1-date2 날짜 차이를 반환함</li>
<li><code>SYSDATE</code>: DBMS 상의 오늘 날짜를 반환.</li>
</ul>
<ul>
<li>DBMS마다 함수 이름과 동작이 다름</li>
<li>날짜형 데이터는 &#39;-&#39;와 &#39;+&#39;를 사용하여 원하는 날짜로부터 이전(-)과 이후(+)를 계산할 수 있음</li>
</ul>
<blockquote>
<p><strong>📌 날짜, 시간함수 사용 주의점</strong><br></p>
</blockquote>
<ol>
<li>DBMS마다 이름과 동작, 의미가 다름</li>
<li>타임존 문제</li>
</ol>
<ul>
<li><code>NOW()</code>나 <code>CURRENT_TIMESTAMP</code>는 DB 서버의 타임존을 기준으로 반환.</li>
<li>서버와 사용자가 다른 지역에 있으면 시간이 어긋날 수 있으므로, 필요시 따로 맞춰줘야함.</li>
</ul>
<ol start="3">
<li>날짜 포맷 출력: <strong><code>DATE_FORMAT()</code></strong>(MySQL) <strong><code>TO_CHAR()</code></strong>(Oracle/Postgres) 포맷 지정</li>
<li><strong>NULL 처리</strong>: </li>
</ol>
<ul>
<li>날짜 컬럼이 NULL이면 함수 적용 시 에러가 발생</li>
<li>기본값으로 설정해둬야함: <code>IFNULL()</code>, <code>COALESCE()</code> 사용해서 널 처리.</li>
</ul>
<ol start="5">
<li>성능 고려: select에서 조회용으로 사용할 때</li>
</ol>
<ul>
<li>다른 부분에서 속성을 함수를 사용해 변환하면 <strong>인덱스에 저장된 값이 아니라 함수 결과를 새로 계산</strong>하여 <strong>테이블 풀 스캔</strong>을 할 가능성이 높음</li>
<li>테이블 풀스캔의 경우 성능이 떨어짐</li>
<li><code>WHEN DATE(order_date) = &#39;2026-04-02&#39;</code> 보다 <code>WHEN order_date &gt;= &#39;2026-03-24 00:00:00&#39; AND order_date &lt; &#39;2026-03-39 00:00:00&#39;</code>가 좋음.</li>
</ul>
<span style="font-size:15px; color:gray">
+ [ MariaDB에서 `NOW()`와 `SYSDATE()`] <br>
NOW(): 쿼리 실행 시작 시점의 시간을 반환. 같은 쿼리 내에서는 항상 동일한 값으로 유지
SYSDATE(): 함수 호출 순간의 시스템 시간을 반환. 같은 쿼리 내에서도 호출 시점마다 값이 달라짐.
</span>

<h4 id="1-adddatedate-interval">(1) ADDDATE(date, interval)</h4>
<p><code>ADDDATE(&#39;날짜&#39;, &#39;INTERVAL 수치단위&#39;)</code></p>
<ul>
<li>지정한 날짜에 <strong>일(day) 또는 시간(interval)</strong>을 더해 새로운 날짜를 반환하는 함수</li>
</ul>
<pre><code class="language-sql">SET @value = &#39;2024-04-01&#39;;

SELECT ADDDATE(@value, INTERVAL -10 DAY) &quot;BEFORE&quot;, ADDDATE(@value, INTERVAL 10 DAY) &quot;AFTER&quot;;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/0f73b1c9-e746-4d47-ac48-18ae84a0918a/image.png" alt=""></p>
<h4 id="2-format의-주요-지정자">(2) format의 주요 지정자</h4>
<p><strong>1. 요일</strong></p>
<ul>
<li><code>%w</code>: 요일 순서 (0~6, Sunday=0)</li>
<li><code>%W</code>: 요일 (Sunday~Saturday)</li>
<li><code>%a</code>: 요일의 약자(Sun~Sat)<br>

</li>
</ul>
<p><strong>2. 날짜</strong></p>
<ul>
<li><code>%d</code>: 한 달 중 날짜 (00~31)</li>
<li><code>%j</code>: 1년 중 날짜 (001~366)<br>

</li>
</ul>
<p><strong>3. 시간</strong></p>
<ul>
<li><code>%h</code>: 12시간 (0~12)</li>
<li><code>%H</code>: 24시간 (0~24)</li>
<li><code>%i</code>: 분 (0~59)</li>
<li><code>%s</code>: 초(0~59)<br>

</li>
</ul>
<p><strong>4. 월</strong></p>
<ul>
<li><code>%m</code>: 월 순서 (01~12, January = 01)</li>
<li><code>%M</code>: 월 이름 (January ~ December)</li>
<li><code>%b</code>: 월 이름 약어(Jan~Dec)</li>
</ul>
<br>

<p><strong>5. 연도</strong></p>
<ul>
<li><code>%Y</code>: 4자리 연도</li>
<li><code>%y</code>: 4자리 연도의 마지막 2자리</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/f87ee528-21cf-489e-ac88-673aa1e9b648/image.png" alt=""></p>
<h4 id="3-str_to_date-함수-date_format-함수">(3) STR_TO_DATE 함수, DATE_FORMAT 함수</h4>
<ul>
<li><code>STR_TO_DATE</code>: CHAR 형(문자열)으로 저장된 날짜를 DATE형으로 변환</li>
<li><code>DATE_FORMAT</code>: 날짜형을 문자형으로 변환함</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/2ed3003e-8d13-490a-bf59-6face4998514/image.png" alt=""></p>
<h4 id="4-sysdate-함수">(4) SYSDATE 함수</h4>
<ul>
<li>데이터베이스에 설정된 현재 날짜와 시간을 반환하는 함수</li>
</ul>
<hr>
<h2 id="1-2-null-값-처리">1-2. NULL 값 처리</h2>
<h3 id="널null-값">널(NULL) 값</h3>
<ul>
<li><p>아직 <strong>지정되지 않은 값</strong>, 즉 값을 알 수도 없고 적용할 수도 없음</p>
</li>
<li><p>&#39;0&#39;이나 빈 문자  또는 공백과는 다른 특별한 값으로 **비교연산자로 비교할 수도 없고, 연산 수행의 결과도 NULL로 반환됨.</p>
</li>
<li><p>NULL 값에 대한 연산 및 집계함수:</p>
<ul>
<li><strong>NULL + 숫자</strong>의 연산 결과는 <strong>NULL</strong></li>
<li>집계 함수를 사용할 때에 <strong>NULL이 포함된 행은 집계에서 제외</strong> (해당 행이 하나도 없을 경우, SUM, AVG의 결과는 NULL, COUNT 함수의 결과는 0이됨)</li>
</ul>
</li>
</ul>
<br>

<h3 id="ifnull-함수">IFNULL 함수</h3>
<ul>
<li><p>NULL 값을 다른 값으로 대치하여 연산하거나 다른 값으로 출력</p>
</li>
<li><p><code>IFNULL(속성, 값)</code>의 형태로 사용하며 속성값이 널일때 &#39;값&#39;으로 대치한다.</p>
</li>
<li><p>MySQL, MariaDB에서 사용</p>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/6d11f5b0-dcde-4b14-a4cc-9a2969c9209a/image.png" alt=""></p>
</li>
</ul>
<h3 id="coalesce-함수">COALESCE 함수</h3>
<ul>
<li>표준 SQL에서 지원</li>
<li><code>COALESCE(인자1, 인자2,...)</code>: 여러 개의 인자 중 널값이 아닌 첫 번째 값을 반환하며 <code>IFNULL</code>과 마찬가지로 <code>COALESCE(속성, &#39;값&#39;)</code>으로 작성하면 널 값을 &#39;값&#39;으로 채워서 출력할 수 있음.</li>
</ul>
<hr>
<h2 id="1-3-행번호-출력">1-3. 행번호 출력</h2>
<h3 id="set">SET</h3>
<ul>
<li>MySQL에서는 <strong>변수</strong>는 이름 앞에 <strong><code>@</code></strong> 기호를 붙이며, <strong>치환문에는 SET과 <code>:=</code> 기호를 사용</strong>한다.</li>
</ul>
<pre><code class="language-sql">SET @seq:=0;

SELECT (@seq := @seq+1) &#39;순번&#39;, custid, name, phone
FROM customer
WHERE @seq &lt; 2;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/72944518-6a7a-456d-b292-c23a3ae63b00/image.png" alt=""></p>
<p>위처럼 적었을 때는 결과가 생각한 것처럼 잘 나오는데, 좀 다르게 <code>@seq &lt;2</code>인 경우에만 출력하려고 하면, 예상과는 다르게 아래처럼 나온다.</p>
<pre><code class="language-sql">SET @seq:=0;

SELECT (@seq := @seq+1) &#39;순번&#39;, custid, name, phone
FROM customer
WHERE @seq &gt; 2;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/1a928ae9-7720-43d1-a40f-38659d5e7e5b/image.png" alt=""></p>
<p>이유는 쿼리 실행 시 동작 순서와 관련이 있다.</p>
<p><code>@seq</code>를 증가시켜주는 부분이 SELECT에 있어서 인데, WHERE절에서 조건을 체크하고 그 행이 조건에 맞으면 이제 SELECT로 가게되는데, 위의 경우에는 처음부터 <code>@seq</code>가 조건에 맞지 않아 SELECT절로 이동하지 않으면서 <code>@seq</code>가 그 뒤의 행들에서도 전혀 증가되지 않고 계속 0으로 남아있게 된다.</p>
<p>그래서 만약에 위에처럼 쿼리를 작성하고 싶으면 아래처럼 <code>@seq</code> 증가시켜주는 부분을 WHERE 절에서 하는 하도록 작성해주면 되었다.</p>
<p>이게 쿼리 실행했을 때 <strong>각 행 별로</strong> 이제 쿼리의 내용대로 돌아가면서? 실행? 조건 체크? 등이 이루어지는 식이고 그 행 별로 이제 적용이 되는? 느낌이었다.</p>
<pre><code class="language-sql">SET @seq:=0;

SELECT (@seq) &#39;순번&#39;, custid, name, phone
FROM customer
WHERE (@seq := @seq+1) &gt; 2;</code></pre>
<hr>
<h2 id="1-4-case-when">1-4 CASE WHEN</h2>
<ul>
<li><strong>조건에 따라 다른 값을 반환</strong></li>
<li>IF-THNE-ELSE와 비슷하게 동작하며 <strong>집계함수와 함께 쓰거나 출력 컬럼을 가공</strong>할 때 유용하다</li>
</ul>
<pre><code class="language-sql">CASE
    WHEN 조건1 THEN 결과1 #WHEN: 조건 지정
    WHEN 조건2 THEN 결과2 #THEN: 조건이 참이면 반환할 값
    ...
    ELSE 기본값 #ELSE: 모든 조건이 거짓일 때 반환할 기본값
END AS 별칭 #END: CASE문 종료</code></pre>
<p>예) 국내 거주자/국외 거주자 출력</p>
<pre><code class="language-sql">SELECT custid, name,
    CASE
        WHEN address LIKE &#39;%대한민국%&#39;
        THEN &quot;국내&quot;
        ELSE &quot;국외&quot;
    END AS &quot;국적&quot;, phone
FROM customer;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/ba26ec45-a864-4409-84f4-3fd4801db554/image.png" alt=""></p>
<hr>
<h1 id="2-부속질의">2. 부속질의</h1>
<h2 id="2-1-부속질의-서브쿼리">2-1. 부속질의 (=서브쿼리)</h2>
<ul>
<li>하나의 SQL문 안에 <strong>다른 SQL문이 중첩</strong>된 질의</li>
<li>주로 메인 쿼리의 조건에 따라 서브 쿼리의 결과를 가져와서 메인 쿼리에서 사용하는 용도로 활용</li>
<li>다른 테이블에서 가져온 데이터로 현재 테이블의 정보를 찾거나 가공하는 등의 작업 수행 가능</li>
<li>조인을 사용하는 방법도 있지만 서브 쿼리가 더 유리한 경우에 서브 쿼리를 사용</li>
</ul>
<blockquote>
<p><strong>부속 질의가 유리할 때</strong></p>
</blockquote>
<ul>
<li><strong>필터링 대상이 매우 적을 때</strong>: 서브 쿼리의 결과값이 단 하나임이 보장되고, 메인 테이블의 양이 방대할 때</li>
<li><strong>복잡한 집계가 포함될 때</strong>: 조인으로 풀면 중복 데이터가 너무 많이 발생해 계산이 꼬이는 경우.<br></li>
<li><code>EXPLAIN</code>을 사용하면 쿼리가 훑고간 행의 수, 실제 남은 데이터의 비율, 인덱스를 사용하는(using index) 아니면 풀테이블 스캔(using filesort)를 하는지 등을 확인해 볼 수 있음.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/25bf3c9d-7b50-4ff7-b7d1-083037d1c9bc/image.png" alt=""></p>
<h4 id="부속질의의-종류">부속질의의 종류</h4>
<table>
<thead>
<tr>
<th align="center">부속질의</th>
<th align="center">실무 용어</th>
<th align="center">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="center">WHERE 부속질의</td>
<td align="center"><strong>중첩질의</strong></td>
<td align="center">WHERE 절에서 술어와 같이 사용되며 결과를 한정. <br> <strong>상관 또는 비상관</strong> 형태</td>
</tr>
<tr>
<td align="center">SELECT 부속질의</td>
<td align="center"><strong>스칼라 부속질의</strong></td>
<td align="center">SELECT 절에서 사용되며 <strong>단일값</strong>을 반환</td>
</tr>
<tr>
<td align="center">FROM 부속질의</td>
<td align="center"><strong>인라인 뷰</strong></td>
<td align="center">FROM 절에서 결과를 뷰 형태로 반환</td>
</tr>
</tbody></table>
<br>

<h3 id="where-부속질의">WHERE 부속질의</h3>
<ul>
<li>중첩질의. 보통 데이터를 <strong>선택 하는 조건 혹은 술어</strong>와 같이 사용.</li>
<li>비교/집합/한정/존재 연산자와 사용.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/ad06486d-49ae-4659-9495-37a1d7abf0dd/image.png" alt=""></p>
<h4 id="비교연산자">비교연산자</h4>
<ul>
<li>비교 연산자 사용 시 <strong>부속질의가 반드시 단일 행, 단일 열을 반환</strong>해야하며, 아닐 경우에는 질의를 처리할 수 없다.</li>
<li>주질의 대상 열 값과 결과 값을 비교 연산자에 적용하여, 참인 경우에만 주질의의 해당 열을 출력한다.</li>
</ul>
<h4 id="in-not-in-집합-연산자">IN, NOT IN (집합 연산자)</h4>
<ul>
<li>IN/NOT IN 연산자는 <strong>주질의의 속성값이 부속질의에서 제공한 결과 집합에 있는지/없는지 확인</strong>하는 역할</li>
<li>주질의는 WHERE절에 사용되는 속성값을 부속질의의 결과 집합과 비교해 하나라도 있으면 참이 됨</li>
</ul>
<h4 id="all-someany한정-연산자">ALL, SOME/ANY(한정 연산자)</h4>
<ul>
<li>하나의 결과값이 아니라 여러 결과값과 비교할 수 있게 해줌</li>
<li>ALL: 모든 결과값보다~ (크거나 작다 등 비교연산자 사용)</li>
<li>SOME/ANY: 반환한 결과값 중 하나라도 조건 만족</li>
</ul>
<pre><code class="language-sql">#예시
SELECT 이름 FROM 학생 
WHERE 키 &gt; ALL (SELECT 키 FROM 학생 WHERE 학년 = 3);

SELECT 이름 FROM 학생 
WHERE 키 &gt; ANY (SELECT 키 FROM 학생 WHERE 학년 = 3);</code></pre>
<p>예) </p>
<pre><code class="language-sql">#동작하지 않는 경우
SELECT *
FROM customer
WHERE custid = (SELECT custid
                FROM orders);

 #수정
 SELECT *
FROM customer
WHERE custid IN (SELECT custid
                FROM orders);</code></pre>
<blockquote>
<p><strong>부속질의 vs 상관질의</strong></p>
</blockquote>
<ul>
<li>부속질의: 단순히 쿼리 안에 들어간 SELECT문으로 독립적으로 실행할 수 있음</li>
<li>상관질의: 부속질의 중 <strong>메인 쿼리의 컬럼을 참조</strong>하여 메인 쿼리의 <strong>각 행마다 실행</strong>되는 경우.</li>
</ul>
<h4 id="exists-not-exists-존재-연산자">EXISTS, NOT EXISTS (존재 연산자)</h4>
<ul>
<li>데이터의 존재 여부를 확인<pre><code class="language-sql">WHERE [NOT] EXISTS (부속질의)</code></pre>
</li>
<li>메인 쿼리와 서브 쿼리가 상관 부속질의의 관계일 때 둘 사이의 연결 관계를 잘 설정해줘야한다</li>
</ul>
<pre><code class="language-sql">SELECT *
FROM customer C
WHERE EXISTS (SELECT custid
                     FROM orders O
                     WHERE C.custid = O.custid);</code></pre>
<pre><code>위에서는 `C.custid = O.custid`로 둘을 연결해주었다.</code></pre><p><img src="https://velog.velcdn.com/images/summeryoung_/post/1cdcba20-4191-4f31-b263-fa967e36e0ba/image.png" alt=""></p>
<hr>
<h2 id="2-2-스칼라-부속질의-select-부속질의">2-2. 스칼라 부속질의 (SELECT 부속질의)</h2>
<ul>
<li>부속질의의 결과 값을 <strong>단일 행, 단일 열의 스칼라 값</strong>으로 반환</li>
<li>만약 결과 값이 다중 행이거나 다중 열이면 DBMS는 어떤 행/열을 출력해야하는지 몰라 에러를 출력</li>
<li>결과값이 없는 경우에는 NULL 출력</li>
<li>SELECT 절에서 사용하니 고객별 주문 총횟수/총액과 같은 부분을 출력할 때 사용, 이때 하나의 행에 출력되는 값이니 여러 개일 수 없는 느낌...?</li>
</ul>
<p>예)
<strong>1. GROUP BY 없이</strong></p>
<pre><code class="language-sql">SELECT custid, (SELECT COUNT(*)
                FROM orders O
                WHERE C.custid = O.custid) AS &quot;주문 횟수&quot;
FROM customer C;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/49b42732-f3c2-4d63-8439-2b9797b6b10a/image.png" alt=""></p>
<ul>
<li>WHERE 절에서 고객의 주문을 찾고, <strong>주문이 없는 경우 결과가 0</strong>이 되는데 이때 <code>GROUP BY</code>가 없이 <code>COUNT()</code>를 쓰기에 대상이 없더라도 무조건 0이 됨</li>
<li>결과: 주문 안 한 고객의 옆에는 0<br>

</li>
</ul>
<p><strong>2. GROUP BY 있는 상태</strong></p>
<pre><code class="language-sql">SELECT custid, (SELECT COUNT(*)
                FROM orders O
                WHERE C.custid = O.custid
                GROUP BY custid) AS &quot;주문 횟수&quot;
FROM customer C;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/3cb7c97c-cee1-4245-8f22-2210bacbf67a/image.png" alt=""></p>
<ul>
<li>WHERE절 이후 그룹별로 묶으려고하는데, 이때 주문이 없으면 WHERE절에서 남은 데이터가 하나도 없기에 그룹 자체가 만들어지지 않음</li>
<li>그룹이 없으면 <code>COUNT()</code>의 대상도 없기에 아무 행도 반환하지 않음.</li>
</ul>
<br>

<h4 id="update-문에서의-스칼라-부속질의select-부속질의">UPDATE 문에서의 스칼라 부속질의(SELECT 부속질의)</h4>
<ul>
<li>스칼라 부속질의(SELECT 부속질의)는 UPDATE문에서도 사용할 수 있음</li>
</ul>
<pre><code class="language-sql">SET SQL_SAFE_UPDATES = 0;

UPDATE orders
SET bookname = (SELECT bookname
                FROM book
                WHERE book.bookid = orders.bookid);

SELECT *
FROM orders;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/9dbd32bf-8a63-48a6-b3e5-72b1fec5d1c9/image.png" alt=""></p>
<hr>
<h2 id="2-3-인라인-뷰-from-부속질의">2-3. 인라인 뷰 (FROM 부속질의)</h2>
<ul>
<li>FROM 절에서 사용되는 부속질의</li>
<li><strong>뷰: 기존 테이블로부터 일시적으로 만들어지는 가상의 테이블</strong></li>
</ul>
<p>예)</p>
<pre><code class="language-sql">SELECT C.name, SUM(o.saleprice) &#39;total&#39;
FROM (SELECT custid, name
      FROM customer
      WHERE custid &lt;= 2) C,
      orders O
WHERE C.custid = O.custid
GROUP BY C.name;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/42d5825b-50f9-4236-a8d6-3b7e50e5de12/image.png" alt=""></p>
<hr>
<h2 id="2-4-cte-common-table-expression-with">2-4. CTE (Common Table Expression, WITH)</h2>
<h3 id="with-cte명-as-select">WITH CTE명 AS (SELECT)</h3>
<ul>
<li>복잡한 SQL 쿼리 내에서 <strong>일시적인 결과 집합 (임시테이블)</strong>을 정의하여, 가독성을 높이고 쿼리를 구조화</li>
<li>메인 쿼리에서 일반 테이블처럼 <strong>재사용</strong>하거나, <strong>재귀 쿼리</strong> 구현에 활용됨</li>
<li>쿼리 실행에만 존재하며 데이터베이스에 영구적으로 저장되지 않음.</li>
</ul>
<pre><code class="language-sql">WITH CTE이름 AS (
    SELECT...
)
#,나 ; 적어주지 않고

#메인 쿼리는 이 아래
SELECT ...
FROM CTE이름;</code></pre>
<p>예)</p>
<pre><code class="language-sql">WITH sales_summary AS (
    SELECT o.custid, c.name, SUM(o.saleprice) AS total_sales
    FROM orders o
         JOIN customer c
         ON o.custid = c.custid
    GROUP BY o.custid
)

SELECT custid, name, total_sales
FROM sales_summary
ORDER BY total_sales DESC;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/139a1efa-adf3-4c37-a417-b5239ee54209/image.png" alt=""></p>
<hr>
<h2 id="2-5-윈도우-함수">2-5. 윈도우 함수</h2>
<h3 id="sql-윈도우-함수">SQL 윈도우 함수</h3>
<p>: 테이블의 <strong>행과 행 간의 관계를 정의하여 데이터를 윈도우(틀)로 그룹화하여 사용하는 함수</strong></p>
<ul>
<li>각 행에 대한 <strong>집계나 순위 계산 결과를 추가</strong>하여 사용</li>
<li><strong>GROUP BY와 달리 행의 개수를 유지하면서 그룹 내 계산 결과를 각 행에 표시</strong></li>
<li>복잡한 조인(JOIN) 없이 행 간 계산, 순위, 누적합계 등을 구할 때 유용</li>
<li><strong><code>OVER()</code></strong> 절과 함께 사용되어 PARTITION BY와 ORDER BY로 범위와 순서 지정</li>
</ul>
<p><span style="font-size:15px"> GROUP BY랑 비슷한데 행이 없어지지 않는다... <br></p>
<p><code>GROUP BY</code>를 하는 경우에는 여러 개의 행이 그룹별로 묶여 사라지지만, 윈도우 함수를 쓰는 경우에는 행 옆에 계산 결과를 붙여주는 식.</span></p>
<blockquote>
<p>SELECT <strong>함수명() OVER (PARTITION BY 컬럼명 ORDER BY 컬럼명)</strong>
FROM 테이블명;
<br></p>
</blockquote>
<ul>
<li><strong>PARTITION BY</strong>: 계산을 수행할 그룹을 나눔</li>
<li><strong>ORDER BY</strong>: 그 그룹 안에서 계산을 수행할 순서</li>
</ul>
<h3 id="주요-윈도우-함수-유형">주요 윈도우 함수 유형</h3>
<h4 id="1-순위함수">1. 순위함수</h4>
<ul>
<li><code>ROW_NUMBER()</code>: 줄 세우기 (1,2,3,4...)</li>
<li><code>RANK()</code>: 공동 순위만큼 건너뛰기 (1,2,2,3...)</li>
<li><code>DENSE_RANK()</code>: 공동순위가 있어도 촘촘하게 (1,2,2,3,...)</li>
</ul>
<pre><code class="language-sql">SELECT B.publisher, B.bookname, 
       SUM(O.saleprice) AS total_sales, 
       DENSE_RANK() OVER (PARTITION BY B.publisher ORDER BY SUM(O.saleprice) DESC) AS rank_in_publisher
FROM orders O 
     JOIN book B 
     ON O.bookid = B.bookid
GROUP BY B.publisher, B.bookname
ORDER BY B.publisher, rank_in_publisher;</code></pre>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/048e67e9-0346-4907-967c-c912595e0809/image.png" alt=""></p>
<h4 id="2-집계함수">2. 집계함수</h4>
<ul>
<li><p>누적 합계를 구할 때 편함.</p>
</li>
<li><p>순위함수(Ranking): ROW_NUMBER(), RANK(), DENSE_RANK()</p>
</li>
<li><p>집계함수(Aggregate): SUM(), AVG(), COUNT(), MAX(), MIN()</p>
</li>
<li><p>분석/값 함수(Value): LEAD(), LAG(), FIRST_VALUE(), LAST_VALUE()...</p>
</li>
</ul>
<p>예) 출판사별로 saleprice 누적합 구하기</p>
<pre><code class="language-sql">SELECT B.publisher, B.bookname,
       SUM(SUM(O.saleprice)) OVER (PARTITION BY B.publisher 
                                   ORDER BY SUM(O.saleprice), B.bookname) AS &#39;출판사별 총 판매액&#39;
FROM orders O 
     JOIN book B 
     ON O.bookid = B.bookid
GROUP BY B.publisher, B.bookname
ORDER BY B.publisher, `출판사별 총 판매액`;</code></pre>
<p>orders와 book 조인 -&gt; 출판사, 책 이름 별로 그루핑 <code>출판사-책이름-그 책의 총판매액: SUM(O.saleprice)</code> -&gt; 윈도우 함수 안에서 <code>PARTITION BY</code>를 통해 출판사별로 분리, 출판사별 <code>SUM(O.saleprice)</code> 계산. -&gt; <code>ORDER BY</code> 통해 책별 총판매액 &amp; 책이름으로 정렬 (같은 가격이어도 분리되어서 나오게) -&gt; 외부 <code>ORDER BY</code>에 윈도우 함수 결과 속성 추가해서 정렬결과대로 보기.</p>
<p><code>SUM(SUM(O.saleprice)</code></p>
<ul>
<li>안쪽 SUM: GROUP BY 결과로 나온 <strong>책 한 권의 합계</strong></li>
<li>바깥 SUM: 윈도우 함수가 만드는 <strong>출판사 내의 누적 합계</strong>
<img src="https://velog.velcdn.com/images/summeryoung_/post/1352b061-0ecc-4187-99a6-c3004895c9ae/image.png" alt=""></li>
</ul>
<h4 id="3-분석값-함수">3. 분석/값 함수</h4>
<ul>
<li>이전 행이나 다음 행의 데이터를 가져올 때</li>
<li>JOIN 없이도 어제와 오늘의 차이를 계산할 수 있음</li>
<li><code>LAG(컬럼)</code>: 현재 행보다 이전(뒤에있는) 행의 값을 가져옴 (과거)</li>
<li><code>LEAD(컬럼)</code>: 현재 행보다 다음(앞에있는) 행의 값을 가져옴 (미래)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/summeryoung_/post/e6583a8d-f432-49e0-a34d-13e76bee05a8/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>