<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>now_here.log</title>
        <link>https://velog.io/</link>
        <description>코드 위에서 춤추고 싶어요</description>
        <lastBuildDate>Wed, 23 Apr 2025 00:43:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>now_here.log</title>
            <url>https://velog.velcdn.com/images/now_here/profile/5a77ba87-d678-45e6-bf6c-2673270f00ef/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. now_here.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/now_here" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[URI와 웹 브라우저 요청 흐름]]></title>
            <link>https://velog.io/@now_here/URI%EC%99%80-%EC%9B%B9-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%9A%94%EC%B2%AD-%ED%9D%90%EB%A6%84</link>
            <guid>https://velog.io/@now_here/URI%EC%99%80-%EC%9B%B9-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%9A%94%EC%B2%AD-%ED%9D%90%EB%A6%84</guid>
            <pubDate>Wed, 23 Apr 2025 00:43:00 GMT</pubDate>
            <description><![CDATA[<h1 id="uri-uniform-resource-identifier">URI (Uniform Resource Identifier)</h1>
<ul>
<li>리소스를 식별하는 통합된 방법</li>
</ul>
<h2 id="uri-url-urn">URI, URL, URN</h2>
<p>URI는 로케이터(locator), 이름(name) 또는 둘 다 추가로 분류될 수 있다.
URI 안에 URL, URN이 있다. </p>
<h2 id="uri-뜻">URI 뜻</h2>
<ul>
<li>Uniform : 리소스 식별하는 통일된 방식</li>
<li>Resource : 자원, URI로 식별할 수 있는 모든 것 (제한 없음)</li>
<li>Idendifier : 다른 항목과 구분하는데 필요한 정보</li>
</ul>
<p>URL : Uniform Resource Locator
-&gt; 리소스가 있는 위치를 지정.
위치는 변할 수 있지만, 이름은 변하지 않는다.</p>
<p>URN : Name - 리소스에 이름을 부여, 이름만으로 실제 리소스를 찾을 수 있는 방법이 보편화 되지 않음 </p>
<h2 id="url-문법">URL 문법</h2>
<ul>
<li>scheme://[userinfo@]host[:port][/path][?query][#fragment]</li>
<li><a href="https://www.google.com:443/search?q=hello&amp;hi=ko">https://www.google.com:443/search?q=hello&amp;hi=ko</a></li>
</ul>
<p><strong>프로토콜</strong> (https)
<strong>호스트명</strong> (<a href="http://www.google.com">www.google.com</a>)
<strong>포트 번호</strong>(443)
<strong>패스</strong> (/search)
<strong>쿼리 파라미터</strong> (q=hello&amp;hi=ko)</p>
<h3 id="스키마"><strong>스키마</strong></h3>
<p>주로 프로토콜을 사용하며, 프로토콜은 어떤 방식으로 자원에 접근할 것인가 하는 약속 규칙이다. (ex. http, https, ftp 등등)
http는 80포트, https는 443 포트를 주로 사용하며 포트는 생략 가능 하다. https는 http에 보안을 추가한 프로토콜이다. (HTTP Secure)</p>
<h3 id="userinfo">userinfo</h3>
<p>URL에 사용자종보를 포함해서 인증하지만 거의 사용하지 않음.</p>
<h3 id="host">host</h3>
<p>도메인명, 또는 ip 주소를 직접 사용할 수 있다.</p>
<h3 id="port">port</h3>
<p>일반적으로 생략가능하며 http는 80, https는 443으로 들어간다.
접속 포트를 의미한다.</p>
<h3 id="path">path</h3>
<p>리소스 경로(path), 계층적 구조로 예시는 다음과 같다.</p>
<ul>
<li>/home/file1.jpg</li>
<li>/members</li>
<li>/members/100</li>
</ul>
<h3 id="쿼리-파라미터">쿼리 파라미터</h3>
<ul>
<li>key=value 형태</li>
<li>?로 시작, &amp;로 추가 가능 ? keyA=valueA&amp;keyB=valueB</li>
<li>query parameter, query string 등으로 불리며 웹서버에 제공하는 파라미터, 문자 형태</li>
</ul>
<h3 id="fragment">fragment</h3>
<p> html 내부 북마크 등에 사용되며 서버에 전송되는 정보는 아니다.</p>
<p>[참고] &quot;모든 개발자를 위한 HTTP 웹 기본 지식&quot; 강의</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTP 학습 - 인터넷 네트워크 (IP, TCP/UDP, PORT, DNS)]]></title>
            <link>https://velog.io/@now_here/HTTP-%ED%95%99%EC%8A%B5-%EC%9D%B8%ED%84%B0%EB%84%B7-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-IP-TCPUDP-PORT-DNS</link>
            <guid>https://velog.io/@now_here/HTTP-%ED%95%99%EC%8A%B5-%EC%9D%B8%ED%84%B0%EB%84%B7-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-IP-TCPUDP-PORT-DNS</guid>
            <pubDate>Wed, 16 Apr 2025 01:21:52 GMT</pubDate>
            <description><![CDATA[<h1 id="ipinternet-protocol">IP(Internet Protocol)</h1>
<ul>
<li>지정한 IP 주소 (IP Adress)에 데이터를 전달한다.</li>
<li>패킷(Packet)이라는 통신 단위로 데이터 전달한다.<h2 id="ip-패킷-정보">IP 패킷 정보</h2>
출발지 IP, 목적지 IP, 기타 정보를 <strong>IP 패킷</strong>에 담아서 전송한다.</li>
</ul>
<h2 id="클라이언트-패킷-전달">클라이언트 패킷 전달</h2>
<p>출발지 IP 주소와 목적지 IP 주소, 기타 정보를 담은 패킷이 인터넷 노드들을 통해서 목적지 IP로 간다.</p>
<h2 id="서버-패킷-전달">서버 패킷 전달</h2>
<p>서버 패킷도 마찬 가지인데 이 때 전달받은 인터넷 노드들과는 다른 노드들을 통해서 전달될 수 있다.</p>
<h2 id="ip-프로토콜의-한계">IP 프로토콜의 한계</h2>
<ul>
<li><strong>비연결성</strong> : 패킷을 받을 대상이 없어도 패킷 전송
컴퓨터가 꺼져 있거나, 주소가 이상해도 패킷은 일단 정송된다.</li>
<li><strong>비신뢰성</strong> : 중간에 패킷이 사라지거나 순서대로 안 와도 확인할 방법이 없다.</li>
<li><strong>프로그램 구분</strong> : 같은 IP를 사용하는 서버에서 통신하는 애플리케이션이 둘 이상이라면 구분하는 문제가 있다.</li>
</ul>
<h1 id="tcpudp">TCP/UDP</h1>
<h2 id="인터넷-프로토콜-스택의-4계층">인터넷 프로토콜 스택의 4계층</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/edd6b62a-58a8-444d-8ce1-8236ed1346f1/image.png" alt=""></p>
<h2 id="프로토콜-계층">프로토콜 계층</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/b1e5a000-4a5a-4fa0-9bf7-0a1f8f5938af/image.png" alt=""></p>
<ol>
<li>메시지 생성</li>
<li>SOCKET 라이브러리를 통해 OS에 전달</li>
<li>OS에서 TCP 정보 생성, 메시지 데이터 전달</li>
<li>IP 패킷을 생성 (TCP 데이터 포함)</li>
</ol>
<h2 id="tcpip-패킷-정보">TCP/IP 패킷 정보</h2>
<p>TCP는 출발지 PORT, 목적지 PORT, 전송제어, 순서, 검증 정보 등을 담고 있다. 그리고 그 안에 전송 데이터를 담고 있다.</p>
<h2 id="tcp-특징">TCP 특징</h2>
<p>전송 제어 프로토콜 (Transmission control Protocol)</p>
<ul>
<li>연결지향 : TCP 3 handshake(가상 연결)</li>
<li>데이터 전달 보증 : 누락되거나 손실되면 알 수 있다.</li>
<li>순서 보장 </li>
</ul>
<h2 id="tcp-3-way-handshake">TCP 3 way handshake</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/e47a51f0-f2c6-4555-b4e5-a4813c24f27e/image.png" alt="">
SYN : 접속 요청
ACK : 요청 수락</p>
<ol>
<li>클라이언트가 서버에 접속 요청(SYN)을 보낸다.</li>
<li>서버는 클라이언트의 접속 요청을 수락(ACK) 하면서 서버도 클라이언트에게 접속 요청(SYN)을 보낸다.</li>
<li>클라이언트는 서버의 접속 요청을 수락(ACK)한다. 
(ACK와 함께 데이터 전송 가능)</li>
</ol>
<p>-&gt; 3 way handshake</p>
<h2 id="데이터-전달-보증">데이터 전달 보증</h2>
<p>클라이언트에서 데이터를 전송하면 서버에서 데이터를 잘 받았다고 응답을 보낸다. 응답이 없다면 제대로 전송이 안 됐다는걸 인지할 수 있다.</p>
<h2 id="순서-보장">순서 보장</h2>
<p>클라이언트가 패킷1,2,3을 보냈는데 순서가 이상하게 오면 서버가 클라이언트에게 순서가 이상한 다음부터 다시 보내라고 요청을 한다.</p>
<h1 id="udp">UDP</h1>
<p>사용자 데이터그램 프로토콜(User Datagram Protocol)
TCP와 같은 계층.</p>
<ul>
<li><p>데이터 전달 및 순서가 보장되지 않지만, 단순하고 빠름</p>
</li>
<li><p>IP와 거의 같지만 PORT(애플리케이션 구분 위해 사용)와 체크섬(데이터 정보 검증) 정도만 추가</p>
</li>
<li><p>애플리케이션에서 추가 작업 필요</p>
</li>
<li><p>웹브라우저에서 HTTP 통신할 때 UDP가 요즘 뜨고 있다. (빨리 전송하기 위해)</p>
</li>
</ul>
<hr>
<h1 id="port">PORT</h1>
<h2 id="한-번에-둘-이상-연결해야-하면">한 번에 둘 이상 연결해야 하면?</h2>
<p>-&gt; 서버 안에서 돌아가는 애플리케이션들을 구분하는 것!
(출발지 PORT, 목적지 PORT)</p>
<ul>
<li>0 ~ 65536 할당가능</li>
<li>0 ~ 1023 : 잘 알려진 포트, 사용하지 않는 것이 좋음</li>
<li>FTP - 20,21</li>
<li>TELNET -23</li>
<li>HTTP - 80</li>
<li>HTTPS - 443</li>
</ul>
<h1 id="dns">DNS</h1>
<p>-&gt; IP는 기억하기 어렵고, 변경될 수도 있다.
<strong>DNS도메인 네임 시스템 (Domain Name System)</strong> : 도메인 명을 IP 주소로 변환</p>
<ul>
<li>DNS 서버에 도메인명과 ip를 저장.</li>
<li>사용자는 도메인명을 통해 도메인 서버에 접근, 도메인 서버가 해당 ip로 응답 후 그 ip로 접속하게 된다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[개발 환경, 운영 환경 브랜치 전략]]></title>
            <link>https://velog.io/@now_here/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%9A%B4%EC%98%81-%ED%99%98%EA%B2%BD-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EC%A0%84%EB%9E%B5</link>
            <guid>https://velog.io/@now_here/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EC%9A%B4%EC%98%81-%ED%99%98%EA%B2%BD-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EC%A0%84%EB%9E%B5</guid>
            <pubDate>Sat, 08 Mar 2025 06:22:28 GMT</pubDate>
            <description><![CDATA[<p>개발 환경과 운영 환경에서 어떻게 브랜치를 관리해야 할까?
브랜치는 개발자들이 병렬로 개발하고 실제 운영되는 서비스와 분리하여 개발/테스트 하기 위해 사용된다.</p>
<h1 id="기본-개념">기본 개념</h1>
<p><strong>컴파일</strong> : 개발자가 만든 코드를 컴퓨터가 이해할 수 있는 언어로 바꾸는 작업 </p>
<p><strong>빌드</strong> : 컴파일된 코드를 실제 동작도록 만드는 과정. 소스코드를 변환하는 것 뿐만 아니라 리소스 포함, 의존성 관리, 패키징도 포함한다. (컴파일을 포함해 jar,war 파일을 만드는 작업을 빌드라고 하기도 함)</p>
<p><strong>배포</strong> : 빌드된 애플리케이션을 특정 환경(개발, 스테이징, 운영)에 배치하여 실행할 수 있도록 하는 과정.</p>
<p><strong>스테이징 환경(Staging)</strong> : 운영환경과 거의 유사한 환경. 운영 환경으로 넘어가기 전  <strong>최종 점검하는 테스트환경</strong>이다.</p>
<p><strong>운영 환경(Production)</strong> : <strong>실제 사용자들이 서비스를 이용하는 환경</strong>으로 운영 환경으로 넘어가기 전 스테이징 환경에서 최종적으로 모든 테스트를 마쳐야 한다. <strong>장애 발생시 롤백 가능하도록</strong> 설정하는 것이 중요하다.</p>
<h1 id="개발-환경-브랜치-전략">개발 환경 브랜치 전략</h1>
<h2 id="1-git-flow-전략">1. Git Flow 전략</h2>
<p>배포 주기가 긴 대규모 프로젝트에 적합한 브랜치 전략.
주요 브랜치로 main , develop 브랜치를 가지고 보조 브랜치로 feature, release, hoxfix 브랜치가 있다.</p>
<p><img src="https://velog.velcdn.com/images/now_here/post/08fd9c74-ad8a-4320-841e-9785ef8ff5c0/image.png" alt=""></p>
<h3 id="main">main</h3>
<ul>
<li>항상 안정적인 상태의 코드만 포함. 프로덕션에 최종적으로 배포되는 코드가 저장되는 브랜치</li>
</ul>
<h3 id="develop">develop</h3>
<ul>
<li>기능 개발의 기본 브랜치로 모든 새로운 기능 브랜치들이 병합되고 테스트 되는 브랜치. 기능 개발이 완료되면 main 브랜치로 병합하여 배포할 준비를 한다.</li>
</ul>
<h3 id="feature">feature</h3>
<ul>
<li>신기능을 개발하는 브랜치. develop 브랜치에서 분기하며 개발 완료시 develop 브랜치로 병합한다.</li>
</ul>
<h3 id="release">release</h3>
<ul>
<li>배포 전에 테스트하고 버그를 수정하는 브랜치. develop에서 분기되며, 배포 준비가 완료되면 main과 develop에 병합된다.</li>
</ul>
<h3 id="hotfix">hotfix</h3>
<ul>
<li>프로덕션에서 발생한 긴급한 버그를 수정하기 위한 브랜치. main 브랜치에서 분기되며, 수정 후 main과 develop 브랜치에 병합된다.</li>
</ul>
<h4 id="git-flow-예제-cli">Git Flow 예제 CLI</h4>
<p><img src="https://velog.velcdn.com/images/now_here/post/c73560a5-6003-4611-b491-f6743c6679ea/image.png" alt=""></p>
<pre><code class="language-bash">git branch develop
git checkout develop
git branch feature/a
git checkout feature/a
git commit - m &#39;service a&#39;
git commit - m &#39;service a2&#39;
git checkout master
git branch hotfix
git checkout hotfix
git commit -m &#39;bug fix&#39;
git checkout master
git merge hotfix
git checkout develop
git merge hotfix

git merge feature/a -m &#39;merge feature/a into develop&#39;
git branch release
git checkout release
// 테스트 완료 커밋 추가
git commit -m &#39;relaese: finish test&#39;

git checkout develop
git merge release -m &#39;merge relase into develop&#39;

git checkout main
git merge release -m &#39;merge release into main&#39;</code></pre>
<p>위의 그림을 보면 알겠지만 브랜치가 너무 많아 복잡하다. 체계적인 브랜치 관리가 가능하고 여러 기능을 병렬적으로 개발할 수 있다는 장점이 있지만 브랜치가 많아 관리가 복잡하다는 단점이 있다. CI/CD 환경이 발전하면서 GitHub Flow나 Trunk-Based Development(TBD) 같은 단순한 전략이 더 선호된다. 간소화된 전략중 하나인 GitLab Flow를 알아보자.</p>
<h1 id="gitlab-flow">GitLab Flow</h1>
<p>GitLab Flow는 <strong>Git Flow보다 단순하면서도 CI/CD에 최적화된 브랜치 전략</strong>.  
Git Flow는 <code>develop</code>, <code>feature</code>, <code>release</code>, <code>hotfix</code> 등 다양한 브랜치를 사용하지만,<br>GitLab Flow는 <strong>보다 간단한 브랜치 구조</strong>로 유지보수와 배포를 쉽게 만들었다.</p>
<h2 id="-gitlab-flow-기본-개념">** GitLab Flow 기본 개념**</h2>
<p>GitLab Flow의 <strong>3가지 모델</strong></p>
<ol>
<li>Production Branch Model (기본적인 GitLab Flow)  </li>
<li><strong>Environment Branch Model</strong> (운영 환경별 브랜치 전략)  </li>
<li>Release Branch Model (버전 릴리스 전략)</li>
</ol>
<hr>
<h2 id="1-기본-gitlab-flow-production-branch-model"><strong>1. 기본 GitLab Flow (Production Branch Model)</strong></h2>
<p><strong>✔ 브랜치 구조:</strong></p>
<pre><code class="language-bash">main  -----&gt; (프로덕션 배포)
  |
  |-- feature/*  -----&gt; (기능 개발 후 main으로 Merge Request)</code></pre>
<p>** 동작 방식:**</p>
<ul>
<li><p><code>main</code> 브랜치는 <strong>항상 운영 환경(Production) 코드</strong>를 유지.</p>
</li>
<li><p>새 기능은 <code>feature</code> 브랜치에서 개발 후, <code>Merge Request(MR, PR)</code>를 생성.</p>
</li>
<li><p>코드 리뷰 후 <code>main</code>에 병합되면 자동으로 배포(CI/CD 활용).</p>
<p><strong>차이점:</strong>  </p>
</li>
<li><p>Git Flow처럼 <code>develop</code>, <code>release</code> 브랜치 없이 <strong>직접 <code>main</code>으로 병합!</strong></p>
</li>
<li><p><strong>빠른 배포(Continuous Deployment, CD)를 목표</strong>로 함.</p>
</li>
<li><p><strong><code>feature</code> 브랜치에서 충분히 테스트한 후 <code>main</code>으로 병합</strong>해야 함.</p>
</li>
</ul>
<p>✔ <strong>사용 예시:</strong><br>스타트업, 빠르게 배포하는 SaaS 서비스, 지속적 배포가 필요한 프로젝트.</p>
<h2 id="2-환경-기반-브랜치-전략-environment-branch-model"><strong>2. 환경 기반 브랜치 전략 (Environment Branch Model)</strong></h2>
<p><strong>✔ 브랜치 구조:</strong></p>
<pre><code class="language-bash">main       -----&gt; (프로덕션 배포)
  |
  |-- staging  -----&gt; (사전 테스트 환경)
  |     |
  |     |-- feature/*  -----&gt; (기능 개발 후 staging으로 MR)</code></pre>
<p>** 동작 방식:**</p>
<ul>
<li><code>feature</code> 브랜치에서 개발 후 <code>staging</code> 브랜치로 병합 (테스트 환경)</li>
<li><code>staging</code>에서 QA 테스트 후, 이상 없으면 <code>main</code>으로 병합하여 프로덕션 배포</li>
</ul>
<p><strong>차이점:</strong>  </p>
<ul>
<li>프로덕션(<code>main</code>)에 직접 병합하기 전에 <code>staging</code>을 거쳐 안정성을 확보.</li>
<li><strong>테스트를 철저히 해야 하는 서비스</strong>(ex. 금융, 의료, 대기업 프로젝트)에 적합.</li>
</ul>
<p>✔ <strong>사용 예시:</strong><br>금융 서비스, 의료 시스템, 보안이 중요한 프로젝트.</p>
<h2 id="3-릴리스-브랜치-전략-release-branch-model"><strong>3. 릴리스 브랜치 전략 (Release Branch Model)</strong></h2>
<p><strong>✔ 브랜치 구조:</strong></p>
<pre><code class="language-bash">main         -----&gt; (최신 안정 릴리스)
  |
  |-- release/2.0  -----&gt; (v2.0 릴리스 준비)
  |      |
  |      |-- hotfix/2.0.1  -----&gt; (긴급 수정 후 release 브랜치에 반영)</code></pre>
<p>** 동작 방식:**</p>
<ul>
<li>특정 버전을 릴리스할 때, <code>release/*</code> 브랜치를 생성해 안정화.</li>
<li>버그 수정이 필요하면 <code>hotfix/*</code> 브랜치를 만들어 수정 후 <code>release/*</code>에 병합.</li>
<li>안정화된 버전이 되면 <code>main</code>에 병합 후 배포.</li>
</ul>
<p><strong>차이점:</strong>  </p>
<ul>
<li>Git Flow와 비슷하지만 <strong>release 브랜치만 사용</strong>해 복잡도를 줄임.</li>
<li>특정 버전 릴리스 후 유지보수가 필요할 때 유용.</li>
</ul>
<p>✔ <strong>사용 예시:</strong><br> <strong>소프트웨어 버전 관리가 중요한 프로젝트 (ex. 대형 애플리케이션, 패키지 소프트웨어, 게임 개발).</strong></p>
<p>** GitLab Flow vs Git Flow 차이점**</p>
<table>
<thead>
<tr>
<th></th>
<th><strong>GitLab Flow</strong></th>
<th><strong>Git Flow</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>브랜치 개수</strong></td>
<td>적음 (단순)</td>
<td>많음 (복잡)</td>
</tr>
<tr>
<td><strong>배포 방식</strong></td>
<td>지속적 배포 (CI/CD 중심)</td>
<td>릴리스 중심</td>
</tr>
<tr>
<td><strong>테스트 환경</strong></td>
<td><code>staging</code> 브랜치 사용 가능</td>
<td><code>develop</code>, <code>release</code> 브랜치 사용</td>
</tr>
<tr>
<td><strong>사용 사례</strong></td>
<td>빠른 배포가 필요한 서비스</td>
<td>전통적인 소프트웨어 개발</td>
</tr>
</tbody></table>
<p><strong>GitLab Flow는 배포를 빠르게 하고, 브랜치 관리를 단순하게 하는 데 초점을 맞춘다!</strong><br><strong>Git Flow는 여러 브랜치를 활용해 명확한 배포 프로세스를 유지하는 데 중점을 둔다!</strong></p>
<hr>
<h1 id="정리">정리</h1>
<p>개발 환경에서는 GitLab Flow의 (환경 기반 브랜치) 전략이 유효해 보이고, 운영 환경에서는 Git Flow나 GitLab Flow (릴리스 브랜치 전략)이 유효해보인다.</p>
<h2 id="개발환경에서는-gitlab-flow-환경-기반-브랜치-전략">개발환경에서는 GitLab Flow (환경 기반 브랜치 전략)</h2>
<p>GitLab Flow (환경 기반 브랜치 전략)은 개발, 테스트, 운영 등 환경별로 브랜치를 나누는 방식으로 개발환경에서 다음과 같은 이점이 있다.</p>
<p><strong>1) 환경별 배포 안정성 유지</strong></p>
<ul>
<li>개발 환경에서는 <strong>지속적인 기능 개발과 통합 테스트</strong>가 이루어지므로,<br><code>develop</code> 같은 중앙 브랜치 없이 환경별 브랜치를 두면 <strong>각 환경에서 안정성을 유지할 수 있음</strong>.</li>
<li>예시:<pre><code>main        (운영 환경 - 프로덕션 배포)
  |
  |--- staging   (테스트 환경 - QA)
  |--- develop   (개발 환경 - 내부 테스트)
  |--- feature/* (기능 개발 브랜치)</code></pre></li>
<li>이런 구조를 사용하면, <strong>각 환경에서 독립적으로 테스트하고 안정성을 확보한 후, 상위 환경으로 배포할 수 있음</strong>.</li>
</ul>
<p><strong>2) CI/CD 자동 배포와 궁합이 좋음</strong></p>
<ul>
<li>GitLab Flow에서는 환경별 브랜치를 CI/CD 파이프라인과 연결해서 <strong>자동 배포</strong>를 설정할 수 있음. 
(GitLab Flow의 환경 기반 브랜치 전략을 적용하면 <strong>각 브랜치가 특정 서버(환경)와 연결됨.</strong><br>즉, 코드가 특정 브랜치에 병합되면 <strong>자동으로 해당 환경으로 배포</strong>할 수 있음)</li>
<li>예시:</li>
<li><em>CI/CD 자동 배포 예제*</em><ul>
<li><code>feature/*</code> → <strong>코드 검토 후 <code>develop</code>에 병합</strong> → <strong>개발 서버 자동 배포</strong></li>
<li><code>develop</code> → <strong>테스트 후 <code>staging</code>에 병합</strong> → <strong>QA 환경 자동 배포</strong></li>
<li><code>staging</code> → <strong>운영 준비 완료 후 <code>main</code>에 병합</strong> → <strong>프로덕션 자동 배포</strong></li>
</ul>
</li>
</ul>
<p>따라서 <strong>각 브랜치가 특정 환경과 연결되어 있어, 배포 자동화에 적합함</strong>.</p>
<p><strong>3) 빠른 피드백 루프 가능</strong></p>
<ul>
<li>개발 환경에서는 <strong>기능 개발 후 바로 통합 테스트가 필요</strong>함.</li>
<li>GitLab Flow를 사용하면, <code>feature/*</code> 브랜치를 <code>develop</code>에 병합하면서 바로 테스트 가능.</li>
<li>기능을 개발하면서 <strong>테스트를 빠르게 진행하고, 환경별 안정성을 확보할 수 있음</strong>.</li>
</ul>
<p>** 결론:**  
-&gt; <strong>환경 기반 브랜치 전략을 사용하면 환경별 배포 안정성을 유지하면서도, CI/CD와 연계하여 자동 배포 및 빠른 피드백이 가능하기 때문에 개발 환경에서 유효함.</strong></p>
<hr>
<h2 id="운영-환경에서는-git-flow-또는-gitlab-flow-릴리스-브랜치-전략">운영 환경에서는 &quot;Git Flow&quot; 또는 &quot;GitLab Flow (릴리스 브랜치 전략)&quot;</h2>
<p>운영 환경에서는 <strong>안정성과 긴급 대응이 중요</strong>하기 때문에, Git Flow 또는 GitLab Flow의 <strong>릴리스 브랜치 전략</strong>이 적합하다.</p>
<p> <strong>1) 운영 환경에서는 &quot;안정적인 코드만 배포&quot;가 핵심</strong></p>
<ul>
<li>운영 시스템은 <strong>레거시 코드와 신규 코드가 공존하는 경우가 많고, 즉시 배포가 어렵기 때문에, 철저한 테스트 후 배포해야 함.</strong></li>
<li>운영 환경에서는 <code>release/*</code> 브랜치를 사용하면, <strong>운영 배포 전 충분한 QA 및 승인 프로세스를 거칠 수 있음</strong>.</li>
<li>예시:<pre><code>main        (운영 환경 - 최종 안정 코드)
  |
  |--- release/v1.2   (릴리스 브랜치 - 운영 전 최종 검증)
  |--- hotfix/*       (긴급 수정 브랜치 - 운영 이슈 대응)</code></pre></li>
<li>운영 환경에서는 <strong>기능 개발 속도보다는 &quot;안정성&quot;이 중요하기 때문에</strong>, <strong>Git Flow의 <code>release</code> 브랜치를 활용하면 안정적인 배포가 가능</strong>.</li>
</ul>
<p><strong>2) &quot;긴급 패치 대응 (Hotfix)&quot;를 고려해야 함</strong></p>
<ul>
<li>병원 시스템 같은 <strong>운영 환경에서는 긴급한 버그 수정(Hotfix)이 필요할 가능성이 높음</strong>.</li>
<li>Git Flow에서는 <code>hotfix/*</code> 브랜치를 사용하여, <strong>운영 브랜치(<code>main</code>)에서 바로 긴급 패치를 적용하고, 이후 <code>develop</code>에도 반영</strong>.</li>
<li>예시:<pre><code class="language-sh">git checkout -b hotfix/urgent-bugfix main
# 버그 수정 후 main에 반영
git checkout main
git merge hotfix/urgent-bugfix
git push origin main
# 이후 develop에도 반영
git checkout develop
git merge hotfix/urgent-bugfix
git push origin develop</code></pre>
</li>
</ul>
<p><strong>3) 릴리스 브랜치를 사용하면 &quot;운영 승인 및 테스트 프로세스&quot;를 강화할 수 있음</strong></p>
<ul>
<li>운영 환경에서는 <strong>단순히 코드가 통합되었다고 바로 배포하는 것이 아니라, 배포 전 검증 과정이 필요</strong>함.</li>
<li>GitLab Flow의 릴리스 브랜치를 사용하면, 운영 환경에 맞춰 <strong>최종 QA 테스트, 문서 작업, 승인 절차를 진행할 수 있음</strong>.</li>
</ul>
<p>** 결론:**  
 <strong>운영 환경에서는 안정적인 배포와 긴급 패치 대응이 중요하기 때문에, Git Flow의 <code>release/*</code> &amp; <code>hotfix/*</code> 브랜치를 활용하면 운영 환경에서 유효함.</strong><br> <strong>GitLab Flow의 릴리스 브랜치를 사용하면, 운영 환경에서 배포 안정성을 더욱 강화할 수 있음.</strong></p>
<hr>
<h2 id="-최종-정리">** 최종 정리**</h2>
<table>
<thead>
<tr>
<th>환경</th>
<th>유효한 브랜치 전략</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td><strong>개발 환경</strong></td>
<td><strong>GitLab Flow (환경 기반 브랜치 전략)</strong></td>
<td>환경별 브랜치를 두고, CI/CD 자동 배포 및 빠른 피드백 가능</td>
</tr>
<tr>
<td><strong>운영 환경</strong></td>
<td><strong>Git Flow 또는 GitLab Flow (릴리스 브랜치 전략)</strong></td>
<td>운영 배포 안정성 확보, <code>release/*</code> 브랜치로 검증 후 배포, <code>hotfix/*</code>로 긴급 대응 가능</td>
</tr>
</tbody></table>
<p>✔ <strong>개발 환경에서는 GitLab Flow의 환경 기반 브랜치 전략이 CI/CD와 잘 맞기 때문에 유용함</strong><br>✔ <strong>운영 환경에서는 안정성과 긴급 대응이 중요하기 때문에 Git Flow 또는 GitLab Flow (릴리스 브랜치 전략)이 적합함</strong>  </p>
<hr>
<h3 id="참고">[참고]</h3>
<ul>
<li><p><a href="https://nvie.com/posts/a-successful-git-branching-model/">https://nvie.com/posts/a-successful-git-branching-model/</a>  (Git Flow 블로그)</p>
</li>
<li><p><a href="https://explorer89.tistory.com/m/213#:~:text=Git%EC%97%90%EC%84%9C%20**%EB%B8%8C%EB%9E%9C%EC%B9%98">https://explorer89.tistory.com/m/213#:~:text=Git%EC%97%90%EC%84%9C%20**%EB%B8%8C%EB%9E%9C%EC%B9%98</a> (Git에서 브랜치(Branch)를 사용하는 이유와 장점은 무엇인가요?)</p>
</li>
<li><p><a href="https://parkstate.tistory.com/33">https://parkstate.tistory.com/33</a> (Git브랜치 전략(feat.Git Flow, Github Flow, Gitlab Flow)</p>
</li>
<li><p><a href="https://weaklion1.tistory.com/35">https://weaklion1.tistory.com/35</a> (각 브랜치 전략 비교 (git flow, github flow, gitlab flow, TBD)</p>
</li>
<li><p><a href="https://ujuc.github.io/2015/12/16/git-flow-github-flow-gitlab-flow/">https://ujuc.github.io/2015/12/16/git-flow-github-flow-gitlab-flow/</a>(Git flow, GitHub flow, GitLab flow)</p>
</li>
<li><p><a href="https://brownbears.tistory.com/605">https://brownbears.tistory.com/605</a> ([Git] 브랜치 전략 - GitLab Flow)</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[가상화 기술 (feat. 도커, 쿠버네티스)]]></title>
            <link>https://velog.io/@now_here/%EB%8F%84%EC%BB%A4-feat.%EA%B0%80%EC%83%81%ED%99%94-%EA%B8%B0%EC%88%A0</link>
            <guid>https://velog.io/@now_here/%EB%8F%84%EC%BB%A4-feat.%EA%B0%80%EC%83%81%ED%99%94-%EA%B8%B0%EC%88%A0</guid>
            <pubDate>Fri, 07 Mar 2025 00:50:26 GMT</pubDate>
            <description><![CDATA[<h1 id="용어-정의">용어 정의</h1>
<p><img src="https://velog.velcdn.com/images/now_here/post/dcb960cc-2e05-482b-ab78-40ea75d9d3f1/image.png" alt=""></p>
<h2 id="가상화-기술">가상화 기술</h2>
<p><strong>하드웨어 자원을 논리적으로 분리하여 하나의 물리적 시스템에서 여러 개의 가상 환경을 운영할 수 있도록 하는 기술.</strong></p>
<h3 id="가상화-기술이란"><strong>가상화 기술이란?</strong></h3>
<p>가상화(Virtualization) 기술은 <strong>하드웨어 자원을 논리적으로 분리하여 하나의 물리적 시스템에서 여러 개의 가상 환경을 운영할 수 있도록 하는 기술</strong>이야. 이를 통해 컴퓨팅 자원을 더 효율적으로 사용하고, 확장성과 유연성을 제공할 수 있어.</p>
<hr>
<h2 id="가상화-기술의-핵심-개념"><strong>가상화 기술의 핵심 개념</strong></h2>
<ol>
<li><p><strong>하이퍼바이저(Hypervisor)</strong></p>
<ul>
<li>가상화를 지원하는 핵심 소프트웨어로, 하나의 물리적 시스템에서 여러 개의 가상 머신을 실행하도록 함</li>
<li><strong>타입 1(베어메탈, Bare Metal):</strong> 하드웨어 위에서 직접 실행 (ex. VMware ESXi, Microsoft Hyper-V, KVM)</li>
<li><strong>타입 2(호스트 기반, Hosted):</strong> 운영체제 위에서 실행 (ex. VirtualBox, VMware Workstation)</li>
</ul>
</li>
<li><p><strong>가상 머신(VM, Virtual Machine)</strong></p>
<ul>
<li><strong>하드웨어를 가상화하여 하나의 물리적인 서버</strong>에서 여러 개의 운영체제(OS)를 실행할 수 있게 함</li>
<li>물리 서버(Host Machine) 위에 하이퍼바이저가 설치되고, 하이퍼 바이저가 물리 리소스(CPU, 메모리, 스토리지 등)를 가상화하여 가상 머신(VM)을 생성함</li>
<li>각 가상 머신은 독립적은 OS를 설치하고 실행할 수 있어 서로 다른 OS를 하나의 물리 서버에서 운영할 수 있음 
(그래서 각 가상 머신은 운영체제를 포함해야 하므로 부팅 속도가 느리고 리소스 사용량이 많음)</li>
<li>대표적인 가상화 소프트웨어: VMware, VirtualBox, KVM 등</li>
</ul>
</li>
<li><p><strong>컨테이너(Container)</strong></p>
<ul>
<li><strong>OS 수준에서 가상화</strong>를 수행하여 독립적인 실행 환경을 제공하는 기술</li>
<li>가상 머신보다 가볍고 빠르며, 개발 및 배포 자동화에 유리.</li>
<li>대표적인 기술: Docker, Kubernetes</li>
</ul>
</li>
<li><p><strong>OS 가상화 (Operating System Virtualization)</strong></p>
<ul>
<li>OS 내부에서 애플리케이션이 실행될 독립적인 환경(컨테이너)을 제공하는 기술.</li>
<li><strong>하이퍼바이저 없이</strong>, <strong>하나의 운영체제 커널을 공유</strong>하면서 여러 개의 컨테이너를 실행함.</li>
<li>동작 과정 : 호스트 OS가 실행되고 OS 내부에서 컨테이너 엔진(Docker)을 실행하여 가상 환경을 생성. 각각의 컨테이너는 독립된 애플리케이션과 라이브러리를 포함하지만 호스트 OS의 커널을 공유함. VM과 달리 운영체제를 포함하지 않으므로 가볍고 빠름.</li>
</ul>
</li>
</ol>
<h2 id="가상화-기술을-왜-사용할까"><strong>가상화 기술을 왜 사용할까?</strong></h2>
<p> <strong>자원 효율성</strong>  </p>
<ul>
<li>하나의 서버에서 여러 개의 VM이나 컨테이너를 실행하여 하드웨어 리소스를 최대한 활용할 수 있음.</li>
</ul>
<p><strong>운영 및 관리 편의성</strong>  </p>
<ul>
<li>개발, 테스트, 배포 환경을 독립적으로 구축할 수 있어, 환경 차이로 인한 문제를 줄일 수 있음.</li>
</ul>
<p><strong>확장성과 유연성</strong>  </p>
<ul>
<li>새로운 애플리케이션을 빠르게 배포하고, 필요에 따라 시스템을 쉽게 확장 또는 축소 가능.</li>
</ul>
<p><strong>보안 및 격리</strong>  </p>
<ul>
<li>가상 환경이 서로 격리되어 있어 한 환경에서 문제가 발생해도 다른 환경에 영향을 주지 않음.</li>
</ul>
<p><strong>배포 및 CI/CD와 연계</strong>  </p>
<ul>
<li>컨테이너 기술(Docker, Kubernetes 등)은 CI/CD(지속적 통합/지속적 배포)와 연계하여 자동화된 배포가 가능.</li>
</ul>
<h2 id="가상화-기술의-주요-사용-사례"><strong>가상화 기술의 주요 사용 사례</strong></h2>
<p><strong>서버 가상화(Server Virtualization)</strong>  </p>
<ul>
<li>하나의 서버에서 여러 개의 가상 머신을 운영하여 리소스 최적화.</li>
</ul>
<p><strong>데스크톱 가상화(VDI, Virtual Desktop Infrastructure)</strong>  </p>
<ul>
<li>중앙 서버에서 가상 데스크톱을 제공하여 보안과 관리 효율성 강화.</li>
</ul>
<p><strong>네트워크 가상화(NFV, Network Functions Virtualization)</strong>  </p>
<ul>
<li>네트워크 기능을 가상화하여 유연한 네트워크 운영 가능.</li>
</ul>
<p><strong>컨테이너 오케스트레이션(Container Orchestration)</strong>  </p>
<ul>
<li>Kubernetes를 사용하여 대규모 컨테이너 환경을 관리하고 자동화.</li>
</ul>
<h3 id="가상화-vs-컨테이너"><strong>가상화 vs 컨테이너</strong></h3>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>가상 머신(VM)</th>
<th>컨테이너(Container)</th>
</tr>
</thead>
<tbody><tr>
<td>실행 방식</td>
<td>하드웨어 가상화</td>
<td>OS 가상화</td>
</tr>
<tr>
<td>속도</td>
<td>느림</td>
<td>빠름</td>
</tr>
<tr>
<td>리소스 사용</td>
<td>무겁고 많은 리소스 필요</td>
<td>가볍고 적은 리소스 사용</td>
</tr>
<tr>
<td>독립성</td>
<td>완전한 OS 포함, 높은 격리 수준</td>
<td>OS 공유, 격리 수준이 낮음</td>
</tr>
<tr>
<td>대표 기술</td>
<td>VMware, KVM, VirtualBox</td>
<td>Docker, Kubernetes</td>
</tr>
</tbody></table>
<hr>
<h2 id="컨테이너-container">컨테이너, Container</h2>
<p>컨테이너는 <strong>애플리케이션과 그 실행에 필요한 라이브러리 및 종속성(Dependencies)을 하나의 패키지로 묶어 격리된 환경에서 실행하는 기술</strong>이다. 이 때, <strong>호스트 운영체제의 커널을 공유</strong>하면서도 다른 컨테이너와는 독립적으로 동작하기 때문에 VM(가상머신)보다 가볍고 속도가 빠르다.</p>
<h3 id="컨테이너-주요-개념">컨테이너 주요 개념</h3>
<p><strong>이미지(Image)</strong>: 
실행 가능한 컨테이너의 템플릿(설정 파일과 애플리케이션이 포함됨)
예: nginx:latest, mysql:8.0 (각 애플리케이션이 포함된 미리 정의된 실행 환경)</p>
<p><strong>컨테이너(Container)</strong>: 
이미지를 실행하면 생성되는 독립적인 실행 환경.
이미지는 &quot;설정도 포함된 패키지&quot;라면, 컨테이너는 &quot;그 패키지를 실행한 것&quot;</p>
<h3 id="컨테이너-동작-방식">컨테이너 동작 방식</h3>
<ol>
<li><strong>컨테이너 엔진(Docker 등)</strong>이 설치된 호스트 OS가 있음.</li>
<li>각 컨테이너는 애플리케이션 + 필수 라이브러리를 포함한 상태로 실행됨.</li>
<li>모든 컨테이너는 호스트 OS의 커널을 공유하며, 필요한 리소스만 할당받아 실행됨.</li>
<li>컨테이너끼리는 격리되어 있어 서로 영향을 주지 않음.</li>
</ol>
<h2 id="도커-docker">도커, Docker</h2>
<p>도커는 서버에서 실행되는 컨테이너를 관리하는 프로그램으로 가장 널리 사용되는 <strong>컨테이너 기술</strong>이다. 도커를 사욤함으로써 개발자가 애플리케이션을 패키징하여 어디서든 동일한 환경에서 실행 가능하도록 해준다.</p>
<h2 id="쿠버네티스-kubernates">쿠버네티스, Kubernates</h2>
<p>여러 개의 서버에 도커를 이용해서 컨테이너를 관리한다고 하자. 각각의 서버에서 컨테이너가 필요하면 추가하고 필요 없어지면 제거하는 등의 작업을 모두 관리하는데 어려움이 생긴다.
그래서 필요한게 이런 도커를 관리하는 매니저 역할이다. 이런 매니저 역할을 하는 것이 쿠버네티스이다.</p>
<p>즉, 쿠버네티스는 도커가 실행중인 여러 대의 서버를 관리하는 <strong>컨테이너 오케스트레이션 도구</strong> (각각의 서버의 도커에게 대신 지시해줌)</p>
<h3 id="쿠버네티스-특징">쿠버네티스 특징</h3>
<ol>
<li><p>컨테이너 오케스트레이션 도구</p>
<ul>
<li>여러 대의 서버에서 실행되는 Docker 컨테이너를 자동으로 관리하는 역할을 함</li>
<li>컨테이너 배포, 스케일링, 로드 밸런싱, 장애 복구 등을 자동으로 수행</li>
</ul>
</li>
<li><p>마스터-노드 구조</p>
<ul>
<li>마스터(Kubernetes Control Plane): 전체 시스템을 제어하고 노드들에게 지시하는 역할</li>
<li>노드(Node, Worker Node): 실제 컨테이너(Docker 등)를 실행하는 서버들</li>
</ul>
</li>
<li><p>자동화된 컨테이너 관리</p>
<ul>
<li>컨테이너가 필요하면 자동으로 추가하고, 필요 없어지면 제거하는 등의 작업을 수행</li>
<li>특정 컨테이너가 장애가 나면 자동으로 다시 실행</li>
</ul>
</li>
</ol>
<h3 id="-쿠버네티스가-필요한-이유">** 쿠버네티스가 필요한 이유**</h3>
<ol>
<li><p><strong>수동 관리의 어려움 해결</strong>  </p>
<ul>
<li>여러 대의 서버에서 컨테이너를 직접 관리하는 것은 비효율적이고 복잡함.  </li>
<li>쿠버네티스는 서버(노드)들을 하나의 클러스터로 묶고 자동으로 관리해 줌.</li>
</ul>
</li>
<li><p><strong>자동 배포 및 롤백 지원</strong>  </p>
<ul>
<li>새 버전을 배포할 때, 서비스 중단 없이 점진적으로 배포 가능.  </li>
<li>문제가 발생하면 즉시 이전 버전으로 롤백 가능.</li>
</ul>
</li>
<li><p><strong>수평 확장(Scaling) 지원</strong>  </p>
<ul>
<li>트래픽이 증가하면 컨테이너 개수를 자동으로 늘려 부하를 분산시킴.  </li>
<li>반대로 트래픽이 줄면 불필요한 컨테이너를 자동으로 삭제하여 리소스를 절약.</li>
</ul>
</li>
<li><p><strong>셀프 힐링(Self-Healing) 기능</strong>  </p>
<ul>
<li>컨테이너가 다운되거나 오류가 발생하면 자동으로 새로운 컨테이너를 생성하여 복구.</li>
</ul>
</li>
<li><p><strong>로드 밸런싱 기능</strong>  </p>
<ul>
<li>여러 개의 컨테이너에 트래픽을 자동으로 분배하여 특정 컨테이너에 과부하가 걸리지 않도록 조정.</li>
</ul>
</li>
</ol>
<h3 id="-쿠버네티스의-주요-개념">** 쿠버네티스의 주요 개념**</h3>
<table>
<thead>
<tr>
<th>개념</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Pod(파드)</strong></td>
<td>컨테이너가 실행되는 최소 단위 (1개 이상의 컨테이너 포함 가능)</td>
</tr>
<tr>
<td><strong>Node(노드)</strong></td>
<td>컨테이너를 실행하는 서버 (워커 노드)</td>
</tr>
<tr>
<td><strong>Cluster(클러스터)</strong></td>
<td>여러 개의 노드가 모여 있는 집합</td>
</tr>
<tr>
<td><strong>Deployment(디플로이먼트)</strong></td>
<td>컨테이너를 배포하고 업데이트하는 방식 관리</td>
</tr>
<tr>
<td><strong>Service(서비스)</strong></td>
<td>네트워크를 통해 컨테이너에 접근하는 방법 제공</td>
</tr>
<tr>
<td><strong>Ingress(인그레스)</strong></td>
<td>외부에서 내부 서비스에 접근할 수 있도록 설정</td>
</tr>
</tbody></table>
<hr>
<p>쿠버네티스는 <strong>여러 개의 서버에서 실행 중인 Docker 컨테이너를 자동으로 관리하고 최적화하는 컨테이너 오케스트레이션 도구</strong>야.  </p>
<p><strong>Docker가 컨테이너를 실행하는 기술이라면, Kubernetes는 컨테이너들을 효율적으로 배포하고 관리하는 역할을 한다!</strong> </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CI/CD 가 뭔데? (feat.파이프라인)]]></title>
            <link>https://velog.io/@now_here/CICD-%EA%B0%80-%EB%AD%94%EB%8D%B0-feat.%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8</link>
            <guid>https://velog.io/@now_here/CICD-%EA%B0%80-%EB%AD%94%EB%8D%B0-feat.%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8</guid>
            <pubDate>Wed, 05 Mar 2025 03:29:55 GMT</pubDate>
            <description><![CDATA[<h1 id="cicd-지속적-통합-지속적-제공배포">CI/CD 지속적 통합, 지속적 제공/배포</h1>
<p><img src="https://velog.velcdn.com/images/now_here/post/77dd0053-c826-4f58-bf37-a511e704a82f/image.png" alt=""></p>
<h1 id="지속적-통합continuous-integration">지속적 통합(Continuous Integration)</h1>
<p>CI(지속적 통합)는 코드 변경 사항을 자주 공유 브랜치(트렁크)에 병합하고, 자동화된 빌드 &amp; 테스트를 실행하여 품질을 유지하는 <strong>개발 방식</strong>!</p>
<blockquote>
<p>CI/CD에서 &quot;CI&quot;는 언제나 지속적 통합을 의미하며, 지속적 통합은 코드 변경 사항을 다시 공유 분기 또는 &quot;트렁크&quot;로 더 빈번하게 병합하는 것을 용이하게 하는 개발자용 자동화 프로세스입니다. 이러한 업데이트가 이루어지면 병합된 코드 변경 사항의 신뢰성을 보장하기 위해 자동화된 테스트 단계가 트리거됩니다.
...
동시에 개발 중인 애플리케이션의 분기가 너무 많아 상호 충돌할 가능성이 있는 문제에 대한 해결책으로 CI를 고려할 수 있습니다.
...
성공적인 CI란 개발자가 애플리케이션에 적용한 변경 사항들이 병합된 후 이러한 변경 사항이 애플리케이션을 손상시키지 않도록 자동으로 애플리케이션을 빌드하고 다양한 수준의 자동화된 테스트(일반적으로 단위 테스트와 통합 테스트)를 실행하여 해당 변경 사항을 검증하는 것입니다. 즉, 클래스와 기능에서부터 전체 애플리케이션을 구성하는 다양한 모듈에 이르기까지 모든 것을 테스트합니다. CI의 장점 중 하나는 자동화된 테스트를 통해 새 코드와 기존 코드 간 충돌이 발견되는 경우 해당 버그를 빠르게, 자주 수정하기가 더 용이해진다는 것입니다.</p>
</blockquote>
<p>즉, 정리하면 지속적 통합은 빈번하게 푸쉬가 일어나 머지를 하는 환경에서 유용하게 사용할 수 있는 방법이다. CI를 적용하면 커밋을 푸쉬하고 나서 애플리케이션이 정상 동작하는지 클래스에서부터 기능까지, 전체를 구성하는 모듈 전체를 테스트하고 충돌이 발견되는 경우 버그를 빠르고 자주 수정하기가 용이해진다.</p>
<hr>
<h1 id="지속적-제공배포">지속적 제공/배포</h1>
<p>지속적 제공 및/또는 배포(Continuous Delivery/Deployment)는 코드 변경 사항의 통합, 테스트, 제공을 나타내는 프로세스로, 두 가지 부분으로 구성된다. 지속적 제공에는 자동 프로덕션 배포 기능이 없는 반면, 지속적 배포는 업데이트를 프로덕션 환경에 자동으로 배포한다.
어느 것을 선택할지는 개발팀과 운영팀의 위험 허용 범위와 구체적인 요구 사항에 따라 달라진다.</p>
<h2 id="지속적-제공continous-delivery">지속적 제공(Continous Delivery)</h2>
<p>CI에서 빌드와 테스트를 자동화한 다음 검증된 코드를 레포지토리에 자동으로 릴리스 하는 것을 의미한다.
따라서 효과적인 CD를 제공하려면 CI가 개발 파이프라인에 구축되어 있어야 한다.</p>
<blockquote>
<p>일반적으로 지속적 제공이란 개발자의 애플리케이션 변경 사항이 자동으로 버그 테스트를 거치고 리포지토리(예: GitHub, 컨테이너 레지스트리)로 업로드된다는 것을 의미합니다. <strong>이후 리포지토리에서 운영 팀이 변경 사항을 라이브 프로덕션 환경으로 배포할 수 있습니다</strong>. 그 결과 개발 팀과 비즈니스 팀 간 가시성 및 의사 소통 부족 문제가 해결될 수 있습니다. 이를 위한 지속적 제공의 목표는 언제나 프로덕션 환경으로 배포할 준비가 되어 있는 코드베이스를 갖추고 새로운 코드를 배포하는 데 필요한 노력을 최소화하는 것입니다.</p>
</blockquote>
<h2 id="지속적-배포">지속적 배포</h2>
<p>지속적 제공의 확장으로, 개발자의 변경 사항을 리포지토리에서 프로덕션으로 릴리스하는 것을 자동화하여 <strong>고객이 사용할 수 있도록 하는 것</strong>을 의미한다.</p>
<h2 id="파이프라인">파이프라인</h2>
<p>파이프라인은 코드를 빌드, 테스트, 배포하는 과정을 거쳐 소프트웨어 개발을 추진하는 프로세스이며, CI/CD라고도 합니다.
-&gt; CI/CD 파이프라인은 새 버전의 소프트웨어를 제공하기 위해 수행하는 일련의 단계.
CI/CD를 실행했다면 CI/CD 파이프라인이 확립된 것!</p>
<hr>
<h2 id="cicd기본-개념">CI/CD기본 개념</h2>
<p>CI(Continuous Integration)에서 <strong>Runner, Job, Stage</strong>는 핵심 개념이다.
GitLab CI/CD뿐만 아니라, Jenkins, GitHub Actions 등 다른 CI/CD 도구에서도 유사한 개념이 적용된다.
(여기서는 GitLab CI/CD를 바탕으로 기본 개념들을 살펴봄)</p>
<ul>
<li>Runner (실행기) : CI/CD 작업을 실행하는 프로그램 또는 서버</li>
<li>Job (작업단위) : CI/CD 파이프라인에서 실행되는 개별 작업</li>
<li>Stage (단계) 
: Job을 그룹화하는 개념으로, CI/CD에서 실행되는 단계의 순서를 정의</li>
</ul>
<hr>
<h3 id="-1-runner-실행기">** 1. Runner (실행기)**</h3>
<p><strong>Runner</strong>는 <strong>CI/CD 작업을 실행하는 프로그램 또는 서버</strong>를 의미.  </p>
<ul>
<li><code>.gitlab-ci.yml</code> 파일에 정의된 <strong>Job</strong>을 실행하는 역할을 함</li>
<li>GitLab에서는 자체 Runner를 설치하거나, GitLab에서 제공하는 <strong>Shared Runner</strong>를 사용할 수 있음</li>
<li><strong>Docker, Virtual Machine, Bare Metal 환경에서 실행 가능</strong>.</li>
</ul>
<p><strong>Runner 종류</strong></p>
<ol>
<li><strong>Shared Runner</strong>: GitLab이 제공하는 공용 Runner. (무료 플랜에서는 제한적)</li>
<li><strong>Specific Runner</strong>: 특정 프로젝트 또는 그룹에서만 실행되도록 설정한 Runner.</li>
<li><strong>Self-hosted Runner</strong>: 사용자가 직접 설정한 Runner.</li>
</ol>
<p><strong>GitLab Runner 설치 예시</strong></p>
<pre><code class="language-bash"># Runner 다운로드 및 등록
curl -L --output gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
chmod +x gitlab-runner
./gitlab-runner register</code></pre>
<p> <strong>Runner는 실제로 Job을 실행하는 환경</strong>!</p>
<h3 id="-2-job-작업-단위">** 2. Job (작업 단위)**</h3>
<p><strong>Job</strong>은 <strong>CI/CD 파이프라인에서 실행되는 개별 작업</strong>을 의미.  </p>
<ul>
<li><p>예를 들어, <strong>&quot;코드 빌드&quot;, &quot;테스트 실행&quot;, &quot;배포&quot;</strong> 같은 단계가 각각 Job이 될 수 있음.</p>
</li>
<li><p><code>.gitlab-ci.yml</code> 파일에서 Job을 정의할 수 있음.</p>
<p><strong>Job 예제</strong></p>
<pre><code class="language-yaml">test-job:
stage: test
script:
  - echo &quot;Running tests...&quot;
  - mvn test # 예시: Java 테스트 실행</code></pre>
<p>(이 Job은 <code>mvn test</code>를 실행하는 작업을 정의)</p>
</li>
</ul>
<hr>
<h2 id="-3-stage-단계">** 3. Stage (단계)**</h2>
<p><strong>Stage</strong>는 Job을 그룹화하는 개념으로, <strong>CI/CD에서 실행되는 단계의 순서를 정의</strong>해.  </p>
<ul>
<li>CI/CD에서는 여러 개의 Stage가 순차적으로 실행됨.</li>
<li>일반적으로 <strong>build → test → deploy</strong> 순으로 진행됨.</li>
<li>같은 Stage에 있는 Job들은 <strong>병렬 실행</strong>될 수 있음.</li>
</ul>
<h4 id="stage-예제">Stage 예제</h4>
<pre><code class="language-yaml">stages:
  - build
  - test
  - deploy

build-job:
  stage: build
  script:
    - echo &quot;Building the application...&quot;
    - mvn clean package

test-job:
  stage: test
  script:
    - echo &quot;Running tests...&quot;
    - mvn test

deploy-job:
  stage: deploy
  script:
    - echo &quot;Deploying application...&quot;
  only:
    - main    # main 브랜치에서만 실행됨</code></pre>
<p><strong>Stage 실행 순서</strong>:</p>
<ol>
<li><code>build-job</code> 실행  </li>
<li><code>test-job</code> 실행  </li>
<li><code>deploy-job</code> 실행 (단, <code>main</code> 브랜치에서만 실행됨)</li>
</ol>
<h4 id="only">only</h4>
<pre><code class="language-yaml">deploy-job:
  stage: deploy
  script:
    - echo &quot;Deploying to production...&quot;
    - scp build/libs/app.jar user@server:/home/deploy/
  only:
    - main  # main 브랜치에서만 실행됨
</code></pre>
<ul>
<li>only를 사용하면 해당 브랜치가 push 될 때만 해당 작업이 동작함</li>
</ul>
<p>(다른 브랜치에서 <code>push</code> 후 main에 <code>merge</code>를 하면 main 브랜치는 commit이 추가되는 것이므로 CI/CD가 실행됨)</p>
<h4 id="except">except</h4>
<pre><code class="language-yaml">build-job:
  stage: build
  script:
    - echo &quot;Building...&quot;
  except:
    - develop  # develop 브랜치에서는 실행되지 않음</code></pre>
<ul>
<li>특정 브랜치에서 동작 안 하도록 할 수 있음</li>
</ul>
<h4 id="merge-request">Merge Request</h4>
<pre><code class="language-yaml">build-job:
  stage: build
  script:
    - echo &quot;Building...&quot;
  only:
    - merge_requests  #  Merge Request가 생성될 때만 실행됨</code></pre>
<ul>
<li><code>push</code> 할 때는 실행되지 않지만, Merge Request가 생성될 때 자동 실행됨.</li>
</ul>
<h3 id="-runner-job-stage-관계-정리">** Runner, Job, Stage 관계 정리**</h3>
<ol>
<li><strong>Runner</strong>: 실제로 Job을 실행하는 환경.</li>
<li><strong>Job</strong>: 실행할 개별 작업(예: 빌드, 테스트, 배포).</li>
<li><strong>Stage</strong>: 여러 개의 Job을 그룹화하여 실행 순서를 정의.</li>
</ol>
<hr>
<h2 id="cicd-구현-도구-및-구현-방법">CI/CD 구현 도구 및 구현 방법</h2>
<p>앞서 말한 것처럼 CI는 메인 브랜치에 빈번하게 병합을 해도 문제 없이 실행되도록 하는 빌드/테스트를 자동화하는 방법론이다. 이를 구현하기 위해서 사용하는 도구들을 살펴보자.</p>
<ul>
<li>Jenkins: 가장 널리 사용되는 오픈소스 CI/CD 도구.</li>
<li>GitHub Actions: GitHub에서 제공하는 CI/CD 기능.</li>
<li>GitLab CI/CD: GitLab에 내장된 CI/CD 기능.</li>
<li>CircleCI, Travis CI: 클라우드 기반 CI/CD 서비스.</li>
</ul>
<p>이 중에 지금 프로젝트에 사용되는 <strong>GitLab CI/CD</strong>의 사용방법을 살펴보자.</p>
<hr>
<h3 id="gitlab-cicd-장단점">GitLab CI/CD 장,단점</h3>
<h4 id="-gitlab-cicd의-장점">** GitLab CI/CD의 장점**</h4>
<ol>
<li><p><strong>GitLab과 완벽한 통합</strong>  </p>
<ul>
<li>GitLab 저장소에 내장된 CI/CD 기능이므로 <strong>별도 설정 없이 바로 사용 가능</strong>.</li>
<li>별도의 Jenkins 같은 외부 CI/CD 도구 없이 GitLab 내에서 처리 가능.</li>
</ul>
</li>
<li><p><strong>YAML 기반의 간단한 설정</strong>  </p>
<ul>
<li><code>.gitlab-ci.yml</code> 파일을 추가하면 CI/CD 파이프라인을 쉽게 설정할 수 있음.</li>
<li>코드 기반으로 관리 가능하여 버전 관리가 용이.</li>
</ul>
</li>
<li><p><strong>자동화된 병렬 실행 및 병렬 테스트</strong>  </p>
<ul>
<li>여러 작업을 동시에 실행할 수 있어 <strong>빌드 속도 향상</strong>.</li>
</ul>
</li>
<li><p><strong>강력한 권한 관리</strong>  </p>
<ul>
<li>GitLab 자체의 <strong>RBAC(Role-Based Access Control)</strong>을 통해 프로젝트 단위로 권한 관리 가능.</li>
</ul>
</li>
<li><p><strong>자체 Runner 사용 가능</strong>  </p>
<ul>
<li>GitLab Runner를 직접 구축하여 <strong>자원 최적화</strong> 가능.</li>
<li>퍼블릭 클라우드 또는 온프레미스 환경에서도 유연하게 활용 가능.</li>
</ul>
</li>
<li><p><strong>무료 사용 가능</strong>  </p>
<ul>
<li>오픈소스 버전(GitLab CE)에서도 충분한 기능을 제공.</li>
<li>클라우드(GitLab.com)에서는 기본적으로 400분의 CI/CD 실행 시간을 무료 제공.</li>
</ul>
</li>
</ol>
<h4 id="-gitlab-cicd의-단점">** GitLab CI/CD의 단점**</h4>
<ol>
<li><p><strong>초기 설정이 다소 복잡할 수 있음</strong>  </p>
<ul>
<li>GitHub Actions처럼 기본 템플릿이 많지 않아 처음 설정할 때 <strong>직접 YAML을 작성해야 함</strong>.</li>
<li>Runner를 별도로 설정해야 하는 경우가 있음.</li>
</ul>
</li>
<li><p><strong>GitLab Runner 관리 필요</strong>  </p>
<ul>
<li>자체 Runner를 운영할 경우 <strong>리소스 관리와 유지보수 부담</strong>이 생김.</li>
<li>클라우드에서 무료 실행 시간이 제한적이라 추가 사용 시 비용 발생 가능.</li>
</ul>
</li>
<li><p><strong>서드파티 연동이 제한적</strong>  </p>
<ul>
<li>Jenkins, CircleCI 등에 비해 <strong>플러그인 지원이 적음</strong>.</li>
<li>일부 외부 서비스와의 연동이 번거로울 수 있음.</li>
</ul>
</li>
<li><p><strong>빌드 속도가 느릴 수 있음</strong>  </p>
<ul>
<li>기본 Shared Runner의 성능이 제한적이므로 복잡한 빌드는 <strong>시간이 오래 걸릴 수 있음</strong>.</li>
<li>고성능 실행이 필요하면 유료 플랜이나 자체 Runner를 사용해야 함.</li>
</ul>
</li>
</ol>
<h3 id="gitlab-cicd-구현-예시">GitLab CI/CD 구현 예시</h3>
<p>GitLab CI/CD의 4단계를 다음과 같이 구성했다고 하자.
<code>build → package → deploy → sync</code>
GitLab CI/CD에서 <strong>각 <code>stage</code>는 특정 작업(Job)들을 실행하는 논리적인 단계</strong>로, 각 단계별로 무슨 작업을 수행하는지 알아보자.</p>
<pre><code class="language-yaml">image: gradle:8.0-jdk17  # GitLab Runner에서 사용할 기본 이미지

stages:
  - build
  - package
  - deploy
  - sync

variables:
  GRADLE_USER_HOME: &quot;$CI_PROJECT_DIR/.gradle&quot;  # Gradle 캐시 경로

cache:
  key: gradle
  paths:
    - .gradle/wrapper
    - .gradle/caches

build-job:
  stage: build
  script:
    - echo &quot;Building the Java application...&quot;
    - gradle clean build -x test
  artifacts:
    paths:
      - build/

package-job:
  stage: package
  script:
    - echo &quot;Packaging the application...&quot;
    - gradle build
  artifacts:
    paths:
      - build/libs/*.jar

deploy-job:
  stage: deploy
  script:
    - echo &quot;Deploying application...&quot;
    - scp build/libs/myapp.jar user@server:/home/deploy/
  only:
    - main

sync-job:
  stage: sync
  script:
    - echo &quot;Syncing application...&quot;
    - ssh user@server &quot;sudo systemctl restart myapp&quot;
    - ssh user@server &quot;rm -rf /home/deploy/cache/*&quot;
  only:
    - main</code></pre>
<h3 id="-build-단계">** Build 단계**</h3>
<h4 id="-역할">** 역할**</h4>
<ul>
<li><strong>소스 코드를 컴파일하고 애플리케이션을 빌드하는 단계</strong></li>
<li>Gradle 프로젝트에서는 <code>gradle build</code> 또는 <code>gradle assemble</code>을 실행하는 것이 일반적임.</li>
</ul>
<h3 id="-예제-build-job">** 예제 (<code>build-job</code>)**</h3>
<pre><code class="language-yaml">build-job:
  stage: build
  script:
    - echo &quot;Building the Java application...&quot;
    - gradle clean build -x test  # 테스트 제외하고 빌드 수행
  artifacts:
    paths:
      - build/  # 빌드된 결과를 저장 (JAR, WAR 등)</code></pre>
<p>✔ <strong>결과물:</strong> <code>build/</code> 폴더에 <code>.class</code> 파일이 포함된 컴파일된 코드가 저장됨.<br>✔ <strong>테스트를 제외(-x test)</strong>하여 빌드 속도를 최적화할 수 있음.  </p>
<hr>
<h3 id="-package-단계">** Package 단계**</h3>
<h4 id="-역할-1">** 역할**</h4>
<ul>
<li>빌드된 애플리케이션을 <strong>배포 가능한 형태(JAR, WAR, Docker Image)로 패키징하는 단계</strong></li>
<li>Java 프로젝트에서는 <strong>JAR/WAR 파일 생성</strong>을 수행.</li>
<li>필요하면 Docker 이미지를 생성할 수도 있음.</li>
</ul>
<h3 id="-예제-package-job">** 예제 (<code>package-job</code>)**</h3>
<pre><code class="language-yaml">package-job:
  stage: package
  script:
    - echo &quot;Packaging the Java application...&quot;
    - gradle build  # JAR 또는 WAR 생성
  artifacts:
    paths:
      - build/libs/*.jar  # 빌드된 JAR 파일을 저장</code></pre>
<p>✔ <strong>결과물:</strong> <code>build/libs/</code>에 <code>.jar</code> 또는 <code>.war</code> 파일이 생성됨.<br>✔ <strong>이 단계에서 생성된 아티팩트를 배포 단계에서 사용 가능.</strong>  </p>
<h4 id="-추가-docker-이미지로-패키징-선택">** 추가: Docker 이미지로 패키징 (선택)**</h4>
<pre><code class="language-yaml">package-docker-job:
  stage: package
  script:
    - echo &quot;Building Docker Image...&quot;
    - docker build -t myapp:latest .
    - docker save myapp:latest &gt; myapp.tar
  artifacts:
    paths:
      - myapp.tar  # Docker 이미지를 저장</code></pre>
<p>✔ <strong>Docker로 패키징할 경우 <code>docker build</code>를 사용하여 컨테이너 이미지 생성.</strong><br>✔ <strong>Docker Hub 또는 프라이빗 레지스트리에 push 가능.</strong>  </p>
<hr>
<h2 id="-deploy-단계">** Deploy 단계**</h2>
<h3 id="-역할-2">** 역할**</h3>
<ul>
<li>패키징된 애플리케이션을 <strong>운영 서버 또는 테스트 서버에 배포</strong></li>
<li><code>scp</code>, <code>rsync</code>, <code>kubectl apply</code> 등을 사용하여 서버로 업로드할 수 있음.</li>
</ul>
<h3 id="-예제-deploy-job">** 예제 (<code>deploy-job</code>)**</h3>
<pre><code class="language-yaml">deploy-job:
  stage: deploy
  script:
    - echo &quot;Deploying application...&quot;
    - scp build/libs/myapp.jar user@server:/home/deploy/  # 원격 서버에 업로드
  only:
    - main  # main 브랜치에서만 실행</code></pre>
<p>✔ <strong>배포 대상:</strong> <code>main</code> 브랜치에서만 실행되도록 설정.<br>✔ <strong>배포 방식:</strong> <code>scp</code>를 이용해 원격 서버에 <code>.jar</code> 파일을 업로드.  </p>
<h4 id="-추가-kubernetes-환경에서-배포">** 추가: Kubernetes 환경에서 배포**</h4>
<pre><code class="language-yaml">deploy-k8s-job:
  stage: deploy
  script:
    - echo &quot;Deploying to Kubernetes...&quot;
    - kubectl apply -f k8s/deployment.yaml
  only:
    - main</code></pre>
<p>✔ <strong>Kubernetes 환경에서는 <code>kubectl apply</code>를 사용하여 배포 가능.</strong>  </p>
<hr>
<h2 id="-sync-단계">** Sync 단계**</h2>
<h3 id="-역할-3">** 역할**</h3>
<ul>
<li>여러 서버 간 파일을 동기화하거나, 최신 코드를 반영하는 과정.</li>
<li><strong>배포 후 캐시 삭제, 데이터베이스 마이그레이션, 설정 파일 업데이트 등의 작업을 수행.</strong></li>
</ul>
<h3 id="-예제-sync-job">** 예제 (<code>sync-job</code>)**</h3>
<pre><code class="language-yaml">sync-job:
  stage: sync
  script:
    - echo &quot;Syncing application data...&quot;
    - ssh user@server &quot;sudo systemctl restart myapp&quot;  # 서버에서 애플리케이션 재시작
    - ssh user@server &quot;rm -rf /home/deploy/cache/*&quot;  # 캐시 삭제
  only:
    - main</code></pre>
<p>✔ <strong>배포 후 동기화 과정:</strong> 애플리케이션을 재시작하고, 불필요한 캐시를 삭제.<br>✔ <strong>이 과정은 보통 배포 후 실행됨.</strong>  </p>
<h2 id="-결론">** 결론**</h2>
<p>✔ <strong><code>build</code> 단계</strong>: 소스 코드 빌드<br>✔ <strong><code>package</code> 단계</strong>: <code>.jar</code>, <code>.war</code> 파일 생성 또는 Docker 이미지 빌드<br>✔ <strong><code>deploy</code> 단계</strong>: 패키징된 파일을 운영 서버에 배포<br>✔ <strong><code>sync</code> 단계</strong>: 서버 설정 동기화 및 애플리케이션 재시작  </p>
<hr>
<h3 id="gitlab-cicd-기본-파일">GitLab CI/CD 기본 파일</h3>
<p>GitLab CI/CD는 <code>.gitlab-ci.yml</code> 파일을 기반으로 동작한다. 
-&gt; <em><strong>프로젝트에서 CI/CD 가 어떻게 설정되었는지 보고 싶다면 <code>.gitlab-ci.yml</code> 파일 찾아보자!</strong></em></p>
<ul>
<li><code>.gitlab-ci.yml</code> 파일에 빌드, 테스트, 배포 단계를 정의하면, GitLab이 자동으로 실행</li>
<li>실행 환경은 GitLab Runner라는 에이전트가 담당</li>
<li>이름은 바꿀 수 있지만 기본적으로 <code>.gitlab-ci.yml</code> 파일이 존재해야 GitLab이 파일을 감지함.</li>
<li>include를 통해 CI/CD 설정을 다른 파일에서 관리하도록 할 수 있음.</li>
</ul>
<h4 id="include-예제">include 예제</h4>
<pre><code class="language-yaml">## .gitlab-ci.yml 파일
include:
  - local: &#39;custom-ci.yml&#39;  # &#39;custom-ci.yml&#39;을 불러와서 사용</code></pre>
<hr>
<p> <strong>GitLab CI/CD 공식 문서</strong><br> <a href="https://docs.gitlab.com/ee/ci/">https://docs.gitlab.com/ee/ci/</a></p>
<hr>
<h1 id="정리">정리</h1>
<p>지금까지 CI/CD를 살펴보았다. 사용방법을 보면서는 CI/CD가 하나의 흐름이기 때문에 구분하지 않고 보았다.
마지막으로 이를 구분하여 따로 정리하면 아래와 같다.</p>
<hr>
<h2 id="-cicontinuous-integration-vs-cdcontinuous-deploymentdelivery">** CI(Continuous Integration) vs CD(Continuous Deployment/Delivery)**</h2>
<table>
<thead>
<tr>
<th>개념</th>
<th>설명</th>
<th>예제</th>
</tr>
</thead>
<tbody><tr>
<td><strong>CI (지속적 통합)</strong></td>
<td>코드 변경 사항을 자주 병합하고, 자동으로 빌드 &amp; 테스트 실행</td>
<td><code>build</code>, <code>test</code> 단계</td>
</tr>
<tr>
<td><strong>CD (지속적 배포/전달)</strong></td>
<td>검증된 코드 변경 사항을 자동으로 배포(Deployment) 또는 배포 가능한 상태로 유지(Delivery)</td>
<td><code>deploy</code>, <code>sync</code> 단계</td>
</tr>
</tbody></table>
<hr>
<h2 id="-지금까지-본-내용에서-cicd-구분">** 지금까지 본 내용에서 CI/CD 구분**</h2>
<h3 id="-ci-continuous-integration-관련-내용">** CI (Continuous Integration) 관련 내용**</h3>
<ul>
<li><code>build-job</code> (<code>gradle build</code>, <code>mvn package</code> 등)  </li>
<li><code>test-job</code> (<code>gradle test</code>, <code>mvn test</code>, <code>pytest</code> 등)  </li>
</ul>
<p>✔ <strong>CI는 코드가 변경될 때마다 자동으로 빌드하고 테스트를 실행하는 과정</strong>  </p>
<h3 id="-cd-continuous-deploymentdelivery-관련-내용">** CD (Continuous Deployment/Delivery) 관련 내용**</h3>
<ul>
<li><code>deploy-job</code> (<code>scp</code>, <code>kubectl apply</code>, <code>docker push</code> 등)  </li>
<li><code>sync-job</code> (<code>systemctl restart</code>, 캐시 삭제, 설정 파일 동기화 등)  </li>
</ul>
<p>✔ <strong>CD는 검증된 코드가 자동으로 배포되거나, 배포 가능한 상태로 유지되는 과정</strong>  </p>
<hr>
<h2 id="-cicd-흐름을-다시-정리하면">** CI/CD 흐름을 다시 정리하면?**</h2>
<pre><code class="language-yaml">stages:
  - build   #  CI: 코드 빌드
  - test    #  CI: 자동화된 테스트 실행
  - deploy  #  CD: 배포 실행
  - sync    #  CD: 서버 동기화 (필요한 경우)

build-job:
  stage: build
  script:
    - echo &quot;Building the application...&quot;
    - gradle build -x test

test-job:
  stage: test
  script:
    - echo &quot;Running tests...&quot;
    - gradle test

deploy-job:
  stage: deploy
  script:
    - echo &quot;Deploying the application...&quot;
    - scp build/libs/app.jar user@server:/home/deploy/

sync-job:
  stage: sync
  script:
    - echo &quot;Restarting application...&quot;
    - ssh user@server &quot;sudo systemctl restart myapp&quot;</code></pre>
<p>✔ <code>build</code>, <code>test</code> → CI<br>✔ <code>deploy</code>, <code>sync</code> → CD  </p>
<p>[참고] <a href="https://www.redhat.com/ko/topics/devops/what-is-ci-cd#%EC%9A%94%EC%95%BD">https://www.redhat.com/ko/topics/devops/what-is-ci-cd#%EC%9A%94%EC%95%BD</a></p>
<p><a href="https://www.redhat.com/ko/topics/devops/what-is-continuous-delivery">https://www.redhat.com/ko/topics/devops/what-is-continuous-delivery</a></p>
<p><a href="https://docs.gitlab.com/ee/ci/">https://docs.gitlab.com/ee/ci/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[16장 트랜잭션과 락, 2차 캐시]]></title>
            <link>https://velog.io/@now_here/16%EC%9E%A5-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EA%B3%BC-%EB%9D%BD-2%EC%B0%A8-%EC%BA%90%EC%8B%9C</link>
            <guid>https://velog.io/@now_here/16%EC%9E%A5-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EA%B3%BC-%EB%9D%BD-2%EC%B0%A8-%EC%BA%90%EC%8B%9C</guid>
            <pubDate>Sun, 09 Feb 2025 09:45:25 GMT</pubDate>
            <description><![CDATA[<h1 id="트랜잭션과-락">트랜잭션과 락</h1>
<h2 id="트랜잭션과-격리-수준">트랜잭션과 격리 수준</h2>
<p>트랜잭션은 ASID를 보장해야 한다.</p>
<ul>
<li><strong>원자성</strong> (Atomicity)
: 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공 하거나 실패해야 한다.</li>
<li><strong>일관성</strong> (Consisitency)
: 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다.</li>
<li><strong>격리성</strong> (Isolation)
: 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 동시에 같은 데이터를 수정하지 못하도록 해야 한다.</li>
<li><strong>지속성</strong> (Durability)
: 트랜잭션이 성공적으로 끝나면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 북구해야 한다.</li>
</ul>
<p>트랜잭션간의 <strong>격리성을 완벽히 보장</strong>하려면 트랜재견을 거의 차례대로 실행해야 하는데 이러면 <strong>동시성 처리 성능</strong>이 매우 나빠진다.</p>
<p>ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의했다.</p>
<ul>
<li>READ UNCOMMITED (커밋되지 않은 읽기)</li>
<li>READ COMMITTED (커밋된 읽기)</li>
<li>REPETABLE READ (반복 가능한 읽기)</li>
<li>SERIALIZABLE (직렬화 가능)</li>
</ul>
<p>(READ UNCOMMITED 격리 수준이 가장 낮고 SERIALIZABLE 가장 높음)
격리 수준이 낮을수록 동시성이 증가하지만 다양한 문제가 발생한다.</p>
<p><img src="https://velog.velcdn.com/images/now_here/post/7566f840-9b49-4a26-888c-75f1be33857f/image.png" alt=""></p>
<h3 id="트랜잭션-격리-수준에-따른-문제점">트랜잭션 격리 수준에 따른 문제점</h3>
<p><strong>READ UNCOMMITTED</strong> : DIRTY READ를 허용하는 격리 수준. 즉, 커밋하지 않은 데이터를 읽을 수 있다.
트랜잭션1이 수정하고 있는 중 커밋하지 않아도 트랜잭션 2가 수정중인 데이터를 읽을 수 있다. 이를  <strong>DIRTY READ</strong>라고 한다. 
또한 트랜잭션 2가 DIRTY READ한 데이터를 사용하는데 트랜잭션 1이 롤백하면 데이터 정합성 문제가 발생할 수 있다.</p>
<p><strong>READ COMMITTED</strong> : DIRTY READ를 허용하지 않지만 NON-REPEATABLE READ 허용하는 격리 수준.
즉, 커밋한 데이터만 읽을 수 있다.
DIRTY READ가 발생하지 않음
예를 들어 트랜잭션 1이 회원 A를 조회 중인데 갑자기 트랜잭션 2가 회원 A를 수정하고 커밋하면 트랜잭션 1이 다시 회원 A를 조회했을 때 수정된 데이터가 조회되는데 이처럼 반복해서 같은 데이터를 읽을 수 없는 상태를 <strong>NON-REPEATABLE READ</strong> 라고 한다.</p>
<p><strong>REPEATABLE READ</strong> : NON-REPEATABLE READ를 허용하지 않지만 PHANTOM READ는 허용하는 격리 수준.
즉, 한번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회된다.
다만 데이터가 추가되면 이는 반영돼서 조회된다.
예를 들어 트랜잭션 1이 10살 이하의 회원을 조회했는데 트랜잭션 2가 5살 회원을 추가하고 커밋하면 트랜잭션1이 다시 10살 이하의 회원을 조회했을 때 회원 하나가 추가된 상태로 조회되는데 이처럼 반복 조회시 결과 집합이 달라지는 것을 <strong>PHANTOM READ</strong>라고 한다. </p>
<p><strong>SERIALIZABLE</strong> : 가장 엄격한 트랜잭션 격리 수준.
PHANTOM READ 발생하지 않지만 동시성 처리 성능이 급격히 떨어진다.</p>
<p>애플리케이션 대부분은 동시성 처리가 중요하므로 데이터베이스들은 보통 READ COMMITTED 격릴 수준을 기본으로 사용한다. 일부 중요한 비즈니스 로직에 더 높은 격리 수준이 필요하면 데이터베이스 트랜잭션이 제공하는 잠금 기능을 사용하면 된다.</p>
<blockquote>
<p>참고 : 트랜잭션 격리 수준에 따른 동작 방식은 데이터베이스마다 다르다. 최근에는 더 많은 동시성 처리를 위해 락보다는 MVCC를 사용한다고 한다.</p>
</blockquote>
<h2 id="낙관적-락과-비관적-락-기초">낙관적 락과 비관적 락 기초</h2>
<p>JPA의 영속성 컨텍스트(1차 캐시)를 적절히 활용하면 데이터베이스 트랜잭션이 READ COMMITED 격리 수준이어도 애플리케이션 레벨에서 반복 가능한 읽기(REPEATABLE READ)가 가능하다.
물론 엔티티가 아닌 스칼라 값을 직접 조회하면 영속성 컨텍스트가 관리하지 않기 때문에 반복 가능한 읽기를 할 수 없다.
JPA는 데이터베이스 격리 수준을 READ COMMTIED로 가정한다. 만약 일부 로직에 더 높은 격리 수준이 필요하면 <strong>낙관적 락</strong>과 <strong>비관적 락</strong> 중 하나를 사용하면 된다.</p>
<h3 id="낙관적-락">낙관적 락</h3>
<ul>
<li>트랜잭션 대부분은 충돌이 발생하지 않는다고 낙관적으로 가정하는 방법</li>
</ul>
<p>데이터베이스가 제공하는 락 기능이 아닌 <strong>JPA가 제공하는 버전 관리 기능</strong>을 사용한다. 즉, 애플리케이션이 제공하는 락이다.
특징 : 트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없다.</p>
<h3 id="비관적-락">비관적 락</h3>
<ul>
<li>트랜잭션의 충돌이 발생한다고 가정하고 우선 락을 거는 방법</li>
</ul>
<p><strong>데이터베이스가 제공하는 락 기능</strong>을 사용한다.
ex) select for update 구문</p>
<h3 id="두-번의-갱실-분실-문제-second-lost-updates-problem">두 번의 갱실 분실 문제 (second lost updates problem)</h3>
<p>사용자 1,2가 동시에 게시물의 제목을 수정한다고 할 때 1이 먼저 완료를 하고 2가 나중에 완료를 한 경우 2로 덮어지는 경우가 발생한다. 이것을 <strong>두 번의 갱실 분실 문제</strong>라고 한다.
해당 문제는 데이터베이스 트랜잭션의 범위를 넘어서 트랜잭션으로 해결할 수 없다. 이를 해결하는 방법으로 다음 3가지가 있다.</p>
<ul>
<li>마지막 커밋만 인정하기(기본) : 사용자 1의 내용은 무시하고 마지막 커밋만 인정</li>
<li>최초 커밋만 인정하기 : 사용자 1이 완료했으므로 사용자 2가 커밋을 하면 오류가 발생한다.</li>
<li>충돌하는 갱신 내용 병합하기 : 사용자 1과 사용자 2의 수정사항을 병합한다.</li>
</ul>
<h2 id="version">@Version</h2>
<p>JPA가 제공하는 낙관적 락을 사용하려면 @Version을 사용해서 버전 관리 기능을 추가해야 한다.</p>
<h4 id="version-적용-가능-타입--longlong-integer-int-short-short-timestamp">@Version 적용 가능 타입 : Long(long), Integer (int), Short (short), Timestamp</h4>
<pre><code class="language-java">@Entity
public class Board {
        @Id
        private String id;
        private String title;

        @Version
        private Integer version;
}</code></pre>
<p>버전 관리 기능을 사용하려면 위처럼 버전 관리용 필드를 생성하고 @Version을 붙이면 된다. 이러면 엔티티를 수정할 때마다 버전이 하나씩 자동으로 증가한다.
엔티티를 수정할 때 조회 시점의 버전과 수정 시점의 버전이 다르면 예외가 발생하여 <strong>최초 커밋만 인정하기</strong>가 적용된다.
예를 보자.</p>
<p><img src="https://velog.velcdn.com/images/now_here/post/5482fda8-255b-4aaf-9d50-feddedb65afa/image.png" alt=""></p>
<h4 id="예제">예제</h4>
<pre><code class="language-java">//트랜잭션1 조회 title=&quot;제목A&quot;, version=1
Board board = em.find(Board.class, id);

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

board.setTitle(&quot;제목B&quot;); //트랜잭션 1 데이터 수정

save(board);
tx.commit(); //예외 발생, 데이터베이스 version=2 엔티티 version=1</code></pre>
<p>트랜잭션1이 제목을 바꾸려고 조회를 했다. 그런데 트랜잭션 2에서 조회를 한 다음 제목을 수정하고 커밋해서 version이 수정되었다. 이러면 트랜잭션1이 제목을 변경하고 커밋하려고 하면 version이 달라 예외가 발생한다.</p>
<h3 id="버전-정보-비교-방법">버전 정보 비교 방법</h3>
<p>엔티티를 수정하고 트랜잭션을 커밋하면 영속성 컨텍스트를 플러시 하면서 UPDATE 쿼리를 실행한다. 이때 버전을 사용하는 엔티티면 검색 조건에 엔티티의 버전 정보를 추가한다.</p>
<pre><code class="language-sql">UPDATE BOARD
SET
        TITLE=?
        VERSION=? (버전 + 1 증가)
WHERE
        ID=?
        AND VERSION=? (버전비교)</code></pre>
<p>이처럼 version을 비교하고 엔티티 버전이 같으면 수정을 진행하고 버전의 값을 하나 올린다. 데이터베이스에 버전이 증가해서 수정 중인 엔티티와 버전이 다를 경우에는 수정할 대상이 없다. 이럴 경우 버전이 이미 증가한 것으로 보고 JPA가 예외를 발생시킨다.
버전은 엔티티의 값을 변경하면 증가한다. 값 타입인 임베디드 타입과 값 타입 컬렉션은 논리적인 개념상 해당 엔티티의 값이므로 수정하면 엔티티의 버전이 증가한다.
단 연관관계 필드는 외래 키를 관리하는 연관관계의 주인 필드를 수정할 때만 버전이 증가한다.
@Version은 JPA가 관리하므로 개발자가 벌크 연산을 제외하고는 임의로 수정하면 안 된다.</p>
<blockquote>
<p>참고 : 벌크 연산은 버전을 무시한다. 벌크 연산에서 버전을 증가하려면 버전 필드를 강제로 증가시켜야 함</p>
</blockquote>
<h2 id="jpa-락-사용">JPA 락 사용</h2>
<p>JPA를 사용할 때 추천하는 전략은 READ COMMITED + 낙관적 버전 관리(두 번의 갱신 내역 분실 문제 예방)이다.</p>
<h3 id="락-적용-위치">락 적용 위치</h3>
<ul>
<li>EntityManager.lock(), EntityManger.find(), EntityManger.refresh()</li>
<li>Query.setLockMode() (TypeQuery 포함)</li>
<li>@NamedQuery</li>
</ul>
<h4 id="조회하면서-즉시-락-걸기">조회하면서 즉시 락 걸기</h4>
<pre><code class="language-java">Board board = em.find(Board.class, id, LockModeType.OPTIMISTIC);</code></pre>
<h4 id="필요할-때-락-걸기">필요할 때 락 걸기</h4>
<pre><code class="language-java">Board board = em.find(Board.class, id);
...
em.lock(board, LockModeType.OPTIMISTIC);</code></pre>
<p>JPA가 제공하는 락 옵션은 아래와 같다. (javax.persistence.LockModeType)
<img src="https://velog.velcdn.com/images/now_here/post/36e2bed1-5f41-4ed8-bb45-03097205a505/image.png" alt=""></p>
<h2 id="jpa-낙관적-락">JPA 낙관적 락</h2>
<p>JPA가 제공하는 낙관적 락은 버전(@Version)을 사용한다. 낙관적 락은 트랜잭션을 커밋하는 시점에 충돌을 알 수 있다. </p>
<h3 id="낙관적-락에서-발생하는-예외">낙관적 락에서 발생하는 예외</h3>
<ul>
<li>javax.persistence.OptimisticLockException (JPA 예외)</li>
<li>org.hibernate.StaleObjectStateException (하이버네이트 예외)</li>
<li>org.springframework.orm.ObjectOptimisticLockingFailureException (스프링 예외 추상화)</li>
</ul>
<p>낙관적락은 옵션 없이 @Version으로만으로 적용된다.</p>
<h2 id="낙관적-락-옵션">낙관적 락 옵션</h2>
<h3 id="none">NONE</h3>
<ul>
<li>옵션없이도 @Version만으로 낙관적 락 적용 </li>
</ul>
<p><strong>용도</strong>: 조회한 엔티티를 수정할 때 다른 트랜잭션에 의해 변경(삭제)되지 않아야 하고, 조회 시점부터 수정 시점까지를 보장
<strong>동작</strong>: 엔티티를 수정할 때 버전을 체크하면서 버전을 증가(UPDATE 쿼리 사용). 이때 DB의 버전 값이 현재 버전이 아니면 예외가 발생
<strong>이점</strong>: 두 번의 갱신 분실 문제를 예방</p>
<h3 id="optimistic">OPTIMISTIC</h3>
<p>@Version만 사용하면 엔티티가 수정해야 버전을 체크하지만 OPTIMISTIC 옵션을 추가하면 엔티티를 <strong>조회</strong>만 해도 버전을 체크한다.</p>
<ul>
<li>한 번 조회한 엔티티는 트랜잭션을 종료할 때까지 다른 트랜잭션에서 변경하지 않음을 보장</li>
</ul>
<p><strong>용도</strong>: 조회한 엔티티는 트랜잭션이 끝날 때까지 다른 트랜잭션에 의해 변경되지 않아야 함. 조회 시점부터 트랜잭션이 끝날 때까지 조회한 엔티티가 변경되지 않음을 보장
<strong>동작</strong> : 트랜잭션을 커밋할 때 버전 정보를 조회해서(SELECT 쿼리 사용) 현재 엔티티의 버전과 같은지 검증하고 만약 같지 않으면 예외가 발생
<strong>이점</strong> : OPTIMISTIC 옵션은 DIRTY READ와 NON-REPEATABLE READ를 방지</p>
<h3 id="optimistic_force_increment">OPTIMISTIC_FORCE_INCREMENT</h3>
<ul>
<li>낙관적 락을 사용하면서 버전을 강제로 증가한다.</li>
</ul>
<p><strong>용도</strong> : 논리적인 단위의 엔티티 묶음을 관리할 수 있다. 예를 들어 게시물과 첨부파일이 일대다, 다대일의 양방향 연관관계이고 첨부파일이 연관관계의 주인이다. 게시물을 수정하는데 단순히 첨부파일만 추가하면 게시물의 버전은 증가하지 않는다. 해당 게시물은 물리적으로 변경되지 않았지만, 논리적으로는 변경되었다. 이때 게시물의 버전도 강제로 증가하려면 이 옵션을 사용하면 된다.
동작 : 엔티티를 수정하지 않아도 트랜잭션을 커밋할 때 UPDATE 쿼리를 사용해서 버전 정보를 강제로 증가시킨다. 이때 데이터베이스의 버전이 엔티티의 버전과 다르면 예외가 발생한다. 추가로 엔티티를 수정하면 수정 시 버전 UPDATE가 발생한다. 따라서 총 2번의 버전 증가가 나타날 수 있다.
이점 : 강제로 버전을 증가해서 논리적인 단위의 엔티티 묶음을 버전 관리할 수 있다.</p>
<h2 id="jpa-비관적-락">JPA 비관적 락</h2>
<p>JPA가 제공하는 비관적 락은 <strong>데이터베이스 트랜잭션 락 메커니즘</strong>에 의존한다.
주로 SQL 쿼리에 select for update 구문을 사용하면서 시작하고 버전 정보는 사용하지 않는다.
비관적 락ㅇ느 주로 <strong>PESSIMISTIC_WIRTE</strong> 모드를 사용한다.</p>
<h3 id="비관적-락-특징">비관적 락 특징</h3>
<ul>
<li>엔티티가 아닌 스칼라 타입을 조회할 때도 사용할 수 있다.</li>
<li>데이터를 수정하는 즉시 트랜잭션 충돌을 감지할 수 있다</li>
</ul>
<h3 id="비관적-락에서-발생하는-예외">비관적 락에서 발생하는 예외</h3>
<ul>
<li>javax.persistence.PessimisticLockException(JPA 예외)</li>
<li>org.springframework.dao.PessimisticLockingFailureException(스프링 예외 추상화)</li>
</ul>
<h3 id="pessimistic_write">PESSIMISTIC_WRITE</h3>
<p>비관적 락의 일반적 옵션. 데이터베이스에 쓰기 락을 걸때 사용한다.
<strong>용도</strong> : 데이터베이스에 쓰기 락을 건다.
<strong>동작</strong> : 데이터베이스 select for update를 사용해서 락을 건다.
<strong>이점</strong> : NON-REPEATABLE READ를 방지한다. 락이 걸린 로우는 다른 트랜잭션이 수정할 수 없다.</p>
<h3 id="pessimistic_read">PESSIMISTIC_READ</h3>
<p>데이터를 반복 읽기만 하고 수정하지 않는 용도로 락을 걸 때 사용한다.(일반적으로 잘 사용하지 않음)
데이터베이스 방언에 의해 PESSIMISTIC_WRITE로 동작한다.</p>
<ul>
<li>MySQL : lock in share mode</li>
<li>PostgreSQL : for share</li>
</ul>
<h3 id="pessimistic_force_increment">PESSIMISTIC_FORCE_INCREMENT</h3>
<p>비관적 락중 유일하게 버전 정보를 사용하고 비관적 락이지만 버전 정보를 강제로 증가시킨다.
하이버네이트는 nowait를 지원하는 데이터베이스에 대해서 for update nowait 옵션을 적용한다.</p>
<ul>
<li>오라클 : for update nowait</li>
<li>PostgreSQL : for update nowait</li>
<li>nowait를 지원하지 않으면 for update가 사용된다.</li>
</ul>
<h2 id="비관적-락과-타임아웃">비관적 락과 타임아웃</h2>
<p>비관적 락을 사용하면 락을 획득할 때까지 트랜잭션이 대기하는데 무한정 기다릴 수 없으므로 타임아웃 시간을 줄 수 있다.
타임아웃은 데이터베이스 특성에 따라 동작하지 않을 수 있다.</p>
<hr>
<h1 id="2차-캐시">2차 캐시</h1>
<p>네트워클 통해 데이터베이스에 접근하는 시간 비용은 애플리케이션 서버에서 내부 메모리에 접근하는 시간비용보다 수만에서 수십만 배 비싸다.
영속성 컨텍스트 내부에 1차 캐시를 이용해서 시간을 줄일 수 있지만 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효하다. OSIV를 이용해도 클라이언트 요청이 들어오고 끝날 때까지만 1차 캐시가 유효하다. 따라서 애플리케이셔 전체로 보면 데이터베이스 접근 횟수를 획기적으로 줄이지는 못한다.</p>
<p>하이버네이트를 포함한 대부분의 JPA 구현체들은 애플리케이션 범위의 캐시를 지원하는데 이것을 <strong>공유 캐시</strong> 또는 <strong>2차 캐시</strong>라 한다. 이런 2차 캐시를 이요하면 애플리케이션 조회 성능을 향상할 수 있다.</p>
<h2 id="1차-캐시">1차 캐시</h2>
<p>1차 캐시는 영속성 컨텍스트 내부에 있다. 엔티티 매니저로 조회하거나 변경하는 모든 엔티티는 1차 캐시에 저장된다. 트랜잭션을 커밋하거나 플러시를 호출하면 1차 캐시에 있는 엔티티의 변경 내역을 데이터베이스에 동기화 한다.
JPA를 스프링 프레임워크 같은 컨테이너 위에서 실행하면 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션을 종료할 때 영속성 컨텍스트도 종료한다.
1차 캐시는 끄고 켤 수 있는 옵션이 아니고 영속성 컨텍스트 자체가 사실상 1차 캐시다.</p>
<h3 id="1차-캐시의-특징">1차 캐시의 특징</h3>
<ul>
<li>1차 캐시는 같은 엔티티가 있으면 해당 엔티티를 그대로 반환한다. 따라서 1차 캐시는 객체 동일성(a==b)을 보장한다.</li>
<li>1차 캐시는 기본적으로 영속성 컨텍스트 범위의 캐시다. (컨테이너 환경에서는 트랜잭션 범위의 캐시, OSIV를 적용하면 요청 범위의 캐시)</li>
</ul>
<h2 id="2차-캐시-1">2차 캐시</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/bcd4fba2-2708-4bfb-9eb0-c0259f03afaf/image.png" alt=""></p>
<p>2차 캐시는 애플리케이션 범위의 캐시다. 따라서 애플리케이션을 종료할 때까지 캐시가 유지되며 분산 캐시나 클러스터링 환경의 캐시는 애플리케이션보다 더 오래 유지될 수도 있다.
2차 캐시를 사용하면 엔티티 매니저를 통해 데이터를 조회할 때 우선 2차 캐시에서 찾고 없으면 데이터베이스에서 찾는다.</p>
<p>2차 캐시는 동시성을 극대화하려고 캐시한 객체를 직접 반환하지 않고 <strong>복사본을 만들어서 반환</strong>한다.
만약 캐시한 객체를 그대로 반환하면 여러 곳에서 같은 객체를 동시에 수정하는 문제가 발생할 수 있다. 이 문제를 해결하려면 객체에 락을 걸어야 하는데 이렇게 하면 동시성이 떨어질 수 있다.
락에 비하면 객체를 복사하는 비용은 아주 저렴하다. 따라서 2차 캐시는 원본 대신에 복사본을 반환한다.</p>
<h3 id="2차-캐시의-특징">2차 캐시의 특징</h3>
<ul>
<li>2차 캐시는 영속성 유닛 범위의 캐시다.</li>
<li>2차 캐시는 조회한 객체를 그대로 반환하는 것이 아니라 복사본을 만들어서 반환한다.</li>
<li>2차 캐시는 데이터베이스 기본 키를 기준으로 캐시하지만 영속성 컨텍스트가 다르면 객체 동일성(a==b)을 보장하지 않는다.</li>
</ul>
<h2 id="jpa-2차-캐시-기능">JPA 2차 캐시 기능</h2>
<p>JPA 구현체 대부분은 캐시 기능을 각자 지원했는데 JPA는 2.0에 와서야 캐시 표준을 정의했다. JPA 캐시 표준은 여러 구현체가 공통으로 사용하는 부분만 표준화 해서 세밀한 설정을 하려면 구현체에 의존적인 기능을 사용해야 한다.</p>
<h3 id="캐시-모드-설정">캐시 모드 설정</h3>
<p>2차 캐시를 사용하려면 엔티티에 javax.persistence.Cacheable 어노테이션을 사용하면 된다.
@Cacheable은 @Cacheable(true), @Cacheable(false)를 설정 가능 기본값은 true이다.</p>
<pre><code class="language-java">@Cacheable
@Entity
public class Member {
        @Id @GeneratedValue
        private Long id;
        ...
}</code></pre>
<pre><code class="language-xml">//persistence.xml에 shard-cache-mode를 설정해서 애플리케이션 전체에(정확히는 영속성 유닛 단위) 
//캐시를 어떻게 적용할지 옵션을 설정해야 한다.
&lt;persistence-unit name=&quot;test&quot;&gt;
        &lt;shared-cache-mode&gt;ENABLE_SELECTIVE&lt;/shared-cahce-mode&gt;
&lt;/persistence-unit&gt;

//캐시 모드 스프링 프레임워크 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>
<p>캐시 모드는 javax.persistence.SharedCachedMode에 정의되어 있고 보통 ENABLE_SELECTIVE를 사용한다.
<img src="https://velog.velcdn.com/images/now_here/post/6b4f3371-60f1-4dfb-a20e-ebb933937abe/image.png" alt=""></p>
<h3 id="캐시-조회-저장-방식-설정">캐시 조회, 저장 방식 설정</h3>
<p>캐시를 무시하고 데이터베이스를 직접 조회하거나 캐시를 갱신하려면 캐시 조회 모드와 캐시 보관 모드를 사용하면 된다.</p>
<pre><code class="language-java">em.setProperty(&quot;javax.persistence.cache.retrieveMode&quot;, CacheRetrieveMode.BYPASS);</code></pre>
<p>캐시 조회 모드나 보관 모드에 따라 프로퍼티와 옵션이 다르다.</p>
<h4 id="프로퍼티">프로퍼티</h4>
<ul>
<li>javax.persistence.cache.retrieveMode: 캐시 조회 모드 프로퍼티 이름</li>
<li>javax.persistence.cache.storeMode: 캐시 보관 모드 프로퍼티 이름</li>
</ul>
<h4 id="옵션">옵션</h4>
<ul>
<li>javax.persistence.CacheRetrieveMode: 캐시 조회 모드 설정 옵션</li>
<li>javax.persistence.CacheStoreMode: 캐시 보관 모드 설정 옵션</li>
</ul>
<h4 id="캐시-조회-모드-예제">캐시 조회 모드 예제</h4>
<pre><code class="language-java">//캐시 조회 모드
public eunm CacheRetrieveMode {
    USE,    // (기본값) 캐시에서 조회
    BYPASS    // 캐시를 무시하고 DB에 직접 접근
}    

//캐시 보관 모드
public enum CacheStoreMode {
    USE,    //(기본값) 조회한 데이터 캐시에 저장. 조회한 데이터가 이미 캐시에 있어도 최신화 x, 
            //트랜잭션을 커밋하면 등록 수정한 엔티티도 캐시에 저장
    BYPASS,    // 캐시에 저장하지 않는다
    REFRESH    // USE 전략에 추가로 DB에서 조회한 엔티티를 최신 상태로 다시 캐시한다.
}    </code></pre>
<p>캐시 모드는 EntityManager.setProperty()로 매니저 단위로 설정하거나 더 세밀하게 EntityManager.find(), EntityManager.refresh()에 설정할 수 있다.
Query.setHint()에도 사용할 수 있다.</p>
<pre><code class="language-java">//엔티티 매니저 범위
em.setProperty(&quot;javax.persistence.cache.retrieveMode&quot;, CacheRetrieveMode.BYPASS);
em.setProperty(&quot;javax.persistence.cache.storeMode&quot;, CacheStoreMode.BYPASS);

// find()
Map&lt;String, Object&gt; param = new HashMap&lt;String, Object&gt;();
param.put(&quot;javax.persistence.cache.retriveMode&quot;, CacheRetrieveMode.BYPASS);
param.put(&quot;javax.persistence.cache.storeMode&quot;, CacheStoreMode.BYPASS);

em.find(TestEntity.class, id, param);

// JPQL
em.createQuery(&quot;select e from TestEntity e where e.id = :id&quot;, TestEntity.class)
        .setParameter(&quot;id&quot;, id)
        .setHint(&quot;javax.persistence.cache.retrieveMode&quot;, CacheRetrieveMode.BYPASS)
        .setHint(&quot;javax.persistence.cache.storeMode&quot;, CacheStoreMode.BYPASS)
        .getSingleResult();</code></pre>
<h3 id="jpa-캐시-관리-api">JPA 캐시 관리 API</h3>
<p>JPA는 캐시를 관리하기 위한 javax.persistence.Cache 인터페이스를 제공한다. 이것은 EntityManagerFactory에서 구할 수 있다.</p>
<pre><code class="language-java">// Cache 관리 객체 조회
Cache cache = emf.getCache(); //EntityManageFactory
boolean contains = 
            cache.contains(TestEntity.class, testEntity.getId());
System.out.println(&quot;contains = &quot;, + contains);</code></pre>
<p>아래는 Cache 인터페이스의 설명이다.</p>
<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 void evictAll();

        //JPA Cache 구현체 조회
        public &lt;T&gt; T unwrap(Class&lt;T&gt; cls);
}</code></pre>
<p>하이버네이트와 EHCACHE를 사용해서 실제 2차 캐시를 적용해보자.</p>
<h2 id="하이버네이트와-ehcache-적용">하이버네이트와 EHCACHE 적용</h2>
<h3 id="하이버네이트가-지원하는-3가지-캐시">하이버네이트가 지원하는 3가지 캐시</h3>
<ol>
<li>엔티티 캐시 : 엔티티 단위로 캐시한다. 식별자로 엔티티를 조회하거나 컬렉션이 아닌 연관된 엔티티를 로딩할 때 사용한다.</li>
<li>컬렉션 캐시 : 엔티티와 연관된 컬렉션을 캐시한다. 컬렉션이 엔티티를 담고 있으면 식별자 값만 캐시한다(하이버네이트 기능).</li>
<li>쿼리 캐시 : 쿼리와 파라미터 정보를 키로 사용해서 캐시한다. 결과가 엔티티면 식별자 값만 캐시한다.(하이버네이트 기능).<blockquote>
<p>참고 : JPA표준에는 엔티티 캐시만 정의되어 있다.</p>
</blockquote>
</li>
</ol>
<h3 id="환경설정">환경설정</h3>
<p>하이버네이트에서 EHCAHE를 사용하려면 hibernate-ehcache 라이브러리를 pom.xml에 추가해야 한다.
해당 라이브러리를 추가하면 net.sf.ehcache-core 라이브러리도 추가된다.</p>
<p>EACACHE는 ehcache.xml을 설정 파일로 사용해서 캐시를 얼마만큼 보관할지, 얼마 동안 보관할지와 같은 캐시 정책 설정 파일이다. 이 파일은 src/main/resources에 두자.</p>
<pre><code class="language-xml">&lt;ehcahce&gt;
        &lt;defaultCache 
                maxElementsInMemory=&quot;10000&quot;
                eternal=&quot;false&quot;
                timeToldleSeconds=&quot;1200&quot;
                timeToLiveSeconds=&quot;1200&quot;
                diskExpiryThreadIntervalSeconds=&quot;1200&quot;
                memoryStoreEvictionPolicy=&quot;LRU&quot;
        /&gt;
&lt;/ehcahce&gt;</code></pre>
<p>하이버네이트에서 캐시 사용정보를 설정해야 한다. persistence.xml에 캐시 정보를 추가한다.</p>
<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;properties&gt;
                &lt;property name=&quot;hibernate.cache.use_second_level_cache&quot; value&quot;true&quot;/&gt;
                &lt;property name=&quot;hibernate.cache.use_query_cache&quot; value&quot;true&quot;/&gt;
                &lt;property name=&quot;hibernate.cache.region.factory_class&quot;
                                        value=&quot;org.hibernate.cache.ehcache.EhCacheRegionFactory&quot; /&gt;
                &lt;property name=&quot;hibernate.generate_statistics&quot; value&quot;true&quot; /&gt;
        &lt;/properties&gt;
        ...
&lt;/persistence-unit&gt;</code></pre>
<h4 id="설정한-속성-정보">설정한 속성 정보</h4>
<ul>
<li>hibernate.cache.use_second_level_cache : 2차 캐시를 활성화한다. 엔티티 캐시와 컬렉션 캐시를 사용할 수 있다.</li>
<li>hibernate.cach.use_query_cache : 쿼리 캐시를 활성화한다.</li>
<li>hibernate.cache.region.factory_class : 2차 캐시를 처리할 클래스를 지정한다.여기서는 EHCACHE를 사용하므로 org.hibernate.cache.ehcache.EhCacheRegionFactory를 적용</li>
<li>hibernate.generate_statistics : 이 속성을 true로 설정하면 하이버네이트가 여러 통계정보를 출력해주는데 캐시 적용 여부를 확인할 수 있다. (성능에 영향을 주므로 개발 환경에서만 적용하는 것이 좋음)</li>
</ul>
<h2 id="엔티티-캐시와-컬렉션-캐시">엔티티 캐시와 컬렉션 캐시</h2>
<pre><code class="language-java">@Cacheable // 1
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // 2
@Entity
public class ParentMember {

        @Id @GeneratedValue
        private Long id;

        private String name;

        @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) // 3
        @OneToMany(mappedBy = &quot;parentMember&quot;, cascade = CascadeType.ALL)
        private List&lt;ChildMember&gt; childMembers = new ArrayList&lt;ChildMember&gt;();
        ...
}</code></pre>
<ol>
<li>javax.persistence.Cacheable : 엔티티를 캐시하려면 @Cacheable 어노테이션 적용</li>
<li>org.hibernate.annotations.Cache: 하이버네이트 전용 어노테이션. 캐시와 관련된 더 세밀한 설정을 할 때 사용. 3번처럼 컬렉션 캐시를 적용할 때도 사용한다.</li>
</ol>
<h2 id="cache">@Cache</h2>
<p>org.hibernate.annotations.Cache: 하이버네이트 전용 어노테이션으로 더 세밀한 설정 가능.</p>
<h4 id="하이버네이트-cache-속성">하이버네이트 @Cache 속성</h4>
<p><img src="https://velog.velcdn.com/images/now_here/post/4b7a156a-7798-4525-a000-424acaf58626/image.png" alt="">
캐시 동시성 전략을 설정할 수 있는 usage가 중요하다. CacheConcurrencyStrategy 속성을 보자.</p>
<h4 id="cacheconcurrencystrategy-속성">CacheConcurrencyStrategy 속성</h4>
<p><img src="https://velog.velcdn.com/images/now_here/post/7b8049a7-7a34-498f-ac20-adcc0ba3ffe8/image.png" alt=""></p>
<p>캐시 종류에 따른 동시성 전략 지원 여부는 하이버네이트 공식문서가 제공하는 표를 참고하자.</p>
<h2 id="캐시-영역">캐시 영역</h2>
<p>캐시를 적용한 코드는 캐시 영역(Cache Region)에 저장된다.</p>
<p><strong>엔티티 캐시 영역</strong>은 기본값으로 [패키지명 + 클래스명]을 사용한다.
ex) jpabook.jpashop.domain.cache.ParentMember</p>
<p><strong>컬렉션 캐시 영역</strong>은 캐시 영역 이름에 캐시한 컬렉션의 필드 명이 추가된다.
ex) jpabook.jpashop.domain.cache.ParentMember.childMembers</p>
<p>@Cache(region = &quot;customRegion&quot;, ..._) 처럼 속성을 사용해 캐시 영역을 집정 지정할 수도 있다.
캐시 영역을 위한 접두사를 설정하려면 persistence.xml 설정에 hibernate.cache.region_prefix 사용하면 된다. 예를 들어 core로 설정하면 core.jpabook.jpashop ... 으로 설정하면 된다.</p>
<h2 id="쿼리-캐시">쿼리 캐시</h2>
<p>쿼리 캐시는 <strong>쿼리와 파라미터 정보</strong>를 키로 사용해서 쿼리 결과를 캐시하는 방법이다.
쿼리 캐시를 적용하려면 영속성 유닛을 설정에 hibernate.cache.use_query_cache 옵션을 꼭 true로 설정해야 한다.
또한 쿼리 캐시를 적용하려는 쿼리마다 org.hibernate.cacheable을 true로 설정하는 힌트를 주면 된다.</p>
<pre><code class="language-java">// 쿼리 캐시 적용
em.createQuery(&quot;select i from Item i&quot;, Item.class)
  .setHint(&quot;org.hibernate.cacheable&quot;, true)
  .getResultList();

// NamedQuery에 쿼리 캐시 적용
@Entity
@NamedQuery(
      hints = @QueryHint(name=&quot;org.hibernate.cacheable&quot;, value=&quot;true&quot;), 
      name=&quot;Member.findByUsername&quot;, query=&quot;select m.address from Member m where m.name=:username&quot;
)
public class Member {...}</code></pre>
<h2 id="쿼리-캐시-영역">쿼리 캐시 영역</h2>
<p>hibernate.cache.user_query_cache 옵션을 통해 쿼리 캐시를 활성화하면 두 캐시 영역이 추가된다.</p>
<ul>
<li>org.hibernate.cache.internal.StandardQueryCache : 쿼리 캐시를 저장하는 영역. 이곳에는 쿼리, 쿼리 결과 집합, 쿼리를 실행한 시점의 타임스탬프를 보관한다.</li>
<li>org.hibernate.cache.spi.UpdateTimestampsCache : 쿼리 캐시가 유효한지 확인하기 위해 쿼리 대상 테이블의 가장 최근 변경(등록, 수정, 삭제) 시간을 저장하는 영역이다. 이곳에는 테이블 명과 해당 테이블의 최근 변경된 타임스탬프를 보관한다</li>
</ul>
<p>쿼리 캐시는 캐시한 데이터 집합을 최신 데이터로 유지하려고 쿼리 캐시를 실행하는 시간과 쿼리 캐시가 사용하는 테이블들이 가장 최근에 변경된 시간을 비교한다. 쿼리 캐시를 적용하고 난 후에 쿼리 캐시가 사용하는 테이블에 조금이라도 변경이 있으면 데이터베이스에서 데이터를 읽어와서 쿼리 결과를 다시 캐시한다. 이제부터 엔티티를 변경하면 org.hibernate.cache.spi.UpdateTimestampsCache 캐시 영역에 해당 엔티티가 매핑한 테이블 이름으로 타임스탬프를 갱신한다.</p>
<p>쿼리 캐시를 잘 활용하면 극적인 성능 향상이 있지만 빈번하게 변경이 있는 테이블에 사용하면 오히려 성능이 더 저하된다. 따라서 수정이 거의 일어나지 않는 테이블에 사용해야 효과를 볼 수 있다.</p>
<blockquote>
<p>참고 : org.hibernate.cache.spi.UpdateTimestampsCache 쿼리 캐시 영역은 만료되지 않도록 설정해야 한다. 해당 영역이 만료되면 모든 쿼리 캐시가 무효화된다. EHCACHE의 eternal=”true” 옵션을 사용하면 캐시에서 삭제되지 않는다.</p>
</blockquote>
<h2 id="쿼리-캐시와-컬렉션-캐시의-주의점">쿼리 캐시와 컬렉션 캐시의 주의점</h2>
<p>엔티티 캐시를 사용해서 엔티티를 캐시하면 엔티티 정보를 모두 캐시하지만 <strong>쿼리 캐시와 컬렉션 캐시는 결과 집합의 식별자 값만 캐시한다.</strong> 따라서 쿼리 캐시와 컬렉션 캐시를 조회(캐시 히트)하면 그 안에는 사실 식별자 값만 들어 있다. 그리고 이 식별자 값을 하나씩 엔티티 캐시에서 조회해서 실제 엔티티를 찾는다.
문제는 쿼리 캐시나 컬렉션 캐시만 사용하고 대상 엔티티에 엔티티 캐시를 적용하지 않으면 성능상 심각한 문제가 발생할 수 있다.</p>
<h4 id="예시">예시</h4>
<ol>
<li>select m from Member m 쿼리를 실행했는데 쿼리 캐시가 적용되어 있다. 결과 집합은 100건이다.</li>
<li>결과 집합에는 식별자만 있으므로 한 건씩 엔티티 캐시 영역에서 조회한다.</li>
<li>Member 엔티티는 엔티티 캐시를 사용하지 않으므로 한 건씩 데이터베이스에서 조회한다.</li>
<li>결국 100건의 SQL이 실행된다.</li>
</ol>
<p>쿼리 캐시나 컬렉션 캐시만 사용하고 엔티티 캐시를 사용하지 않으면 최악의 상황에 결과 집합 수만큼 SQL이 실행된다. <strong>따라서 쿼리 캐시나 컬렉션 캐시를 사용하면 결과 대상 엔티티에는 꼭 엔티티 캐시를 적용해야 한다.</strong></p>
<p>참조 : [자바 ORM 표준 JPA 프로그래밍]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[15장 고급 주제와 성능 최적화]]></title>
            <link>https://velog.io/@now_here/15%EC%9E%A5-%EA%B3%A0%EA%B8%89-%EC%A3%BC%EC%A0%9C%EC%99%80-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@now_here/15%EC%9E%A5-%EA%B3%A0%EA%B8%89-%EC%A3%BC%EC%A0%9C%EC%99%80-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Wed, 05 Feb 2025 11:34:33 GMT</pubDate>
            <description><![CDATA[<h1 id="예외-처리">예외 처리</h1>
<h2 id="jpa-표준-예외-정리">JPA 표준 예외 정리</h2>
<p>JPA 표준 예외는 모두 javax.persistence.PersistenceException의 자식 클래스다. 이 예외 클래스는 RuntimeException의 자식이므로 JPA 예외는 모두 언체크 예외다.</p>
<blockquote>
<p>여기서 잠깐!
언체크 예외가 뭔데??
언체크 예외(Unchecked Exception)는 컴파일러가 예외 처리를 강제하지 않는 예외입니다. 즉, 코드 작성 시 예외 처리를 위한 try-catch 블록을 반드시 작성하지 않아도 컴파일 오류가 발생하지 않습니다.</p>
</blockquote>
<p>JPA 표준 예외는 크게 심각한 예외와 그렇지 않은 예외 두 가지로 나뉜다.</p>
<ul>
<li>트랜잭션 롤백을 표시하는 예외 (심각한 예외)</li>
<li>트랜잭션 롤백을 표시하지 않는 예외</li>
</ul>
<p>트랜잭션 롤백을 표시하는 예외는 트랜잭션을 강제적으로 커밋해도 커밋되지 않고 <strong>javax.persistence.RollbackException</strong> 예외가 발생한다.
트랜잭션 롤백을 표시하지 않는 예외는 심각한 예외라가 아니라서 개발자가 트랜잭션을 커밋할지 롤백할지 판단하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/now_here/post/9bc9d5e3-b60c-467d-b7c8-4220c19ee45f/image.png" alt=""></p>
<p>persist()는 이미 영속성 컨텍스트에 존재하는 동일한 식별자를 가진 엔티티가 있으면 중복 등록 문제로 예외가 발생한다. </p>
<p><img src="https://velog.velcdn.com/images/now_here/post/63ec3ca2-007b-4c83-a711-07f8b1bf9bcb/image.png" alt=""></p>
<h2 id="스프링-프레임워크의-jpa-예외-변환">스프링 프레임워크의 JPA 예외 변환</h2>
<p>서비스 계층에서 데이터 접근 계층의 구현 기술에 직접 의존하면 좋은 설계라 할 수 없는 것 처럼 예외도 마찬가지이다.
서비스 계층에서 JPA 예외를 직접 사용하면 JPA예외에 의존하게 되므로 스프링 프레임워크는 데이터 접근 계층에 대한 예외를 추상화해서 개발자에게 제공한다.</p>
<p><img src="https://velog.velcdn.com/images/now_here/post/cae6dc95-05eb-43c1-802f-5c5417abd697/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/now_here/post/8fdc7966-9232-4b11-8a20-ca3befd24edb/image.png" alt=""></p>
<h2 id="스프링-프레임워크에-jpa-예외-변환기-적용">스프링 프레임워크에 JPA 예외 변환기 적용</h2>
<p>JPA 예외를 스프링 프레임워크에서 제공하는 추상화된 예외로 변경하려면 <strong>PersistenceExceptionTransactionPostProcessor</strong>를 스프링 빈으로 등록하면 된다. 이것은 @Repository를 사용한 곳에 <strong>예외 변환 AOP</strong>를 적용해서 JPA 예외를 스프링 프레임워크가 추상화환 예외로 변환해준다.</p>
<h3 id="xml-기반-설정">xml 기반 설정</h3>
<pre><code class="language-xml">&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot; 
       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; 
       xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd&quot;&gt;

    &lt;bean class=&quot;org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor&quot;/&gt;
&lt;/beans&gt;
</code></pre>
<h3 id="자바-기반-설정javaconfig">자바 기반 설정(JavaConfig)</h3>
<pre><code class="language-java">import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;

@Configuration
public class AppConfig {

    @Bean
    public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
        return new PersistenceExceptionTranslationPostProcessor();
    }
}
</code></pre>
<h3 id="예제">예제</h3>
<pre><code class="language-java">@Repository
public class NoResultExceptionTestService {

    @PersistenceContext EntityManager em;

    public member findMember() throws javax.persistence.NoResultException {
        //조회된 데이터 없음
        return em.createQuery(&quot;select m from Member m&quot;, Member.class)
                .getSingleResult();
}              </code></pre>
<p>getSingleResult() 메소드를 사용했는데 조회된 결과가 없다면 javax.persistence.NoResultException이 발생되는데 . 이때 PersistenceExceptionTranslationPostProcessor에서 등록한 AOP가 동작하며 org.springframework.dao.EmptyResultDataAccessException 예외로 변환해서 반환한다.
그러나 변환된 예외가 아닌 그대로의 예외를 사용하고 싶다면 위처럼 throws를 사용해서 JPA 예외의 부모 클래스를 명시적으로 적으면 된다. java.lang.Exception를 선언하면 모든 예외의 부모이므로 예외를 변환하지 않는다.</p>
<h2 id="트랜잭션-롤백시-주의사항">트랜잭션 롤백시 주의사항</h2>
<p>트랜잭션 롤백시 데이터베이스의 반영사항만 롤백한 것이지 수정한 자바 객체까지 원상태로 복구한 것이 아님을 주의해야 한다. 트랜잭션이 롤백된 영속성 컨텍스트를 그대로 사용하는 것은 위험하므로 새로운 영속성 컨텍스트를 생성해서 사용하거나 EntityManager.clear()를 호출해서 영속성 컨텍스트를 초기화해서 사용해야 한다.</p>
<p>스프링 프레임워크에서는 영속성 컨텍스트 범위에 따라서 해결방법이 달라진다.
기본 전략인 <strong>트랜잭션당 영속성 컨텍스트 전략</strong>은 문제가 발생하면 트랜잭션 AOP 종료 시점에 트랜잭션을 롤백하면서 영속성 컨텍스트도 함께 종료하므로 문제되지 않는다.</p>
<p><strong>OSIV</strong> 처럼 영속성 컨텍스트의 범위를 트랜잭션 범위보다 넓게 사용해서 여러 트랜잭션이 하나의 영속성 컨텍스트를 사용할 때 발생한다. 이때는 트랜잭션을 롤백해서 영속성 컨텍스트에 이상이 발생해도 다른 트랜잭션에서 해당 영속성 컨텍스트를 그대로 사용하는 문제가 있다.</p>
<p>스프링 프레임워크는 영속성 컨텍스트의 범위를 트랜잭션의 범위보다 넓게 설정하면 트랜잭션 롤백시 영속성 컨텍스트를 초기화(EntityManager.clear())해서 잘못된 영속성 컨텍스트를 사용하는 문제를 예방한다.</p>
<blockquote>
<p>트랜잭션 2, 3에서 엔티티 ㄱ, 엔티티 ㄴ을 수정했는데 아직 커밋되지 않고 쓰기 지연 저장소에만 저장되어 있는 상태라고 하자. 그러면 영속성 컨텍스트가 초기화되면서 해당 내용들은 db에 반영되지 않으니까 문제되잖아? 그러면 트랜잭션 1이 롤백되는 시점에 트랜잭션 2,3의 내용들은 다시 처음부터 시작되나? 자동으로?</p>
</blockquote>
<hr>
<h4 id="1-트랜잭션과-영속성-컨텍스트의-관계"><strong>1. 트랜잭션과 영속성 컨텍스트의 관계</strong></h4>
<ul>
<li>트랜잭션 2, 3이 같은 영속성 컨텍스트를 사용하고 있는 상태에서 쓰기 지연 저장소(Flush Queue)에 엔티티 수정 내용이 반영된 상황입니다.</li>
<li>만약 트랜잭션 1의 롤백으로 <strong>영속성 컨텍스트가 초기화되면</strong> 트랜잭션 2, 3의 쓰기 지연 데이터도 함께 사라집니다.</li>
</ul>
<h4 id="2-영속성-컨텍스트-초기화가-트랜잭션에-미치는-영향"><strong>2. 영속성 컨텍스트 초기화가 트랜잭션에 미치는 영향</strong></h4>
<ul>
<li>트랜잭션 2, 3은 여전히 활성 상태이지만, 영속성 컨텍스트가 초기화되면서 수정된 엔티티 상태나 쓰기 지연 저장소가 모두 날아갑니다.</li>
<li>이미 수정한 엔티티가 <strong>Detached 상태</strong>가 되기 때문에 이후 flush 시점에서 아무런 데이터가 DB에 반영되지 않게 됩니다.</li>
</ul>
<h4 id="3-트랜잭션-2-3이-자동으로-다시-시작되지-않음"><strong>3. 트랜잭션 2, 3이 자동으로 다시 시작되지 않음</strong></h4>
<p>영속성 컨텍스트 초기화는 단순히 엔티티 상태를 비우는 것이지 <strong>트랜잭션 자체를 초기화하거나 재시작하지 않습니다.</strong> 따라서 트랜잭션 2, 3에서 다음과 같은 문제가 발생할 수 있습니다:</p>
<ul>
<li>데이터 손실: 이미 변경된 엔티티 정보가 flush되지 않고 유실됨.</li>
<li>Lazy Loading 오류: 초기화된 영속성 컨텍스트로 인해 이전에 조회했던 엔티티가 더 이상 사용할 수 없게 됨.</li>
<li>Stale Object 오류: Detached된 엔티티가 여전히 애플리케이션 코드에서 사용되면 비정상적인 오류 발생.</li>
</ul>
<hr>
<h4 id="4-해결-방법"><strong>4. 해결 방법</strong></h4>
<p>OSIV 사용 시 이러한 문제를 피하기 위해 다음과 같은 방법을 고려할 수 있습니다:</p>
<h4 id="41-트랜잭션-경계를-명확히-설정">4.1 트랜잭션 경계를 명확히 설정</h4>
<p>트랜잭션과 영속성 컨텍스트를 가능한 한 짧고 독립적으로 유지하는 것이 가장 안전합니다. </p>
<h4 id="42-트랜잭션-롤백-시-새로운-영속성-컨텍스트-강제-생성">4.2 트랜잭션 롤백 시 새로운 영속성 컨텍스트 강제 생성</h4>
<p>스프링에서는 <code>@Transactional(propagation = Propagation.REQUIRES_NEW)</code>를 통해 트랜잭션 범위를 명확히 분리하고 새로운 영속성 컨텍스트를 생성할 수 있습니다.</p>
<h4 id="43-트랜잭션-관리-도구-활용">4.3 트랜잭션 관리 도구 활용</h4>
<p>Hibernate의 <code>Session</code>을 명시적으로 관리하거나, 트랜잭션 롤백 후 필요한 로직을 다시 실행하도록 코드를 설계합니다.</p>
<h3 id="결론"><strong>결론</strong></h3>
<p>영속성 컨텍스트 초기화는 트랜잭션 상태와 별개로 발생하므로 트랜잭션 2, 3의 데이터가 자동으로 복구되거나 재시작되지는 않습니다. 따라서 OSIV 사용 시 트랜잭션 범위 관리를 더욱 신중하게 고려해야 합니다.</p>
<hr>
<h1 id="엔티티-비교">엔티티 비교</h1>
<p>영속성 컨텍스트에는 1차 캐시가 있다. 영속성컨텍스트를 통해 데이터를 조회하거나 저장하면 1차 캐시에 엔티티가 저장된다. 이렇게 저장된 1차 캐시를 통해 변경감지도 하고 1차 캐시에 있는 엔티티를 반환하며 db와의 통신을 줄일 수도 있다. 이런 1차 캐시는 영속성 컨텍스트와 생명주기를 같이 한다.</p>
<p>1차 캐시의 장점인 <strong>애플리케이션 수준의 반복 가능한 읽기</strong>를 알아보자.
같은 영속성 컨텍스트에서 같은 식별자를 가지고 엔티티를 조회하면 동등성(equals)수준이 아니라 항상 주소값이 같은 인스턴스를 반환하는데 이를 애플리케이션 수준의 반복 가능한 읽기라고 한다.</p>
<h3 id="추가-설명">추가 설명</h3>
<blockquote>
<p>일반적으로 <strong>반복 가능한 읽기(Repeatable Read)</strong>는 데이터베이스 트랜잭션 격리 수준(Isolation Level) 중 하나로, 동일한 트랜잭션 내에서 같은 쿼리를 여러 번 실행하더라도 결과가 동일하도록 보장합니다. 이를 애플리케이션 수준에서 구현한다는 의미는 데이터베이스에 의존하지 않고 애플리케이션 레벨에서 데이터 일관성을 유지하는 방법을 의미합니다.</p>
</blockquote>
<h2 id="영속성-컨텍스트가-같을-때-엔티티-비교">영속성 컨텍스트가 같을 때 엔티티 비교</h2>
<p>예제를 통해 애플리케이션 수준의 반복 가능한 읽기에 대해 알아보자.</p>
<p><img src="https://velog.velcdn.com/images/now_here/post/2b3f2cf4-5904-4d35-a3d7-7c4faeab631a/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 {

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

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

        //Then
        Member findMember = memberRepository.findOne(saveId);
        assertTrue(member == findMember);   //참조값 비교
    }

    @Transactional
    public class MemberService() {

        @Autowired MemberRepository memberRepository;

        public Long join(Member member) {
            ...
            memberRepository.save(member);
            return member.getId();
        }
    }

    @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);
        }
    }
}</code></pre>
<p>MemberServiceTest에 @Transactional 어노테이션이 붙어 있어 이 안에서 실행하는 회원가입()메서드는 같은 트랜잭션 안에서 실행되고 종료된다. 그러므로 회원가입() 메서드에서 사용된 코드는 항상 같은 영속성 컨텍스트에 접근하게 된다. 아래 그림을 보자.</p>
<p><img src="https://velog.velcdn.com/images/now_here/post/1cdb2f22-6391-4889-bf1f-fd0a63678bd4/image.png" alt=""></p>
<p>코드를 보면 회원가입()메서드에서 회원을 생성하고 memberRepository에서 em.persist(member)로 회원을 영속성 컨텍스트에 저장한다. 그리고 저장된 회원을 찾아서 저장한 회원과 비교한다.
같은 트랜잭션 범위에 있으므로 같은 영속성 컨텍스트를 사용하고 이는 참이 된다.</p>
<p>따라서 영속성 컨텍스트가 같으면 엔티티를 비교할 때 다음 3가지 조건을 모두 만족한다.</p>
<ul>
<li>동일성(identical) : == 비교</li>
<li>동등성(equinalent) : equals() 비교</li>
<li>데이터베이스 동등성 : @Id인 데이터베이스 식별자가 같다.</li>
</ul>
<blockquote>
<p>참고 : 테스트 클래스에도 @Transactional이 있고 서비스에도 같은 어노테이션이 있다. 이때 기본 전략은 이미 진행된 트랜잭션이 있으면 그 트랜잭션을 이어서 사용하고 없으면 새로 시작한다.</p>
</blockquote>
<blockquote>
<p>참고2 : 테스트 클래스에 @Transactional을 적용하면 테스트가 끝날 때 트랜잭션을 커밋하지 않고 트랜잭션을 강제로 롤백한다. 그래서 데이터베이스에 영향을 안 주고 반복해서 테스트를 진행 할 수 있다. 
다만 롤백시 영속성 컨텍스트를 플러시하지 않기 때문에 플러시 시점에 어떤 SQL이 실행되는지 콘솔 로그가 남지 않는다. 어떤 SQL이 실행되는지 보고 싶으면 테스트 마지막에 em.flush()를 강제로 후출하면 된다.</p>
</blockquote>
<h2 id="영속성-컨텍스트가-다를-때-엔티티-비교">영속성 컨텍스트가 다를 때 엔티티 비교</h2>
<p>테스트 클래스에 @Trasactional 을 없애서 트랜잭션 범위와 영속성 컨텍스트를 다르게 설정해 보자.
<img src="https://velog.velcdn.com/images/now_here/post/e4d57500-9a7d-4444-870c-eb130992e3da/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 {

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

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

        //Then
        Member findMember = memberRepository.findOne(saveId);
        //findMember는 준영속 상태

        //둘은 다른 주소값을 가진 인스턴스로 false
        assertTrue(member == findMember);   //참조값 비교
    }

    @Transactional  // 서비스에서 트랜잭션 시작
    public class MemberService() {

        @Autowired MemberRepository memberRepository;

        public Long join(Member member) {
            ...
            memberRepository.save(member);
            return member.getId();
        }
    }

    @Repository
    @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>
<p>이 테스트는 assertTrue(member == findMember); 에서 false로 실패한다. 그림을 보며 자세히 살펴보자.
<img src="https://velog.velcdn.com/images/now_here/post/22a14dbb-7599-4ada-a8d3-1459cdb35402/image.png" alt=""></p>
<p>1.먼저 회원가입() 메서드에서 회원가입을 진행하며 서비스계층에서 트랜잭션이 시작된다.
2.영속성 컨텍스트1이 생성되며 여기서 memberRepository를 이용해서 엔티티를 해당 컨텍스트에 영속화한다.
3.서비스 계층이 끝나면서 트랜잭션이 커밋되면서 영속성 컨텍스트가 flush() 된다. 이 후 영속성 컨텍스트1과 트랜잭션이 종료된다. member는 준영속 상태가 된다.
4. 이 후 코드에서 memberRepository에서 저장한 엔티티를 조회하면 리포지토리 계층에서 새로운 트랜잭션이 시작되면서 새로운 영속성 컨텍스트2가 시작된다.
5. 영속성 컨텍스트2에는 찾는 회원이 존재 하지 않는다.
6. db를 조회하여 회원을 찾아온다.
7. db에서 조회한 엔티티를 영속성 컨텍스트2에 가져오고 저장한다.
8. memberRepository.findOne() 메소드가 끝나면서 트랜잭션이 종료되고 영속성 컨텍스트2도 종료된다.</p>
<p>이 처럼 member와 findMember는 다른 영송성 컨텍스트에서 관리되었기 때문에 다른 인스턴스이다.
member == findMember; (실패)
하지만 둘은 같은 데이터베이스 로우를 가르키고 있어 사실상 같은 엔티티로 보아야 한다.</p>
<p>이처럼 영속성 컨텍스트가 다르면 동일성 비교에 실패한다.</p>
<h3 id="영속성-컨텍스트가-다를-때-엔티티-비교-1">영속성 컨텍스트가 다를 때 엔티티 비교</h3>
<ul>
<li>동일성(identical) : == 비교 (실패)</li>
<li>동등성(equinalent) : equals() 비교 만족하지만 equals()를 구현해야 한다. 보통 비즈니스 키로 구현한다.</li>
<li>데이터베이스 동등성 : @Id인 데이터베이스 식별자가 같다.</li>
</ul>
<p>앞서 본 것 처럼 영속성 컨텍스트가 같으면 엔티티 비교는 동일성 비교만으로 충분하다. 따라서 같은 영속성 컨텍스트를 사용하는 OSIV에서는 동일성 비교가 성공하지만 영속성 컨텍스트가 다를 때는 다른 방법을 사용해야 한다.</p>
<h3 id="데이터베이스-동등성-비교">데이터베이스 동등성 비교</h3>
<pre><code class="language-java">member.getId().equals(findMember.getId())    //데이터베이스 식별자 비교</code></pre>
<p>이렇게 식별자로 비교하는 방법이 있지만 엔티티를 먼저 영속화해야 한다는 문제점이 있다. 식별자 값을 먼저 부여한다는 방법도 있지만 항상 식별자를 먼저 부여하는 것을 보장하기는 쉽지 않다.</p>
<h3 id="eqauls-비교">eqauls() 비교</h3>
<p>앞서 설명한 것처럼 <strong>비즈니스 키를 활용한 동등성 비교</strong>를 권장한다.
비즈니스 키가 되는 필드는 보통 중복되지 않고 거의 변하지 않는 데이터베이스 기본 키 후보들이 좋은 대상이다. 객체 상태에서만 비교하므로 유일성만 보장되면 데이터베이스 기본 키 같이 너무 딱딱하게 정하지 않아도 된다.</p>
<hr>
<h1 id="프록시-심화-주제">프록시 심화 주제</h1>
<h2 id="영속성-컨텍스트와-프록시">영속성 컨텍스트와 프록시</h2>
<h4 id="프록시로-조회한-엔티티의-동일성도-보장할까">프록시로 조회한 엔티티의 동일성도 보장할까?</h4>
<p>em.getRefence()로 프록시를 조회했다고 하자. 그 다음에 em.find()로 같은 식별자인 엔티티를 조회하면 어떻게 될까? 하나는 프록시고 하나는 엔티티라고 생각들겠지만 그렇지 않다.
영속성 컨텍스트는 프록시로 조회한 엔티티에 대해서 같은 엔티티를 찾는 요청이 오면 원본 엔티티가 아닌 <strong>처음 조회된 프록시를 반환한다.</strong> 따라서 프록시로 조회해도 영속성 컨텍스트는 영속 엔티티의 동일성을 보장한다. </p>
<h4 id="원본-엔티티를-먼저-조회하고-나중에-프록시를-조회하면-어떻게-될까">원본 엔티티를 먼저 조회하고 나중에 프록시를 조회하면 어떻게 될까?</h4>
<p>원본 엔티티를 먼저 조회하면 영속성 컨텍스트에서 원본 엔티티를 이미 데이터베이스에서 조회하여 가지고 있다. 그러므로 프록시를 조회해도 원본을 반환한다. 이렇게 영속성 컨텍스트에서는 자신이 관리하는 영속 엔티티의 동일성을 보장한다.</p>
<h2 id="프록시-타입-비교">프록시 타입 비교</h2>
<p>프록시는 원본 엔티티를 상속 받아서 만들었으므로 == 비교를 하면 안 된다. 대신에 instanceof를 통해 원본 엔티티의 자식 타입인지 확인한다.
Member엔티티의 프록시 proxyMember를 조회했다고 하자. 그러면 다음은 true이다!
(proxyMember instanceof Member) -&gt; true</p>
<h2 id="프록시-동등성-비교">프록시 동등성 비교</h2>
<p>엔티티의 동등성을 비교하려면 비즈니스 키를 사용해서 equals() 메소드를 오버라이딩하면 된다. 그러나 IDE나 외부 라이브러리를 사용해서 구현한 equals() 메서드로 엔티티를 비교할 때, 비교 대상이 원본 엔티티면 문제가 없지만 프록시면 문제가 발생할 수 있다. equals() 메서드를 오버라이딩할 때 주의점을 알아보자.
(name을 비즈니스 키로 사용하는 회원 엔티티의 equals()를 오버라이딩한다고 가정)</p>
<h3 id="주의점">주의점</h3>
<p> <strong>1. 프록시의 타입 비교는 == 대신에 instanceof를 사용해야 한다.</strong>
 프록시는 앞서 말한 것처럼 == 동일성 비교를 하면 안 되고 instanceof를 사용해야 한다. 그러므로 eqauls()를 오버라이딩 할 때 다음과 같이 한다.</p>
<pre><code class="language-java"> @Override
 public boolean equals(Object obj) {
     ...
    if (!(obj instanceof Member)) return false;
    if (name != null ? !name.equals(member.name) : member.name != null) return false
    ...
}</code></pre>
<p> <strong>2. 프록시의 멤버변수에 직접접근 하면 안 되고 getter를 사용해야 한다.</strong>
 비즈니스 키를 이용해서 동등성을 비교한다고 했다. 그러나 member.name과 같이 멤버변수에 직접 접근하면 안 된다. 프록시의 경우 실제 데이터를 가지고 있지 않아 member.name의 결과가 항상 null을 반환하는 문제가 생기기 때문이다. 그러나 member.getName()처럼 getter를 통해 접근하면 Hibernate가 프록시 초기화를 수행해 원본 엔티티에 접근할 수 있어 동등성 비교를 할 수 있게 된다. 따라서 접근자 메서드를 사용해서 동등성 비교를 진행하자.</p>
<h3 id="프록시-동등성-비교-예제">프록시 동등성 비교 예제</h3>
<pre><code class="language-java"> @Override
 public boolean equals(Object obj) {
     if (this == obj) return true;
    if (!(obj instanceof Member)) return false;

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

    return true;
}</code></pre>
<h2 id="상속관계와-프록시">상속관계와 프록시</h2>
<p> 프록시를 부모 타입으로 조회하면 문제가 발생한다.
 <strong>프록시를 부모 타입으로 조회하면 부모의 타입을 기반으로 프록시가 생성</strong>되기 때문에 다음과 같은 문제가 있다.</p>
<ul>
<li>instanceof 연산을 사용할 수 없다.</li>
<li>하위 타입으로 다운캐스팅을 할 수 없다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/now_here/post/34834214-0a70-433a-a089-36f3a7621c1a/image.png" alt="">
예제를 봐보자. Item을 상속하는 Book을 조회하고자 한다. 이때 Item을 프록시 객체로 받고 Book을 조회할 수 있을까?</p>
<pre><code class="language-java">@Test
public void 부모타입으로_프록시조회() {
        //테스트 데이터 준비
        Book saveBook = new Book();
        saveBook.setName(&quot;jpabook&quot;);
        saveBook.setAuthor(&quot;kim&quot;);
        em.persist(saveBook);

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

        //테스트 시작
        Item proxyItem = em.getReference(Item.class, saveBook.getId());
        System.out.println(&quot;proxyItem = &quot; + proxyItem.getClass());
        // 출력결과
        //proxyItem = class jpabook.proxy.advanced.item.Item_$$_jvstXXX

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

        //결과 검증
        Assert.assertFalse(proxyItem.getClass() == Book.class);    //false
        Assert.assertFalse(proxyItem instanceof Book);    //false
        Assert.assertTrue(proxyItem instanceof Item);    //true
}</code></pre>
<blockquote>
<p>  // 출력결과
        proxyItem = class jpabook.proxy.advanced.item.Item_$$_jvstXXX</p>
</blockquote>
<p>em.getReference() 메소드를 사용해서 Item 엔티티를 프록시로 조회했다. 이때 실제 조회한 엔티티는 Book엔티티이므로 Book 엔티티를 기준으로 원본 엔티티 인스턴스가 생성된다. 그러나 Item 엔티티를 대상으로 프록시를 조회했으므로 proxyItem은 Item을 타입을 기반으로 만들어진다.
이런 문제를 해결하는 방법을 알아보자.</p>
<h2 id="jpql로-대상-직접-조회">JPQL로 대상 직접 조회</h2>
<p>처음부터 자식 타입을 직접 조회해서 연산하기. 다만 이 방법은 다형성을 활용할 수 없다.</p>
<pre><code class="language-java">Book jpqlBook = em.createQuery
    (&quot;select b from Book b where b.id=:bookId&quot;, Book.class)
    .getSingleResult();</code></pre>
<h2 id="프록시-벗기기">프록시 벗기기</h2>
<h3 id="하이버네이트가-제공하는-기능-사용하기">하이버네이트가 제공하는 기능 사용하기</h3>
<pre><code class="language-java">//하이버네이트가 제공하는 프록시에서 원본 엔티티를 찾는 기능을 사용하는 메서드
public static &lt;T&gt; T unProxy(Object entity) {
    if (entity instanceof HibernateProxy) {
        entity = ((HibernateProxy) entity)
                    .getHibernateLazyInitializer()
                    .getImplememtation();
     }
     return (T) entity;</code></pre>
<p>영속성 컨텍스트는 한 번 프록시로 노출한 엔티티는 계속 노출하여 영속 엔티티의 동일성을 보장한다. 그래서 클라이언트는 조회한 엔티티가 프록시인지 아닌지 구분하지 않고 사용할 수 있다. 그러나 이 방법은 프록시에서 원본 엔티티를 직접 꺼내기 때문에 프록시와 원본 엔티티의 동이성 비교가 실패한다는 문제가 있다.</p>
<p>이 방법을 사용할 때는 원본 엔티티가 필요한 곳에서 잠깐 사용하고 다른 곳에서 사용되지 않도록 하는 것이 중요하다. 참고로 원본 엔티티의 값을 직접 변경해도 변경 감지 기능은 동작한다.</p>
<h2 id="기능을-위한-별도의-인터페이스-제공">기능을 위한 별도의 인터페이스 제공</h2>
<p>Item 클래스가 상속하는 특별한 인터페이스를 만드는 방법도 있다.</p>
<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;

        //Getter, Setter
        ...
}

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

        //Getter, Setter

        @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;

        //Getter, Setter

        @Override
        public String getTitle() {
                return &quot;[제목:&quot; + getName() + &quot; 감독:&quot; + director + &quot; 배우 :&quot; + actor + &quot;]&quot;;
        }        
}
</code></pre>
<p>TitleView라는 공통 인터페이스를 만들고 자식 클래스들은 인터페이스의 getTitle()을 각자 구현한다.
이러고 OrderItem에 printItem() 메소드를 구현한다.</p>
<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;

        ...

        public void printItem() {
                System.out.println(&quot;TITLE=&quot; + item.getTitle());
        }
}</code></pre>
<p>이러면 이제 Item 구현체에 맞는 getTitle()이 동작한다.
이 방법은 두 가지 장점을 제공한다.</p>
<ul>
<li>각각의 클래스가 자신에 맞는 기능을 구현하여 <strong>다형성 활용</strong>의 좋은 방법이다.</li>
<li>클라리언트는 대상 객체가 프록시인지 아닌지 고민하지 않아도 된다.</li>
</ul>
<p>이 방법을 사용할 때는 프록시의 특징 때문에 프록시의 대상이 되는 타입에 인터페이스를 적용해야 한다.
여기서는 Item이 지연로딩으로 프록시 타입을 반환하므로 Item에 공통 인터페이스를 설정했다.</p>
<h2 id="비지터-패턴-사용">비지터 패턴 사용</h2>
<p>비지터패턴으로 상속관계와 프록시 문제를 해결해보자.</p>
<p><img src="https://velog.velcdn.com/images/now_here/post/f9656860-df8d-4826-b1e3-f8862dbe8983/image.png" alt=""></p>
<p>비지터 패턴은 Visitor와 Visitor를 받아들이는 대상 클래스로 구성된다. 여기서 Item이 accept(visitor)를 사용해서 Visitor를 받아들이고 실제 로직은 Visitor가 처리한다.</p>
<h3 id="예제-코드">예제 코드</h3>
<pre><code class="language-java">public interface Visitor {

        void visit(Book book);
        void visit(Album album);
        void visit(Movie movie);
}

// 비지터 구현 - 대상 클래스의 내용을 출려하는 visitor
public class PrintVisitor implements Visitor {
        @Override
        public void visit(Book book) {
                //넘어오는 book은 Proxy가 아닌 원본 엔티티
                System.out.println(&quot;book.class = &quot; + book.getClass());
                System.out.println(&quot;[PrintVisitor] [제목: &quot; + book.getName() + 
                        &quot;저자 :&quot; + book.getAutor() + &quot;]&quot;);
        }

        @Override
        public void visit(Album album) {...}

        @Override
        public void visit(Movie album) {...}
}

// 대상 클래스의 제목을 보관하는 visitor
public class TitleVisitor implements Visitor {
        private String title;

        public String getTitle() {
                return title;
        }

        @Override
        public void visit(Book book) {
                title = &quot;[제목:&quot; + book.getName() + &quot;저자:&quot; + book.getAuthor() + &quot;]&quot;;
        }

        @Override
        public void visit(Album album) {...}

        @Override
        public void visit(Movie movie) {...}
}
</code></pre>
<h3 id="대상-클래스">대상 클래스</h3>
<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;

        ...

        public abstract void accept(Visitor visitor);
}

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

        //Getter, Setter

        @Override
        public void accept(Visitor visitor) {
                visitor.visit(this);
        }
}

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

        @Override
        public void accept(Visitor visitor) {
                visitor.visit(this);
        }
}

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

        @Override
        public void accept(Visitor visitor) {
                visitor.visit(this);
        }
}</code></pre>
<p>자식 클래스에서 구현한 accept를 보면 단순히 파라미터로 넘어온 Visitor의 visit(this) 메소드를 호출하면서 자신(this)를 파라미터로 넘기는 것이 전부다. 이렇게 해서 실제 로직은 visitor에 위임한다.</p>
<h3 id="비지터-패턴-실행">비지터 패턴 실행</h3>
<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>
<blockquote>
<p>//출력결과
book.class = class.jpabook.advanced.item.Book
[PrintVisitor]  [제목:jpabook 저자:kim]</p>
</blockquote>
<p>item이 프록시여서 먼저 프록시가 accept() 메소드를 받고 원본 엔티티(book)의 accept를 싱행한다. 원본 엔티티는 자신(this)을 visitor에 파라미터로 넘겨주어 book의 클래스가 프록시가 아닌 원본 엔티티인것을 확인 할 수 있다.</p>
<p>이렇게 비지터 패턴을 사용하면 프록시에 대한 걱정 없이 안전하게 원본 엔티티에 접근할 수 있고 instanceof없이 코드를 구현할 수 있는 장점이 있다.</p>
<h3 id="비지터-패턴-확장성">비지터 패턴 확장성</h3>
<p>비지터 패턴은 새로운 기능이 필요할 때 Visitor만 추가하면 된다. 따라서 기존 코드의 구조를 변경하지 않아도 된다.</p>
<h3 id="비지터-패턴-장점">비지터 패턴 장점</h3>
<ul>
<li>프록시에 대한 걱정 없이 안전하게 원본 엔티티에 접근 가능</li>
<li>instanceof 타입캐스팅 없이 코드 구현</li>
<li>알고리즘과 객체 구조를 분리해서 구조를 수정하지 않고 새로운 동작 추가 가능</li>
</ul>
<h3 id="비지터-패턴-단점">비지터 패턴 단점</h3>
<ul>
<li>너무 복잡하고 더블 디스패치를 사용하기 때문에 이해하기 어렵다.<blockquote>
<p>더블 디스패치란 메서드 호출 시 두 개의 객체가 타입에 따라 적절한 메서드를 선택하는 패턴입니다. 객체 지향 프로그래밍에서는 일반적으로 하나의 객체 타입(레퍼런스)에 따라 메서드를 선택하는 싱글 디스패치가 기본입니다.</p>
</blockquote>
</li>
<li>객체 구조가 변경되면 모든 Visitor를 수정해야 한다.</li>
</ul>
<hr>
<h1 id="성능-최적화">성능 최적화</h1>
<h2 id="n1-문제">N+1 문제</h2>
<h3 id="즉시-로딩과-n1">즉시 로딩과 N+1</h3>
<p>JPQL을 사용할 때 문제가 생긴다. 회원(Member)과 주문(Order)이 양방향 연관관계라고 할 때 다음 JQPL을 보자.</p>
<pre><code class="language-jpql">List&lt;Member&gt; members = em.createQuery(&quot;select m from Member m&quot;, Member.class)
    .getResultList();</code></pre>
<p>JPQL은 즉시로딩, 지연로딩을 신경쓰지 않고 SQL을 실행한다.
이때 조회된 회원이 여러명이면 문제가 생긴다. 예를 들어 3명의 회원이 조회됐다고 해보자.</p>
<pre><code class="language-sql">SELECT * FROM MEMBER //1번 실행으로 회원 여러명 조회
SELECT * FROM ORDERS WHERE MEMBER_ID = 1; //회원과 연관된 주문
SELECT * FROM ORDERS WHERE MEMBER_ID = 2; //회원과 연관된 주문
SELECT * FROM ORDERS WHERE MEMBER_ID = 3; //회원과 연관된 주문</code></pre>
<p>이처럼 처음 실행한 SQL의 수만큼 추가적으로 SQL을 실행하는 것을 N+1문제라고 한다.</p>
<h3 id="지연-로딩과-n1">지연 로딩과 N+1</h3>
<p>지연로딩도 N+1 문제를 피할 수 없다. 로직상 조회한 컬렉션을 초기화한다고 하면 회원 수 만큼 주문도 추가 조회된다.</p>
<pre><code class="language-java">for(Member member : memers) {
    //지연 로딩 초기화
        System.out.println(&quot;member = &quot; + member.getOrders().size());
}

//실행 SQL
SELECT * FROM ORDERS WHERE MEMBER_ID = 1; //회원과 연관된 주문
SELECT * FROM ORDERS WHERE MEMBER_ID = 2; //회원과 연관된 주문
SELECT * FROM ORDERS WHERE MEMBER_ID = 3; //회원과 연관된 주문</code></pre>
<p>*<em>N+1 문제를 피할 수 있는 다양한 방법을 알아보자.
*</em></p>
<h2 id="페치-조인-사용">페치 조인 사용</h2>
<p>N+1 문제를 해결하는 가장 일반적인 방법으로 페치조인이 있다.</p>
<pre><code class="language-sql">select m from Member m join fetch m.orders

//실행 SQL
SELECT M.*, O.* FROM MEMBER M
INNER JOIN ORDERS O ON M.ID=O.MEMBER_ID</code></pre>
<ul>
<li>일대다 조인의 경우 결과가 늘어나 중복된 결과가 나타날 수 있으므로 JPQL의 DISTINCT를 사용해서 중복을 제거하는 것이 좋다.</li>
</ul>
<h2 id="하이버네이트-batchsize">하이버네이트 @BatchSize</h2>
<p>org.hibernate.annotations.BatchSize 어노테이션을 사용하면 연관된 엔티티를 조회할 때 지정한 size만큼 SQL의 IN 절을 사용해서 조회한다.</p>
<pre><code class="language-java">@Entity
public class Member {
        ...
        @org.hibernate.annotaions.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명을 조회하면 2번의 SQL이 발생한다.
지연로딩의 경우는 초기화할 때 5명을 미리 조회하고 6번째가 필요한 경우 추가적으로 SQL을 실행한다.</p>
<h2 id="하이버네이트-fetchfetchmodesubselect">하이버네이트 @Fetch(FetchMode.SUBSELECT)</h2>
<p>해당 엔티티는 연관된 데이터를 조회할 때 서브 쿼리를 사용해서 N+1문제를 해결한다.</p>
<pre><code class="language-java">@Entity
public class Member {
        ...
        @org.hibernate.annotations.Fetch(FetchMode.SUBSELECT)
        @OneToMany(mappedBy = &quot;member&quot;, fetch = FetchType.EAGER)
        private List&lt;Order&gt; orders = new ArrayList&lt;Order&gt;();
        ...
}</code></pre>
<h4 id="회원-식별자-값이-10을-초과하는-회원을-조회하는-jpql">회원 식별자 값이 10을 초과하는 회원을 조회하는 JPQL</h4>
<p>즉시 로딩으로 설정하면 조회시점에 지연 로딩은 엔티티를 사용하는 시점에 SQL이 실행된다.</p>
<p>select m from Member m where m.id &gt; 10</p>
<pre><code class="language-sql">SELECT O FROM ORDERS O 
    WHERE O.MEMBER_ID IN (
            SELECT 
                    M.ID
            FROM
                    MEMBER M
            WHERE M.ID &gt; 10
)</code></pre>
<h2 id="n1-정리">N+1 정리</h2>
<p>추천 방법 : 지연 로딩만 사용하기.
즉시 로딩 전략은 N+1문제와 비즈니스 로직에 따라 필요하지 않은 엔티티까지 조회하는 문제가 자주 발생한다.
또한 최적화가 어렵다.
따라서 모두 지연 로딩으로 설정하고 성능 최적화가 꼭 필요한 곳에는 JPQL 페치 조인을 사용하자.</p>
<h4 id="jpa-글로벌-페치-전략-기본값">JPA 글로벌 페치 전략 기본값</h4>
<ul>
<li>@OneToOne, @ManyToOne: 기본 페치 전략은 즉시 로딩</li>
<li>@OntToMany, @ManyToMany: 기본 페치 전략은 지연 로딩</li>
</ul>
<p>기본값이 즉시로딩인 것들은 fetch=FetchType.LAZY로 설정하도록 하자.</p>
<hr>
<h2 id="읽기-전용-쿼리의-성능-최적화">읽기 전용 쿼리의 성능 최적화</h2>
<p>엔티티가 영속성 컨텍스트에 관리되면 1차 캐시, 변경 감지등의 이점이 많다.
하지만 영속성 컨텍스트는 변경 감지를 위해 스냅샷 인스턴스를 보관하므로 더 많은 메모리를 사용하는 단점이 있다.
조회만 하고 변경하는 일이 없다면 <strong>읽기 전용으로 엔티티를 조회하여</strong> 메모리 사용량을 최적화할 수 있다.</p>
<p>다음 JPQL 쿼리를 최적화하자.</p>
<pre><code class="language-sql">select o from Order o</code></pre>
<h3 id="스칼라-타입으로-조회">스칼라 타입으로 조회</h3>
<p>스칼라 타입으로 모든 타입을 조회하기. (스칼라 타입은 엔티티 영속성 컨텍스트가 관리하지 않음)</p>
<pre><code class="language-sql">select o.id, o.name, o.price from Order o</code></pre>
<h3 id="읽기-전용-쿼리-힌트-사용">읽기 전용 쿼리 힌트 사용</h3>
<p>하이버네이트에서 전용 힌트 org.hibernate.readOnly 사용하기.
해당 힌트를 이용하면 읽기 전용으로 엔티티를 조회할 수 있다. 영속성 컨텍스트에서 관리하지 않으므로 메모리 사용량을 최적화할 수 있다. 스냅샷이 없으므로 엔티티를 수정해도 데이터베이스에 반영되지 않는 점은 알아야 한다.</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;org.hibernate.readOnly&quot;, true);</code></pre>
<h3 id="읽기-전용-트랜잭션-사용">읽기 전용 트랜잭션 사용</h3>
<p>스프링 프레임워크의 @Transactional(readOnly = true)로 트랜잭션을 읽기 전용으로 설정할 수 있다.
이 옵션을 주면 하이버네이트 세션의 플러시 모드를 MANAUAL로 설정하여 강제로 플러시를 호출하지 않는한 플러시가 일어나지 않는다.
트랜잭션을 시작했으므로 트랜잭션 시작, 로직수행, 트랜잭션 커밋의 과정은 이루어지지만 영속성 컨텍스트가 플러시를 하지 않을 뿐이다. 플러시를 하지 않으므로 스냅샷 비교와 같은 무거운 로직을 수행하지 않아 성능이 향상된다.</p>
<h3 id="트랜잭션-밖에서-읽기">트랜잭션 밖에서 읽기</h3>
<p>트랜잭션 없이 엔티티를 조회하는 방법이다. 물론 데이터 변경을 위해서는 트랜잭션이 필수이므로 조회할 때만 사용해야 한다.</p>
<ul>
<li>스프링 프레임워크에서 사용법
: @Transactional(propagation = Propagation.NOT_SUPPROTED)    //Spring</li>
<li>J2EE 표준 컨테이너 사용법
: @TransactionAttribute(TransactioinAttributetype.NOT_SUPPROTED)    //J2EE</li>
</ul>
<p>트랜잭션을 사용하지 않으면 플러시가 일어나지 않으므로 조회성능이 좋아진다. JPQL도 트랜잭션 없이 실행하면 플러시를 호출하지 않는다.</p>
<hr>
<p>지금까지 읽기 전용 최적화를 위해 여러 방법을 살펴보았는데 스프링 프레임워크를 사용하면 읽기 전용 트랜잭션을 사용하는 것이 편리하다.</p>
<p>읽기 전용 트랜잭션(또는 트랜잭션 밖에서 읽기)과 읽기 전용 쿼리 힌트(또는 스칼라 타입으로 조회)를 동시에 사용하는 것이 가장 효과적이다.</p>
<pre><code class="language-java">@Transactional(readOnly = true) //읽기 전용 트랜잭션 -- 1
public List&lt;DataEntity&gt; findDatas() {
        return em.createQuery(&quot;select d from DataEntity d&quot;, DataEntity.class)
                            .setHint(&quot;org.hibernate.readOnly&quot;, true) //읽기 전용 쿼리 힌트 --2
                            .getResultList();
}</code></pre>
<ol>
<li>읽기 전용 트랜잭션 사용: 플러시를 작동하지 않도록 해서 성능 향상</li>
<li>읽기 전용 엔티티 사용: 엔티티를 읽기 전용으로 조회해서 메모리 절약</li>
</ol>
<hr>
<h2 id="배치-처리">배치 처리</h2>
<p>수백만 건의 데이터를 배치 처리한다고 가정해보자.
엔티티를  계속 조회하면 영속성 컨텍스트에 메모리 부족 오류가 발생한다. 따라서 배치 처리는 적절한 단위로 영속성 컨텍스트를 초기화해야 한다. 또한 2차 캐시를 사용하고 있다면 2차 캐시에 엔티티르 보관하지 않도록 주의해야 한다.</p>
<h3 id="jpa-등록-배치">JPA 등록 배치</h3>
<pre><code class="language-java">// 100건마다 플러시 호출하고 영속성 컨텍스트 초기화
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>
<p>영속성 컨텍스트에 너무 많은 엔티티가 쌓이지 않도록 일정 단위마다 데이터를 플러쉬하고 영속성컨텍스트를 초기화해야 한다.</p>
<p>수정 배치 처리는 수 많은 데이터를 한 번에 메모리에 올려둘 수 없어서 <strong>페이징 처리</strong>, <strong>커서(CURSOR)</strong> 2 가지 방법을 사용한다.</p>
<h3 id="jpa-페이징-배치-처리">JPA 페이징 배치 처리</h3>
<pre><code class="language-java">EntityManger 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)
                            .setMaxResult(pageSize)
                            .getResultList();


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

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

tx.commit();
em.close();</code></pre>
<p>100건씩 가져와서 가격을 100원씩 올리는 코드이다. 이 때 100건을 가져오고 100건을 수정하고 나면 영속성 컨텍스트를 플러쉬하고 초기화한다.</p>
<h3 id="하이버네이트-scroll-사용">하이버네이트 scroll 사용</h3>
<p>JPA는 JDBC 커서(CURSOR)를 지원하지 않아 하이버네이트 세션(Session)을 이용한다. 하이버네이트는 scroll이라는 이름으로 JDBC 커서를 지원한다.</p>
<pre><code class="language-java">EntityTransaction 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(); //영속성 컨텍스트 초기화
        }
}
tx.commit();
session.close();</code></pre>
<p>하이버네이트 전용 기능인 scroll을 사용하기 위해 먼저 em.unwrap() 메서드로 하이버네이트 세션을 구한다.
쿼리를 조회하면서 scroll() 메서드로 ScrollableResults 객체를 반환 받는다.
이 객체의 nest()를 통해 엔티티를 하나씩 조회한다.</p>
<h2 id="하이버네이트-무상태-세션-사용">하이버네이트 무상태 세션 사용</h2>
<p>하이버네이트 무상태 세션은 일반 하이버네이트 세션과 비슷하지만 <strong>영속성 컨텍스트도 없고 2차 캐시도 없다.</strong> 따라서 영속성 컨텍스트를 플러시하거나 초기화하지 않는다. 대신, 엔티티를 수정할 때 직접적으로 update() 메서드를 호출해야 한다.</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&quot;).scroll();

while(scroll.next()) {
        Product p = (Product)scroll.get(0);
        p.setPrice(p.getPrice() + 100);
        session.update(p); //직접 update를 호출
}
tx.commit();
session.close();</code></pre>
<h2 id="sql-쿼리-힌트-사용">SQL 쿼리 힌트 사용</h2>
<p>JPA는 데이터베이스 SQL 힌트 기능을 제공하지 않는다.
SQL 힌트를 사용하려면 하이버네이트를 직접 사용해야 한다.(데이터베이스 벤더에게 제공하는 힌트)
SQL 힌트는 하이버네이트 쿼리가 제공하는 addQueryHint() 메서드를 사용한다. 오라클 데이터베이스에 SQL 힌틀르 사용하는 예제를 보자.</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();</code></pre>
<pre><code class="language-sql">//실행된 SQL

select
        /*+ FULL (MEMBER) */ m.id, m.name
from 
        Member m</code></pre>
<p>하이버네이트 4.3.10 버전에는 오라클 방언에만 힌트가 적용, 다른 데이터베이스에서 SQL 힌트를 사용하려면 각 방언에 org.hibernate.dialect.Dialect에 있는 메소드 오버라이딩이 필요하다.</p>
<pre><code class="language-java">public String getQueryHintString(String query, List&lt;String&gt; hints) {
        return query
}</code></pre>
<h2 id="트랜잭션을-지원하는-쓰기-지연과-성능-최적화">트랜잭션을 지원하는 쓰기 지연과 성능 최적화</h2>
<h3 id="트랜잭션을-지원하는-쓰기-지연과-jdbc-배치">트랜잭션을 지원하는 쓰기 지연과 JDBC 배치</h3>
<p>네트워크 호출 한 번은 단순한 메서드를 수만 번 호출하는 것보다 더 큰 비용이 든다. JDBC가 제공하는 <strong>SQL 배치</strong> 기능을 사용하면 SQL을 모아서 데이터베이스에 한 번에 보낼 수 있다.
하지만 코드를 많이 수정해야 하고, 코드가 많이 얽혀 있는 곳에서는 사용하기 쉽지 않아 수백 수천 건 이상의 데이터를 변경하는 특수한 상황에 SQL 배치 기능을 사용한다.
JPA는 플러시 기능이 있으므로 SQL 배치 기능을 효과적으로 사용할 수 있다.</p>
<p>하이버네이트에서는 다음과 같이 설정하면 데이터를 등록, 수정, 삭제할 때 SQL 배치 기능을 사용한다.</p>
<pre><code class="language-xml">&lt;property name=&quot;hibernate.jdbc.batch_size&quot; value=&quot;50&quot;/&gt;</code></pre>
<p>속성의 값으로 50을 주어서 최대 50건씩 모아서 SQL 배치를 실행하도록 했다. 같은 SQL일 때만 유효한 것을 주의하자. 예를 들어 다음과 같은 경우가 있다고 하자.</p>
<pre><code class="language-java">em.persist(new Member()); //1
em.persist(new Member()); //2
em.persist(new Member()); //3
em.persist(new Member()); //4
em.persist(new Child()); //5, 다른연산
em.persist(new Member()); //6
em.persist(new Member()); //7</code></pre>
<p>4까지 모아서 하나의 SQL 배치를 실행하고 5에서 하나의 SQL 배치 실행, 6,7을 모아서 SQL 배치를 실행하여 총 번의 SQL 배치가 실행된다.</p>
<blockquote>
<p>참고  : IDENTITY 식별자 생성전략을 쓰면 em.persist()를 호출하는 즉시 DB와 통신하므로 쓰기 지연을 활용한 성능 최적화를 할 수 없다.</p>
</blockquote>
<h2 id="트랜잭션을-지원하는-쓰기-지연과-애플리케이션-확장성">트랜잭션을 지원하는 쓰기 지연과 애플리케이션 확장성</h2>
<p>트랜잭션을 지원하는 쓰기 지연과 변경 감지 덕분에 성능이 향상되고 편의를 봤지만 진짜 장점은 <strong>데이터베이스 테이블 로우에 락이 걸리는 시간을 최소회한다는 점</strong>이다.
이 기능은 트랜잭션을 커밋해서 영속성 컨텍스트를 플러시하기 전까지는 데이터베이스에 데이터를 등록, 수정, 삭제하지 않는다. 따라서 커밋 직전까지 데이터베이스 로우에 락을 걸지 않는다.</p>
<h3 id="예시">예시</h3>
<pre><code class="language-java">update(memberA);    //UPDATE SQL A
비즈니스로직A();        //UPDATE SQL ...
비즈니스로직B();        //INSERT SQL ...
commit();</code></pre>
<p>JPA를 사용하지 않고 SQL을 직접 사용하면 맨 처음 update()를 호출할 때 UPDATE SQL을 실행하면서 데이터베이스 로우에 락이 걸린다. 이 락은 비즈니스로직이 모두 실행되고 commit()이 실행될 때까지 유지된다.
트랜잭션 격리 수준에 따라 다르지만 보통 많이 사용하는 커밋된 읽기(Read Committed) 격리 수준이나 그 이상에서는 데이터베이스에 현재 수정 중인 데이터(로우)를 수정하려는 다른 트랜잭션은 락이 풀릴 때까지 대기한다.</p>
<p>JPA는 커밋을 해야 플러시를 호출하고 데이터베이스에 수정 쿼리를 보낸다. commit()을 호출할 때에야 UPDATE SQL을 실행하고 바로 데이터베이스 트랜잭션을 커밋한다. 쿼리를 보내고 바로 트랜잭션을 커밋하므로 <strong>데이터베이스에 락이 걸리는 시간을 최소화한다.</strong></p>
<p>JPA의 쓰기 지연 기능은 데이터베이스에 락이 걸리는 시간을 최소화해서 동시에 더 많은 트랜잭션을 처리할 수 있는 장점이 있다.
참조 : [자바 ORM 표준 JPA 프로그래밍]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[kafka 살펴보기]]></title>
            <link>https://velog.io/@now_here/kafka-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@now_here/kafka-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 01 Feb 2025 08:26:36 GMT</pubDate>
            <description><![CDATA[<p>kafka 제대로 된 동작원리나 개념을 모르는 것 같아 살펴보기로 한다.</p>
<h1 id="kafka가-뭔데">Kafka가 뭔데?</h1>
<p>Kafka를 검색하면 나오는 설명으로 크게 두 가지가 있는 것 같다.</p>
<ol>
<li>대용량 데이터 처리를 위해 설계된 <strong>분산 메시지 스트리밍 플랫폼</strong></li>
<li><strong>이벤트 스트리밍 플랫폼</strong></li>
</ol>
<h3 id="분산-메시지-스트리밍-플랫폼">분산 메시지 스트리밍 플랫폼</h3>
<p>카프카의 기술적 구조에 초점:</p>
<p>분산 시스템: 데이터를 여러 노드에 분산해 처리하므로 확장성과 고가용성을 제공함.
메시지 스트리밍: 메시지가 계속해서 들어오고, 실시간으로 데이터를 전달할 수 있는 스트리밍 처리 지원.</p>
<p>이 정의는 카프카가 기존 메시지 큐(Message Queue) 시스템에서 출발했다는 배경에 맞춰 설명하는 방식.</p>
<ol>
<li>카프카의 주요 역할</li>
</ol>
<p><strong>데이터 스트리밍</strong>: 
실시간으로 데이터를 수집하고 처리하기 위해 데이터를 브로커(broker)에 저장하고 배포
<strong>메시지 브로커</strong>:
서로 다른 시스템 간에 데이터를 안전하게 전달
<strong>데이터 파이프라인</strong>: 
다양한 데이터 소스에서 데이터를 수집하고 분석 시스템으로 전달하는 데이터 파이프라인을 구성</p>
<ol start="2">
<li>왜 카프카가 필요했나?
기존 메시지 브로커(MQ)나 데이터 파이프라인 도구들은 다음과 같은 문제를 가지고 있었음</li>
</ol>
<p><strong>대용량 처리 한계</strong>: 데이터의 양이 폭발적으로 증가하면서 기존 시스템은 확장성이 부족
<strong>실시간 데이터 부족</strong>: 기존 시스템은 배치(batch) 방식으로 데이터를 처리해 실시간 데이터 분석의 어려움
<strong>다양한 데이터 통합 문제</strong>: 여러 데이터 소스에서 들어오는 데이터를 안정적으로 관리하고 통합의 복잡성</p>
<p>카프카는 이러한 문제를 해결하며 실시간 데이터 스트리밍과 높은 확장성을 제공했습니다.</p>
<h3 id="이벤트-스트리밍-플랫폼">이벤트 스트리밍 플랫폼</h3>
<p>카프카의 핵심 개념인 이벤트에 더 초점을 맞춘 설명:</p>
<p>이벤트(Event): 시스템에서 발생한 상태 변화나 중요한 데이터.
예) &quot;유저가 결제를 완료했다&quot;, &quot;센서가 온도 데이터를 보냈다&quot;
스트리밍(Streaming): 이벤트가 발생하자마자 지속적으로 처리하고 소비자에게 전달.</p>
<p>이 정의는 오늘날 카프카가 주로 사용되는 데이터 처리 패러다임을 반영해 설명하는 방식으로, 특히 실시간 데이터 처리가 강조되는 환경에서는 &quot;이벤트 스트리밍 플랫폼&quot;이 더 적절한 표현이다.</p>
<p>공식 문서에서 &quot;이벤트 스트리밍 플랫폼&quot;이라고 설명하고 잇는데 오늘날 kafka가 단순한 메시지 큐 역할을 넘어서 <strong>실시간 이벤트 중심 데이터 처리</strong>를 주요 목적으로 하기 때문인 것 같다.</p>
<h1 id="이벤트-스트리밍이란">이벤트 스트리밍이란?</h1>
<p>카프카가 이벤트 스트리밍 플랫폼에 더 가깝다고 설명했는데 그러면 이벤트 스트리밍이란 무엇일까?</p>
<p>기술적으로 말해서 <strong>이벤트 스트리밍</strong>은 데이터베이스, 센서, 모바일 기기, 클라우드 서비스, 소프트웨어 애플리케이션과 같은 이벤트 소스에서 이벤트 스트림 형태로 <strong>실시간으로 데이터를 캡처하는 것</strong>을 말한다. 이러한 이벤트 스트림을 나중에 검색할 수 있도록 내구성 있게 저장하고 실시간 및 사후 분석을 통해 조작, 처리 및 반응한다. 또한 필요에 따라 이벤트 스트림을 다른 시스템 또는 기술로 전달한다.
결과적으로 이벤트 스트리밍은 <strong>적절한 정보가 적시에 적합한 장소에 전달되도록</strong> 데이터의 지속적인 흐름과 해석을 보장한다.</p>
<h2 id="이벤트-스트리밍-사용-예시">이벤트 스트리밍 사용 예시</h2>
<ul>
<li>증권 거래소, 은행, 보험 등에서 결제 및 금융 거래를 실시간으로 처리합니다.</li>
<li>물류 및 자동차 산업 등에서 자동차, 트럭, 차량대, 배송물을 실시간으로 추적하고 모니터링합니다.</li>
<li>공장이나 풍력 발전소 등 IoT 장치나 기타 장비에서 센서 데이터를 지속적으로 수집하고 분석합니다.</li>
<li>소매, 호텔 및 여행 업계, 모바일 애플리케이션 등에서 고객 상호작용 및 주문을 수집하고 즉시 대응합니다.</li>
<li>병원 치료 중인 환자를 모니터링하고 상태 변화를 예측하여 응급 상황에서 적절한 치료를 보장합니다.</li>
<li>회사의 여러 부서에서 생성된 데이터를 연결, 저장하고 이용 가능하게 만듭니다.</li>
</ul>
<h2 id="kafka-핵심-기술">Kafka 핵심 기술</h2>
<blockquote>
<p>다른 시스템에서 데이터를 지속적으로 가져오고 내보내는 것을 포함하여 이벤트 스트림을 게시(쓰기)하고 구독(읽기)합니다.</p>
</blockquote>
<p>카프카는 <strong>발신자(Producer)</strong>가 데이터를 <strong>토픽(Topic)</strong>에 게시하고, <strong>수신자(Consumer)</strong>가 해당 토픽을 구독하여 데이터를 읽는 구조!</p>
<p>게시(쓰기): 다양한 시스템(예: IoT 센서, 애플리케이션 로그)에서 발생하는 데이터를 실시간으로 카프카에 보냄.
구독(읽기): 다른 서비스나 데이터 파이프라인이 이 데이터를 구독해 실시간 또는 배치(batch)로 처리.
카프카의 장점은 메시지를 손실 없이 고성능으로 처리하는 분산 구조 덕분에 대량의 데이터 스트림을 다룰 수 있다는 점이다.</p>
<blockquote>
<p>원하는 기간 동안 이벤트 스트림을 지속적이고 안정적으로 저장합니다.
카프카는 단순히 이벤트를 전달하는 것만 하지 않고 내구성 있게 저장할 수 있다.</p>
</blockquote>
<p>이벤트는 카프카 브로커의 디스크에 저장되며, 설정에 따라 며칠, 몇 주 또는 무기한 저장 가능.
이를 통해 실시간뿐만 아니라 과거 데이터 재처리도 가능!
<strong>전통적인 메시지 큐(MQ)</strong>는 데이터가 소비되면 삭제되는 반면, 카프카는 데이터의 스토리지 시스템 역할도 수행한다.</p>
<blockquote>
<p>실시간 및 사후 이벤트 스트림 처리</p>
</blockquote>
<p>카프카는 데이터가 발생하는 즉시 실시간 처리와 필요에 따라 회고적(batch) 처리도 지원!</p>
<p>실시간 처리: Kafka Streams, Flink, Spark와 같은 스트리밍 프레임워크를 통해 실시간 데이터 분석.
사후 처리: 특정 시점에 발생한 이벤트 데이터를 다시 읽어 재처리.
실시간과 배치 처리를 모두 지원하기 때문에 데이터 손실 최소화 및 유연한 분석이 가능하다.</p>
<h1 id="카프카의-아키텍처">카프카의 아키텍처</h1>
<p>카프카의 아키텍처를 알려면 먼저 Pub/Sub 구조를 알아야 한다.
카프카의 탄생 배경을 먼저 알면 좋은데, 링크드인에서 대규모 데이터 처리 문제를 겪고 있었고 이를 해결하기 위해 카프카를 만들었다.</p>
<p>실시간 로그 수집: 사용자 활동 데이터, 서버 로그 등이 대량으로 발생
데이터 전달 지연: 기존 시스템은 대량 데이터를 여러 시스템으로 효율적으로 전달하기 어려움
데이터 손실: 장애가 발생하면 메시지가 손실되거나 순서가 뒤바뀌는 문제</p>
<p><img src="https://velog.velcdn.com/images/now_here/post/dcdd0913-b0ea-4596-8579-1efb78aed1ed/image.png" alt=""></p>
<p>기존 메시지 큐의 한계
당시 주로 사용되던 메시지 큐(Message Queue) 시스템들은 포인트-투-포인트(Point-to-Point) 구조가 일반적이었다.</p>
<ul>
<li>발신자가 수신자에게 직접 메시지를 보내는 방식.</li>
<li>수신자가 메시지를 소비하면 삭제됨(데이터의 내구성 부족).</li>
<li>동일한 메시지를 여러 시스템에서 동시에 처리하기 어려움.</li>
</ul>
<p>카프카는 이런 문제를 해결하기 위해 Pub/Sub(발행/구독) 구조를 채택하고, Pub/Sub구조는 아래와 같은 장점이 있다.</p>
<p><strong>느슨한 결합(Decoupling)</strong>
발신자(Producer)와 수신자(Consumer)가 직접 연결되지 않아도 됨.
여러 Consumer가 동시에 동일한 메시지를 수신 가능.</p>
<p><strong>데이터 재사용</strong>
Consumer가 메시지를 읽더라도 메시지는 카프카 브로커에 내구성 있게 저장됨.
필요할 때 과거 데이터를 다시 읽을 수 있어 데이터 분석에 유리.</p>
<p><strong>확장성</strong>
분산 브로커 아키텍처 덕분에 수평적으로 확장 가능.
수천 개의 Producer와 Consumer가 동시에 연결되어도 안정적으로 작동.</p>
<p><strong>대용량 실시간 처리</strong>
Pub/Sub 구조 덕분에 실시간으로 이벤트 스트리밍을 처리할 수 있어 대규모 트래픽 대응이 가능.</p>
<h2 id="pubsub-구조">Pub/Sub 구조</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/3a478469-db64-4f8c-9dd9-0a86602b84b7/image.png" alt="">
Pub/Sub(발행/구독) 모델은 데이터를 <strong>발행자(Producer)</strong>가 보내면 <strong>구독자(Consumer)</strong>가 해당 데이터를 받아보는 방식으로 중간에 <strong>브로커(Broker)</strong>가 있어서 발행과 구독을 관리한다.
(카프카에서는 Publisher의 역할을 Producer가 하고 Subscriber의 역할을 Consumer가 행함)</p>
<h4 id="구성-요소">구성 요소</h4>
<p><strong>발행자(Producer)</strong>: 메시지를 생성하고 브로커에 보냄
예) 뉴스 기사가 작성되면 서버에 업로드
<strong>브로커(Broker)</strong>: 메시지를 받아서 원하는 구독자에게 전달
예) 카프카가 여기서 브로커 역할
<strong>구독자(Consumer)</strong>: 브로커에서 메시지를 구독하여 처리
예) 뉴스 앱에서 특정 주제 뉴스를 받아보기</p>
<h4 id="동작-방식">동작 방식</h4>
<p>발행자는 자신이 보내는 메시지를 <strong>특정 주제(Topic)</strong>에 게시함.
구독자는 그 주제를 구독해 필요한 데이터만 받아봄.</p>
<hr>
<h1 id="카프카-주요-개념용어들">카프카 주요 개념/용어들</h1>
<p><img src="https://velog.velcdn.com/images/now_here/post/2de8d38e-ca62-4fe1-85a3-55ab6ab9829c/image.png" alt=""></p>
<h2 id="kafka-cluster">kafka cluster</h2>
<ul>
<li>여러 대의 Kafka Broker가 모인 그룹</li>
<li>데이터 분산 처리, 고가용성(HA)을 위해 사용됨</li>
<li>일반적이로 3개 이상의 Broker가 클러스터를 이룸.</li>
</ul>
<h3 id="kafka-cluster의-기능">Kafka Cluster의 기능</h3>
<p>1) 고가용성 (High Availability)
하나의 Kafka Broker가 고장나도 나머지 Broker들이 데이터 처리를 이어갈 수 있다.
클러스터가 아니었다면 브로커 하나가 다운될 때 서비스 전체가 멈출 위험이 있다.</p>
<p>2) 확장성 (Scalability)
데이터 처리량이 증가할 때 새로운 Broker를 추가해서 더 많은 데이터를 분산 처리할 수 있다.
단일 브로커로는 대량의 데이터를 처리하는 데 한계가 있다.</p>
<p>3) 데이터 복제 (Replication)
동일한 데이터를 여러 Broker에 복제해서 데이터 손실을 방지한다!
예를 들어, 하나의 브로커가 고장나도 복제된 데이터가 다른 브로커에 남아 있음.</p>
<p>간단하게 말하자면 Kafka Cluster는 여러 대의 트럭으로 구성된 배송 네트워크! 한 트럭이 고장나도 나머지 트럭이 계속 배송할 수 있다.</p>
<h4 id="데이터-분산-구조">데이터 분산 구조</h4>
<ol>
<li>Producer가 데이터를 Kafka Cluster로 전송</li>
<li>Kafka Cluster는 데이터의 복제와 파티션을 여러 Broker에 분산 저장</li>
<li>Consumer는 여러 Broker에서 데이터를 병렬로 읽음</li>
</ol>
<h2 id="producer생산자">Producer(생산자)</h2>
<ul>
<li>데이터를 카프카로 전송하는 역할
ex) 결제 시스템에서 결제 완료 이벤트를 생성
Producer -&gt; Kafka Broker (Kafkak Cluster) -&gt; Topic
좀 더 정확히 말하면 Producer는 데이터를 Kafka Cluster 내의 Broker에 잇는 Topic에 보내는 구조이다.</li>
</ul>
<h2 id="broker브로커">Broker(브로커)</h2>
<ul>
<li>데이터를 저장하고 관리하는 서버.
카프카 클러스터는 여러 개의 브로커로 구성될 수 있으며, 데이터를 분산 저장한다.
프로듀서와 컨슈머가 데이터를 주고받을 때 직접 연결되는 대상이 Broker이다.</li>
</ul>
<h2 id="topic토픽">Topic(토픽)</h2>
<ul>
<li>데이터가 저장되는 논리적인 공간. 특정 종류의 이벤트 데이터를 구분하기 위해 Topic을 사용한다.
토픽은 <strong>Partition(파티션)</strong>으로 나뉘며, 데이터의 분산 저장 및 병렬 처리를 지원한다.
파티션은 다른 서버(Broker)에 분산되어 저장될 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/now_here/post/68a722d1-7355-49fc-be87-1dc2d59329e6/image.png" alt=""></p>
<p>예를 들어 위에 그림에 &quot;Topic A&quot;가 partition 0, partition 1, partition 2로 나뉘어져 있는데 이 파티션들이 Broker 1, Broker 2, Broker 3에 존재한다. </p>
<p>partition에 보면 Leader와 Follower로 구성되어 있는데 이는 복제와 관련되어 있다.
예를 들어 Broker 1에 partition 0을 보자. Broker 1에 partition 0에는 Leader라고 되어 있는데 이는 처음에 데이터가 들어왔을 때 저장되는 partition을 의미한다. 이 후에 Leader 파티션에 저장이 된 후에 Broker2, 3에 존재하는 Follower partition 0들에 데이터가 복제된다. </p>
<p>이를 자세히 알아보기 위해 카프카에서의 <strong>병렬 처리</strong>와 <strong>복제(Replication)</strong>을 알아보자.</p>
<h3 id="데이터-병렬-처리">데이터 병렬 처리</h3>
<p>병렬 처리란 Kafka 클러스터 내에서 파티션들이 각각 <strong>다른 브로커에서 독립적으로 리더 역할을 수행하며 동시에 메시지를 처리하는 것</strong>을 의미한다.</p>
<h4 id="병렬-처리-동작-방식">병렬 처리 동작 방식</h4>
<ul>
<li>프로듀서가 데이터를 발행할 때, Kafka는 <strong>파티션 키(partition key)</strong>나 <strong>라운드 로빈 방식</strong> 등을 통해 적절한 파티션에 메시지를 분배 </li>
<li>각 파티션의 <strong>리더(Leader)</strong> 브로커는 독립적으로 데이터를 저장</li>
<li>따라서 다음과 같은 구조가 병렬 처리의 핵심</li>
</ul>
<table>
<thead>
<tr>
<th>Partition</th>
<th>Leader Broker</th>
<th>메시지 처리</th>
</tr>
</thead>
<tbody><tr>
<td>Partition 0</td>
<td>Broker 1</td>
<td>동시에 메시지 저장</td>
</tr>
<tr>
<td>Partition 1</td>
<td>Broker 2</td>
<td>동시에 메시지 저장</td>
</tr>
<tr>
<td>Partition 2</td>
<td>Broker 3</td>
<td>동시에 메시지 저장</td>
</tr>
</tbody></table>
<h4 id="병렬-처리의-장점">병렬 처리의 장점</h4>
<ul>
<li><strong>고성능 처리:</strong> 파티션이 여러 브로커에 분산되어 있어 병렬로 메시지를 저장 및 처리하므로 처리량(Throughput)이 크게 증가</li>
<li><strong>확장성:</strong> 파티션과 브로커 수를 늘림으로써 처리 성능을 수평적으로 확장</li>
<li><strong>내구성:</strong> 복제를 통한 데이터 안정성 보장</li>
</ul>
<h3 id="복제replication와-리더-팔로워-개념">복제(Replication)와 리더-팔로워 개념</h3>
<ol>
<li><p><strong>복제(Replication)</strong>:  
Kafka에서는 데이터 안정성과 고가용성을 위해 파티션을 여러 브로커에 복제한다. 복제본 개수는 토픽 설정에서 <strong>Replication Factor</strong>로 지정된다. 예를 들어, <code>Replication Factor = 3</code>이라면 각 파티션이 3개의 브로커에 복제된다.</p>
</li>
<li><p><strong>리더(Leader)와 팔로워(Follower)</strong>:</p>
<ul>
<li>각 파티션에는 하나의 <strong>리더(Leader)</strong>가 존재</li>
<li>모든 <strong>읽기/쓰기 요청은 리더를 통해 처리</strong>.</li>
<li>리더 외에 다른 브로커에 저장된 복제본들은 <strong>팔로워(Follower)</strong>로 작동하며, 리더의 데이터를 실시간으로 동기화한다.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="그림-설명">그림 설명</h3>
<table>
<thead>
<tr>
<th>Partition</th>
<th>Broker 1</th>
<th>Broker 2</th>
<th>Broker 3</th>
</tr>
</thead>
<tbody><tr>
<td>Partition 0</td>
<td>Leader</td>
<td>Follower</td>
<td>Follower</td>
</tr>
<tr>
<td>Partition 1</td>
<td>Follower</td>
<td>Leader</td>
<td>Follower</td>
</tr>
<tr>
<td>Partition 2</td>
<td>Follower</td>
<td>Follower</td>
<td>Leader</td>
</tr>
</tbody></table>
<ul>
<li>각 파티션은 <strong>한 브로커에서 리더 역할</strong>을 하고, 나머지 브로커는 해당 파티션의 복제본(Follower)을 저장한다.</li>
<li>이 구조는 장애 상황에서 리더 브로커가 다운되더라도 팔로워가 리더로 승격(Leader Election)되어 서비스가 지속될 수 있도록 한다.</li>
</ul>
<h3 id="복제-시점">복제 시점</h3>
<ol>
<li><p>리더 파티션에 메시지가 저장된 직후
프로듀서가 리더(Leader) 파티션에 메시지를 성공적으로 쓰면, Kafka는 즉시 팔로워(Follower)들에게 해당 데이터를 복제.
즉, Broker 1의 Partition 0 (Leader)에 데이터가 저장되면 Broker 2나 Broker 3의 Partition 0 (Follower)로 복제가 이루어진다.</p>
</li>
<li><p>비동기(Asynchronous) 복제
복제는 비동기 방식으로 이루어진다. 즉, 리더에 데이터가 먼저 쓰인 후 팔로워들이 이를 복사한다.</p>
</li>
</ol>
<p>팔로워 파티션들은 복제된 데이터만 저장하며 데이터 읽기나 쓰기 요청을 처리하지 않는다고 한다. 만약 리더가 장애가 발생하면 Kafka가 <strong>리더 선출(Leader Election)</strong>을 통해 팔로워 중 하나를 새로운 리더로 승격한다.</p>
<h2 id="consumer소비자">Consumer(소비자)</h2>
<ul>
<li>카프카로부터 데이터를 읽어 처리하는 역할
ex) 결제 완료 이벤트를 기반으로 영수증 발행 시스템이 이벤트를 처리</li>
</ul>
<h3 id="카프카에서의-구독-consumer-group"><strong>카프카에서의 구독 (Consumer Group)</strong></h3>
<p><strong>카프카의 구독</strong>은 컨슈머가 특정 <strong>토픽</strong>에 발행된 메시지를 읽어오는 프로세스를 의미한다. 여기서 중요한 개념은 <strong>컨슈머 그룹</strong>이다.</p>
<ol>
<li><p><strong>구독과 메시지 소비</strong></p>
<ul>
<li>카프카에서 <strong>구독</strong>은 컨슈머가 하나 이상의 토픽에 대해 <strong>메시지를 읽기</strong> 시작하는 행위 </li>
<li>토픽에 메시지가 발행되면, <strong>구독 중인 컨슈머</strong>는 이를 <strong>읽을 수</strong> 있다.</li>
</ul>
</li>
<li><p><strong>컨슈머 그룹</strong></p>
<ul>
<li>카프카에서는 <strong>컨슈머 그룹</strong>을 활용하여 여러 컨슈머가 하나의 <strong>토픽</strong>에 대해 동시에 작업을 나누어 할 수 있다. 즉, 여러 컨슈머가 <strong>각각 다른 파티션</strong>을 읽어 처리하는 방식.</li>
<li><strong>컨슈머 그룹 내에서 파티션을 나누어 읽는다</strong>는 점에서 <strong>병렬 처리</strong>가 가능해지며, 하나의 토픽에 대해 여러 컨슈머가 동시에 데이터를 처리할 수 있다.</li>
</ul>
</li>
<li><p><strong>메시지 읽기 시점</strong></p>
<ul>
<li>카프카는 <strong>토픽의 파티션</strong>에 대해 <strong>메시지 소비를 주기적으로</strong> 한다. 컨슈머는 <strong>토픽에 새로운 메시지가 발행되었을 때</strong> 그 메시지를 <strong>즉시 읽지 않고* 주기적으로 **poll()</strong> 메서드를 통해 메시지를 확인한다.</li>
<li>즉, <strong>메시지가 발행되고 일정 시간이 지난 후</strong>에 <strong>컨슈머가 이를 읽어오는 방식</strong>으로 이때, <code>poll()</code>을 사용해 새로운 메시지가 있는지 주기적으로 확인하면서 메시지를 읽는다.</li>
</ul>
</li>
</ol>
<h3 id="구독-과정"><strong>구독 과정</strong></h3>
<ol>
<li><p><strong>컨슈머가 구독을 시작한다</strong></p>
<ul>
<li>예를 들어, <code>Consumer1</code>은 <code>chat-room-1</code> 토픽을 구독한다고 설정되었다면, 이 컨슈머는 해당 토픽의 메시지를 읽을 준비가 된다.</li>
</ul>
</li>
<li><p><strong>메시지가 발행된다</strong></p>
<ul>
<li>채팅방 1에 사용자가 메시지를 보냅니다. 이 메시지는 카프카의 <code>chat-room-1</code> 토픽에 발행된다. 이제 이 메시지는 <strong>발행</strong>된 상태가 된다.</li>
</ul>
</li>
<li><p><strong>컨슈머가 <code>poll()</code> 메서드로 메시지를 확인한다</strong></p>
<ul>
<li><code>Consumer1</code>은 <code>poll()</code> 메서드를 호출해 주기적으로 카프카로부터 메시지를 받아온다. 메시지가 없으면 아무것도 받지 않고 대기하게 된다.</li>
<li>카프카는 메시지가 있는지 확인한 후, <strong>새로운 메시지를 컨슈머에게 전달</strong></li>
</ul>
</li>
<li><p><strong>컨슈머가 메시지를 처리한다</strong></p>
<ul>
<li><code>Consumer1</code>은 카프카에서 받은 메시지를 처리하고, 필요한 작업(예: 메시지 화면에 출력)을 한다.</li>
</ul>
</li>
<li><p><strong>메시지 오프셋</strong></p>
<ul>
<li>카프카에서 각 컨슈머는 <strong>메시지 오프셋</strong>을 관리한다. 이 오프셋은 컨슈머가 어떤 메시지까지 읽었는지를 추적한다.</li>
<li>컨슈머는 메시지를 <strong>읽은 후 오프셋을 업데이트</strong>하며, <strong>중단 후 다시 시작</strong>할 때 이전에 읽은 위치부터 계속해서 메시지를 처리할 수 있다.</li>
</ul>
</li>
</ol>
<h3 id="구독의-핵심-포인트"><strong>구독의 핵심 포인트</strong></h3>
<ul>
<li><strong>메시지 발행</strong>은 즉시 일어나지만, <strong>컨슈머는 실시간으로 바로 읽지 않습니다</strong>. 대신 <strong>주기적으로 메시지를 확인</strong>하면서 읽습니다.</li>
<li><strong>Poll()</strong>을 사용하여 <strong>주기적으로 메시지를 확인</strong>하고, 해당 메시지를 읽어옵니다.</li>
<li><strong>컨슈머 그룹</strong>을 사용하면 여러 컨슈머가 분산되어 <strong>병렬 처리</strong>가 가능하지만, 한 파티션당 하나의 컨슈머만 메시지를 읽을 수 있습니다.</li>
</ul>
<h3 id="컨슈머와-파티션의-관계">컨슈머와 파티션의 관계</h3>
<ol>
<li>컨슈머는 토픽을 구독한다.</li>
</ol>
<ul>
<li>카프카에서 토픽은 하나 이상의 파티션으로 나뉘어져 있는데, 컨슈머는 <strong>토픽</strong>을 구독함으로써 파티션들에 발행된 메시지를 읽게 된다.</li>
</ul>
<ol start="2">
<li><strong>파티션은 반드시 하나의 컨슈머만 처리한다.</strong></li>
</ol>
<ul>
<li>한 파티션은 컨슈머 그룹 내에서 하나의 컨슈머만 처리할 수 있다. 즉, 파티션당 하나의 컨슈머만 연결된다. (<strong>메시지가 중복 처리되지 않도록 보장하기 위해서</strong>)
예를 들어, 컨슈머 그룹 A가 토픽 X를 구독하고 있고, 토픽 X에 3개의 파티션이 있다고 가정해 보자. 그러면 컨슈머 그룹 A에 최소 3개의 컨슈머가 있어야만 각 파티션을 하나씩 처리할 수 있다.</li>
</ul>
<ol start="3">
<li>컨슈머 그룹과 파티션 배정</li>
</ol>
<ul>
<li>컨슈머 그룹 내에서 각 파티션은 하나의 컨슈머에만 할당된다. 즉, 하나의 파티션은 동시에 여러 컨슈머가 처리할 수 없고 하나의 컨슈머만 처리하게 된다.</li>
<li>만약 컨슈머 그룹 내에 컨슈머의 수가 파티션 수보다 적으면, 하나의 컨슈머가 여러 파티션을 처리하게 된다.</li>
<li>반대로, 컨슈머의 수가 파티션 수보다 많으면, 일부 컨슈머는 파티션을 할당받지 못하게 된다.</li>
</ul>
<h2 id="zookeeper">Zookeeper</h2>
<ul>
<li>클러스터 메타데이터(브로커 정보, 파티션 리더 등)를 관리하는데 사용
(참고로 최근에는 Zookeeper 없이도 운영 가능한 KRaft 모드로 대체되고 있다고 함)</li>
</ul>
<h3 id="1-zookeeper란">1. <strong>Zookeeper란?</strong></h3>
<p><strong>Zookeeper</strong>는 <strong>분산 시스템의 코디네이션 서비스</strong>로, 여러 서버들 간의 <strong>상태 정보</strong>, <strong>구성 정보</strong>, <strong>동기화</strong> 등을 관리하는 역할을 한다. 쉽게 말하면, 여러 서버들 간에 협력하고 데이터를 일관되게 유지하려면 Zookeeper가 그들 간의 <strong>조정자 역할</strong>을 한다.</p>
<h4 id="zookeeper의-주요-기능">Zookeeper의 주요 기능:</h4>
<ul>
<li><strong>노드 관리</strong>: 클러스터 내의 <strong>서버</strong>(노드)들이 서로 정보를 공유하고, 서버 간에 <strong>상태 변경</strong>을 감지하고 조정할 수 있게 해준다.</li>
<li><strong>리더 선출</strong>: <strong>리더/팔로워</strong> 모델을 통해 <strong>리더 서버</strong>를 선출하고, 이를 기반으로 데이터를 처리하게 한다.</li>
<li><strong>구성 관리</strong>: 클러스터의 <strong>구성 정보</strong>를 중앙에서 관리하고, 변경 사항을 클러스터에 전파한다.</li>
<li><strong>메타데이터 관리</strong>: 카프카에서 <strong>파티션 리더</strong>와 <strong>팔로워</strong>를 관리하는 중요한 정보를 Zookeeper에서 유지한다.</li>
</ul>
<h3 id="2-카프카와-zookeeper의-관계">2. <strong>카프카와 Zookeeper의 관계</strong></h3>
<p>카프카는 처음에 분산 시스템을 구성하기 위해 <strong>Zookeeper</strong>를 사용하여 클러스터의 상태를 관리하고 <strong>파티션</strong>의 <strong>리더</strong>와 <strong>팔로워</strong>를 관리하는데 사용했다. 예를 들어, <strong>Zookeeper</strong>는 카프카의 <strong>브로커</strong>가 <strong>리더/팔로워 상태</strong>를 변경할 때 이 정보를 서로 공유하고 관리하는 역할을 한다.</p>
<h4 id="카프카와-zookeeper가-함께-하는-이유">카프카와 Zookeeper가 함께 하는 이유:</h4>
<ul>
<li><strong>브로커 정보 관리</strong>: 카프카는 클러스터에 여러 개의 <strong>브로커</strong>를 가지고 있는데, <strong>Zookeeper</strong>는 브로커 간의 <strong>연결 상태</strong>와 <strong>구성 정보</strong>를 관리한다.</li>
<li><strong>파티션 관리</strong>: 각 <strong>파티션의 리더</strong>와 <strong>팔로워</strong>를 <strong>Zookeeper</strong>에서 관리하여, 파티션을 다른 브로커로 이동할 때 이를 동기화하고 조정한다.</li>
<li><strong>클러스터 상태 추적</strong>: 클러스터 내 브로커가 <strong>가동 중인지, 다운되었는지</strong> 같은 상태를 <strong>Zookeeper</strong>에서 관리하며, <strong>리더 선출</strong>을 통해 <strong>카프카의 안정성</strong>을 보장.</li>
</ul>
<h3 id="3-zookeeper를-사용하는-방법">3. <strong>Zookeeper를 사용하는 방법</strong></h3>
<p>카프카는 설정 파일에서 Zookeeper의 <strong>주소</strong>를 지정해야 했다. 예를 들어, <strong>카프카 설정</strong>에서 다음과 같이 <strong>Zookeeper 주소</strong>를 설정할 수 있다.</p>
<pre><code class="language-properties"># 카프카 서버 설정 (server.properties)
zookeeper.connect=localhost:2181  # Zookeeper 서버의 주소</code></pre>
<p>카프카 클러스터를 구성할 때 <strong>Zookeeper가 필수</strong>였고, 카프카는 내부적으로 <strong>Zookeeper를 통해 브로커</strong>와 <strong>파티션 리더 관리</strong>를 했다.</p>
<h3 id="4-kraft-모드-kraft--kafka-raft">4. <strong>KRaft 모드 (KRaft = Kafka Raft)</strong></h3>
<p><strong>KRaft 모드</strong>는 카프카가 <strong>Zookeeper 없이</strong> 자체적으로 <strong>Raft 프로토콜</strong>을 사용하여 <strong>리더 선출</strong>과 <strong>메타데이터 관리</strong>를 하는 모드!
<strong>Raft 프로토콜</strong>은 분산 시스템에서 <strong>일관성을 유지</strong>하면서 <strong>리더를 선출</strong>하고, <strong>파티션</strong>을 관리하는 알고리즘이다.</p>
<h4 id="kraft-모드의-특징">KRaft 모드의 특징:</h4>
<ul>
<li><strong>Zookeeper 없이 카프카 자체적으로 관리</strong>: 카프카가 <strong>Zookeeper 없이</strong> 자체적으로 클러스터를 관리하고 <strong>메타데이터</strong>를 유지</li>
<li><strong>Raft 프로토콜 사용</strong>: 카프카가 <strong>Raft 프로토콜</strong>을 사용하여 <strong>리더 선출</strong>과 <strong>메타데이터 동기화</strong></li>
<li><strong>단순화된 아키텍처</strong>: Zookeeper를 제거함으로써 카프카 클러스터의 <strong>구성 및 관리가 간소화</strong></li>
</ul>
<h3 id="5-왜-kraft-모드를-사용하나">5. <strong>왜 KRaft 모드를 사용하나?</strong></h3>
<p>KRaft 모드는 <strong>Zookeeper의 필요성</strong>을 없애고 카프카 자체로 <strong>리더 선출</strong>과 <strong>메타데이터 관리</strong>를 하여 <strong>운영의 복잡성</strong>을 크게 줄일 수 있다.</p>
<h4 id="kraft-모드를-사용하는-이유">KRaft 모드를 사용하는 이유:</h4>
<ul>
<li><strong>단순화된 클러스터 관리</strong>: Zookeeper를 따로 운영할 필요가 없어서 <strong>클러스터 관리가 단순화</strong></li>
<li><strong>운영 비용 절감</strong>: 별도의 <strong>Zookeeper 클러스터</strong>를 관리할 필요가 없어서 <strong>운영 비용</strong> 절감</li>
<li><strong>고가용성 향상</strong>: 카프카가 <strong>자체적으로 리더 선출</strong>을 하므로 Zookeeper의 장애가 카프카에 미치는 영향 최소화</li>
<li><strong>미래 지향적인 설계</strong>: Zookeeper 없이 카프카를 운영할 수 있는 <strong>현대적인 분산 시스템</strong>으로의 발전을 위해 KRaft 모드를 사용</li>
</ul>
<h3 id="6-kraft-모드의-설정">6. <strong>KRaft 모드의 설정</strong></h3>
<p>KRaft 모드를 사용하려면, 카프카 설정에서 Zookeeper 관련 항목을 제거하고, 대신 <strong>KRaft 모드 설정</strong>을 활성화해야 한다.</p>
<pre><code class="language-properties"># 카프카 KRaft 모드 설정 (server.properties)
# Zookeeper 설정을 비활성화
zookeeper.connect= # 비워두거나 제거

# KRaft 모드 활성화
process.roles=broker,controller
# 카프카가 브로커 역할과 컨트롤러 역할을 동시에 할 수 있도록 설정</code></pre>
<h3 id="7-결론">7. <strong>결론</strong></h3>
<ul>
<li><strong>Zookeeper</strong>는 카프카의 클러스터와 파티션 관리를 위한 <strong>분산 코디네이션 시스템</strong>. 하지만 최근에는 <strong>KRaft 모드</strong>가 도입되어 <strong>Zookeeper 없이 카프카를 운영</strong>할 수 있게 되었다.</li>
<li><strong>KRaft 모드</strong>는 <strong>Raft 프로토콜</strong>을 사용하여 카프카 자체적으로 <strong>리더 선출</strong>과 <strong>메타데이터 관리</strong>를 할 수 있게 하며, 운영의 <strong>복잡성을 줄이고</strong> 안정성을 높이는 데 도움을 준다.</li>
</ul>
<h2 id="참고">참고</h2>
<p><a href="https://kafka.apache.org/intro#intro_platform">https://kafka.apache.org/intro#intro_platform</a>
<a href="https://velog.io/@hahnwoong/%EC%B9%B4%ED%94%84%EC%B9%B4-%EC%B9%B4%ED%94%84%EC%B9%B4-%EC%B9%B4%ED%94%84%EC%B9%B4-%EC%B9%B4%ED%94%84%EC%B9%B4.-%EC%96%B4-%EC%96%B4%ED%97%88-%EC%96%B4%EB%A0%A4%EC%9B%8C">https://velog.io/@hahnwoong/%EC%B9%B4%ED%94%84%EC%B9%B4-%EC%B9%B4%ED%94%84%EC%B9%B4-%EC%B9%B4%ED%94%84%EC%B9%B4-%EC%B9%B4%ED%94%84%EC%B9%B4.-%EC%96%B4-%EC%96%B4%ED%97%88-%EC%96%B4%EB%A0%A4%EC%9B%8C</a>
<a href="https://velog.io/@djc06048/%EA%B0%9C%EB%85%90%EC%A0%95%EB%A6%AC-Kafka-Redis-RabbitMQ%EB%9E%80">https://velog.io/@djc06048/%EA%B0%9C%EB%85%90%EC%A0%95%EB%A6%AC-Kafka-Redis-RabbitMQ%EB%9E%80</a>
<a href="https://velog.io/@hyeondev/Apache-Kafka-%EC%9D%98-%EA%B8%B0%EB%B3%B8-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90">https://velog.io/@hyeondev/Apache-Kafka-%EC%9D%98-%EA%B8%B0%EB%B3%B8-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[14. 컬렉션과 부가기능]]></title>
            <link>https://velog.io/@now_here/14.-%EC%BB%AC%EB%A0%89%EC%85%98%EA%B3%BC-%EB%B6%80%EA%B0%80%EA%B8%B0%EB%8A%A5</link>
            <guid>https://velog.io/@now_here/14.-%EC%BB%AC%EB%A0%89%EC%85%98%EA%B3%BC-%EB%B6%80%EA%B0%80%EA%B8%B0%EB%8A%A5</guid>
            <pubDate>Sat, 25 Jan 2025 09:23:58 GMT</pubDate>
            <description><![CDATA[<h1 id="컬렉션">컬렉션</h1>
<p><img src="https://velog.velcdn.com/images/now_here/post/15d92f1f-447e-4f7d-a8ad-bea81ea2a7b1/image.png" alt=""></p>
<p>JPA는 자바의 컬렉션을 위와 같이 제공하고 다음과 같은 경우에 컬렉션을 사용합니다.</p>
<ul>
<li>@OneToMany, @ManyToOne를 사용하여 일대다나 다대다 엔티티 관계를 매핑할 경우</li>
<li>@ElementCollection을 사용해서 값 타입을 하나 이상 보관할 때</li>
</ul>
<p>JPA 구현체마다 제공하는 컬렉션 기능이 다를 수 있다. 여기서는 하이버네이트 구현체를 기준으로 이야기한다.</p>
<h2 id="jpa와-컬렉션">JPA와 컬렉션</h2>
<p>하이버네이트에서 엔티티를 영속 상태로 만들 때 컬렉션 필드를 하이버네이트의 컬렉션으로 감싸서 사용한다.
예를 들어 일대다 관계인 팀 엔티티와 멤버 엔티티가 가정하자. 멤버 엔티티는 팀엔티티에 @OneToMany로 연관관계가 설정되어 있다.</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>(8장 프록시에서 배운 것을 기억해보자. @OneToMany처럼 다 엔티티를 연관관계로 가지면 지연로딩이 기본값)</p>
<pre><code class="language-java">Team team = new Team();

System.out.println(&quot;before persist = &quot; + team.getMembers().getClass());
// before persist = class.java.util.ArrayList
em.persist(team);
System.out.println(&quot;after persist = &quot; + team.getMembers().getClass());
//after persist = class org.hibernate.collection.internal.PersistentBag</code></pre>
<p>팀과 일대다로 연관관계를 가진 members의 타입이 team 엔티티가 영속 상태 이전에는 ArrayList를 가지고 영속 상태 이후에는 하이버네이트가 제고하는 PersistentBag 타입으로 변경된 것을 볼 수 있다. 하이버네이트는 이처럼 영속 상태 이후에 관리를 편리하게 하기 위해 원본 컬렉션을 감싸고 있는 래퍼 컬렉션을 제공한다.
이런 특징 때문에 위처럼 바로 컬렉션을 사용할 때 위처럼 바로 초기화해서 사용하는 것을 권장한다.</p>
<pre><code class="language-java">    private Collection&lt;Member&gt; members = new ArrayList&lt;Member&gt;();</code></pre>
<h2 id="collection-list">Collection, List</h2>
<p>Collection과 List는 중복을 허용하는 컬렉션으로 PersistentBag을 래퍼 컬렉션으로 사용한다. 해당 래퍼 컬렉션은 <strong>순서는 보장하지 않는다.</strong>
이 인터페이스는 ArrayList()로 초기화한다.
Collection, List는 중복을 허용하므로 객체를 추가하는 add() 메서드에서는 어떤 비교도 하지 않고 저장하며 항상 true를 반환한다.
<strong>단순히 저장만 하기 때문에 객체를 추가한다고 해도 지연 로딩된 컬렉션을 초기화하지 않는다.</strong>
같은 엔티티가 있는지 찾거나(contains()), 삭제하는 경우(remove())할 때는 eqauls() 비교를 한다.</p>
<h2 id="set">Set</h2>
<p>중복을 허용하지 않고 순서를 보장하지 않는 컬렉션으로 래퍼 컬렉션으로는 PersistentSet을 이용한다.
이 인터페이스는 HashSet()으로 초기화한다.
Set은 중복을 허용하지 않으므로 add() 메서드로 객체를 추가할 때 equals()를 사용해서 같은 객체가 있는지 비교한다. 같은 객체가 없으면 객체를 추가하고 true를, 같은 객체가 있으면 추가하지 않고 false를 반환한다.
HashSet은 해쉬 알고리즘을 사용하므로 hashcode()도 함께 사용하여 비교한다.</p>
<pre><code class="language-java">Set&lt;Comments&gt; comments = new HashSet&lt;Comment&gt;();
...

boolean result = comments.add(data)    //hashcode + equals 비교
comments.contatins(comment);    //hashcode + equals 비교
comments.remove(comment);        hashcode + equals 비교</code></pre>
<p>Set은 객체를 추가하기 위해서 이미 존재하는지 확인해야 한다. 따라서 엔티티를 추가할 때 <strong>지연 로딩된 컬렉션을 초기화한다.</strong></p>
<h2 id="list--ordercolumn">List + @OrderColumn</h2>
<p>기본적으로 List 컬렉션은 PersistentBag을 래퍼 컬렉션으로 사용한다고 했다. 이 래퍼 컬렉션은 순서를 보장하지 않기 때문에 순서가 필요한 경우 @OrderColumn을 사용해야 한다.
이 경우 하이버네이트는 내부 컬렉션인 PersistentList를 사용한다.</p>
<pre><code class="language-java">@Entity
public class Board {
    ...
    @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 {
    ...
    @ManyToOne
    @JoinColumn(name = &quot;BOARD_ID&quot;)
    private Board board;
      ...
}</code></pre>
<p>comments는 List 인터페이스를 사용하고 @OrderColumn을 사용해서 순서가 있는 컬렉션으로 인식한다.
순서가 있는 컬렉션은 데이터베이스에 순서 값도 함께 관리한다. 이 때, @OrderColumn의 name 속성으로 지정한 &quot;POSITION&quot;으로 관리되며 일대다 관계의 특성상 다쪽인 Comment 테이블에서 관리된다.</p>
<h3 id="ordercolumn의-단점">@OrderColumn의 단점</h3>
<ul>
<li><p>@OrderColumn을 Board 엔티티에서 매핑하므로 Comment는 POSITION의 값을 알 수 없다. 그래서 Comment를 INSERT할 때는 POSITION 값이 저장되지 않는다.</p>
</li>
<li><p>List를 변경하면 연관된 위치 값들을 모두 변경해야 한다. 예를 들어 댓글 2를 삭제하면 댓글3, 댓글4 처럼 뒤에 있는 POSTION들을 1씩 줄이는 update sql이 발생한다.</p>
</li>
<li><p>중간에 POSTION이 없으면 조회한 LIST에는 null이 보관된다. 예를 들어 강제로 comment에서 댓글 2를 삭제하고 다른 POSTION 값들을 수정하지 않으면 데이터 베이스에 POSITION값들은 [0,2,3]이 된다. 
그래서 컬렉션을 조회할 경우 원래 POSTION 1 자리에는 null이기 때문에 NullPointException이 발생한다.</p>
</li>
</ul>
<h2 id="orderby">@OrderBy</h2>
<p>@OrderBy는 데이터베이스의 order by를 잉요해서 컬렉션을 정렬하다. 따라서 정렬용 컬럼이 따로 필요하지 않고 모든 컬렉션에서 사용가능하다.</p>
<pre><code class="language-java">@Entity
public class Team {
    ...
    @OneToMany(mappedBy = &quot;team&quot;)
    @OrderBy(&quot;username desc, id asc&quot;)
    private Set&lt;Member&gt; members = new HashSet&lt;Member&gt;();
    ...
}</code></pre>
<p>위는 @OrderBy를 사용한 예이다. @OrderBy의 값은 JPQL의 order by절처럼 엔티티의 필드를 대상으로 한다.
해당 컬렉션을 초기화하면 SQL에 ORDER BY가 사용된다.
(하이버네이트는 Set에서 @OrderBy를 사용해서 결과를 조회하면 순서 유지를 위해 HashSet대신에 LinkedHashSet을 내부에서 사용함)</p>
<hr>
<h1 id="converter">@Converter</h1>
<p>컨버터를 사용하면 엔티티의 데이터를 변환해서 데이터베이스에 저장할 수 있다.
예를 들어 엔티티의 vip 필드를 자바의 boolean 타입을 이용하면 데이터베이스에는 0 또는 1이 저장된다. 근데 0 또는 1말고 &#39;Y&#39;, &#39;N&#39;이라는 문자를 저장하고 싶을 때 converter를 사용할 수 있다.</p>
<pre><code class="language-java">@Entity
public class Member {
    ...

    @Convert(converter=BooleanToYNConverter.class)
    private boolean vip;
    ...
}</code></pre>
<p>@Converter를 사용해서 데이터베이스에 저장되기 전에 BooleanToYNConverter가 동작하도록 했다.</p>
<pre><code class="language-java">@Converter
public class BooleanToYNConverter implements AttributeConverter&lt;Boolean, String&gt; {

    @Override
    public String convertToDatebaseColumn(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>
<p>컨버터 클래스는 @Converter 어노테이션을 사용하고 AttributeConverter 인터페이스를 구현해야 한다.  제네릭에 현재 타입과 변환할 타입을 지정해야 한다. (여기서는 &lt;Boolean, String&gt;)</p>
<h4 id="attributeconverter-구현-메서드">AttributeConverter 구현 메서드</h4>
<ul>
<li>convertToDatabaseColumn() : 엔티티의 데이터를 데이터베이스 컬럼에 저장할 데이터로 변환한다.</li>
<li>convertToEntityAttribute() : 데이터베이스에서 조회한 컬럼 데이터를 엔티티의 데이터로 변환한다.</li>
</ul>
<p>컨버터는 클래스 레벨에도 설정할 수 있는데 이 때, attributeName 속성으로 어떤 필드에 컨버터를 적용할지 명시해야 한다.</p>
<pre><code class="language-java">@Entity
@Convert(converter=BooleanToYNConverter.class, ttributeName = &quot;vip&quot;)
public class Member {
    ...
    private boolean vip;
    ...
}</code></pre>
<h2 id="글로벌-설정">글로벌 설정</h2>
<p>@Converter의 autoApply = true 옵션을 사용해서 글로벌 설정을 할 수 있다. 예를 들어 위에서 만든 컨버터를 모든 boolean 타입에 대해 적용하려면 다음과 같이 사용할 수 있다.</p>
<pre><code class="language-java">@Converter(autoApply = true)
public class BooleanToYNConverter implements AttributeConverter&lt;Boolean, String&gt; {

    @Override
    public String convertToDatebaseColumn(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>
<hr>
<h1 id="리스너">리스너</h1>
<p>리스너는 특정 이벤트가 발생했을 때 동작하는 콜백 메서드를 제공하여, 해당 이벤트에 대한 특정 동작을 실행하도록 설계된 구성 요소이다. 리스너는 주로 이벤트 기반 프로그래밍에서 사용되며, 애플리케이션에서 <strong>&quot;특정 시점&quot;</strong>에 동작하도록 정의된다.</p>
<p>JPA의 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트(엔티티의 상태 변화 - 저장/수정등)를 처리할 수 있다.</p>
<h2 id="이벤트-종류">이벤트 종류</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/bfdbefc1-f827-4b4e-9718-6c1bd5f05b42/image.png" alt=""></p>
<ol>
<li><strong>PostLoad</strong>
엔티티가 영속성 컨텍스트에 조회된 직후 또는 refresh를 호출한 후 (2차 캐시에 저장되어 있어도 호출)</li>
<li><strong>PrePersist</strong>
persist() 메서드를 호출해서 엔티티를 영속성 컨텍스트에 관리하기 직전에 호출된다. 식별자 생성 전략을 사용한 경우 엔티티에 식별자는 아직 존재하지 않는다. 간략하게 설명하면 [persist() -&gt; PrePersist -&gt; db와 상호작용하여 식별자 가져옴] 이 순서이기 때문에 식별자 생성 전략을 사용하면 식별자와 관련된 로깅 시 문제가 될 수 있다는 말이다.
새로운 인스턴스를 merge할 때도 수행된다.</li>
<li><strong>PreUpdate</strong>
flush나 commit을 호출해서 엔티티를 데이터베이스에 수정하기 직전에 호출된다.</li>
<li><strong>PreRemove</strong>
remove() 메서드를 호출해서 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출된다. 또한 삭제 명령어로 영속성 전이가 일어날 때도 호출된다. orphanRemoval에 대해서는 flush나 commit 시에 호출된다.</li>
<li><strong>PostPersist</strong>
flush나 commit을 호출해서 엔티티를 데이터베이스에 저장한 직후에 호출된다. 식별자가 항상 존재한다.
식별자 생성 전략이 IDENTITY면 식별자를 생성하기 위해 persist()를 호출하면서 데이터베이스에 해당 엔티티를 저장하므로 이때는 persist()를 호출한 직후에 바로 PostPersist가 호출된다.</li>
<li><strong>PostUpdate</strong>
flush나 commit을 호출해서 엔티티를 데이터베이스에 수정한 직후에 호출된다.</li>
<li><strong>PostRemove</strong>
flush나 commit을 호출해서 엔티티를 데이터베이스에 삭제한 직후에 호출된다.</li>
</ol>
<h3 id="jpa-리스너"><strong>JPA 리스너</strong></h3>
<h4 id="정의">정의:</h4>
<p>JPA 리스너는 <strong>엔티티 객체의 생명주기</strong> 동안 발생하는 이벤트(예: 엔티티 생성, 수정, 삭제 등)에 반응하여 동작합니다. 이는 JPA의 <strong>엔티티 리스너(Entity Listener)</strong> 및 <strong>콜백 메서드</strong>를 통해 구현됩니다.</p>
<h4 id="주요-특징">주요 특징:</h4>
<ul>
<li><strong>생명주기 이벤트 처리:</strong> JPA 엔티티에서 발생하는 이벤트를 캡처하여, 추가 로직(예: 데이터 감사, 로깅)을 수행할 수 있습니다.</li>
<li><strong>어노테이션 기반:</strong> JPA 리스너는 특정 이벤트를 처리하기 위해 @PrePersist, @PostPersist, @PreUpdate, @PostUpdate 등 어노테이션을 사용합니다.</li>
<li><strong>독립적인 리스너 클래스:</strong> 리스너 로직을 엔티티 외부의 독립적인 클래스에 작성할 수 있습니다.</li>
</ul>
<h4 id="생명주기-이벤트">생명주기 이벤트:</h4>
<table>
<thead>
<tr>
<th>이벤트</th>
<th>어노테이션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>PrePersist</td>
<td>@PrePersist</td>
<td>엔티티가 저장되기 전에 실행</td>
</tr>
<tr>
<td>PostPersist</td>
<td>@PostPersist</td>
<td>엔티티가 저장된 후 실행</td>
</tr>
<tr>
<td>PreUpdate</td>
<td>@PreUpdate</td>
<td>엔티티가 수정되기 전에 실행</td>
</tr>
<tr>
<td>PostUpdate</td>
<td>@PostUpdate</td>
<td>엔티티가 수정된 후 실행</td>
</tr>
<tr>
<td>PreRemove</td>
<td>@PreRemove</td>
<td>엔티티가 삭제되기 전에 실행</td>
</tr>
<tr>
<td>PostRemove</td>
<td>@PostRemove</td>
<td>엔티티가 삭제된 후 실행</td>
</tr>
<tr>
<td>PostLoad</td>
<td>@PostLoad</td>
<td>엔티티가 로드된 후 실행</td>
</tr>
</tbody></table>
<h2 id="이벤트-적용-위치">이벤트 적용 위치</h2>
<ul>
<li>엔티티에 직접 적용</li>
<li>별도의 리스너 등록</li>
<li>기본 리스너 사용</li>
</ul>
<h3 id="엔티티에-직접-적용">엔티티에 직접 적용</h3>
<pre><code class="language-java">@Entity
public class Duck {
    @Id @GeneratedValue
    public Long id;
    ...

    @PrePersist
    public void prePersist() {
        System.out.prinln(&quot;Duck.prePersist id=&quot; + id);
    }
    ...
}    </code></pre>
<p>위에서 설명한 어노테이션을 이용해서 필요에 따라 엔티티에 직접 적용할 수 있다.</p>
<h3 id="별도의-리스너-등록">별도의 리스너 등록</h3>
<p><strong>1. 엔티티 리스너 정의</strong></p>
<pre><code class="language-java">public class AuditListener {

    @PrePersist
    public void beforePersist(Object entity) {
        System.out.println(&quot;Before persisting: &quot; + entity);
    }

    @PostPersist
    public void afterPersist(Object entity) {
        System.out.println(&quot;After persisting: &quot; + entity);
    }
}</code></pre>
<p><strong>2. 엔티티에 리스너 연결</strong></p>
<pre><code class="language-java">@Entity
@EntityListeners(AuditListener.class)
public class User {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    // Getters and setters...
}</code></pre>
<p>리스너는 대상 엔티티를 파라미터로 받을 수 있다. 반환 타입은 void로 설정해야 한다.</p>
<h3 id="기본-리스너-사용">기본 리스너 사용</h3>
<p>모든 엔티티의 이벤트를 처리하려면 META-INF/orm.xml에 기본 리스너로 등록하면 된다.</p>
<h4 id="이벤트-호출-순서">이벤트 호출 순서</h4>
<ol>
<li>기본 리스너</li>
<li>부모 클래스 리스너</li>
<li>리스너</li>
<li>엔티티</li>
</ol>
<h3 id="더-세빌한-설정">더 세빌한 설정</h3>
<ul>
<li>javax.persistence.ExcludeDefaultListeners : 기본 리스너 무시</li>
<li>javax.persistence.ExcludeSuperclassListeners: 상위 클래스 이벤트 리스너 무시</li>
</ul>
<hr>
<h1 id="엔티티-그래프">엔티티 그래프</h1>
<p>엔티티를 조회할 때 연관된 엔티티까지 조회하기 위해서 글로벌 fetch 옵션을 FetchType.EAGER 설정할 수 있다. 그러나 이 방법은 성능 저하를 가져올 수 있어 보통 FetcyType.LAZY를 사용하고 필요한 경우에만 JPQL 페치 조인을 사용한다.
그러나 이 방법도 경우에 따라서 함께 조회하는 엔티티가 무엇이냐에 따라서 JPQL을 여러개 작성해야 한다는 단점이 있다.
이는 이 방법이 JPQL이 데이터를 조회하는 기능뿐만 아니라 연관된 엔티티를 함께 조회하기 때문에 발생하는 문제이다. </p>
<p>JPA2.1에 추가된 엔티티 그래프 기능을 사용하면 엔티티를 조회하는 시점에 함께 조회할 연관된 엔티티를 선택할 수 있다. 따라서 JPQL은 데이터를 조회하는 기능만 하고 연관된 엔티티를 함께 조회하는 기능은 엔티티 그래프를 사용하면 된다. 
<strong>엔티티 그래프 기능</strong>은 엔티티 조회시점에 연관된 엔티티들을 함께 조회하는 기능이다.</p>
<h4 id="엔티티-그래프-예제-모델">엔티티 그래프 예제 모델</h4>
<p><img src="https://velog.velcdn.com/images/now_here/post/b9ecb775-395b-4ead-ba1b-f1c4881b104c/image.png" alt=""></p>
<h2 id="named-엔티티-그래프">Named 엔티티 그래프</h2>
<ul>
<li>엔티티 클래스에 어노테이션을 사용하여 정적으로 정의.</li>
</ul>
<p>주문(Order)을 조회할 때 연관된 회원(Member)도 함께 조회하는 엔티티 그래프를 사용하는 예제.</p>
<pre><code class="language-java">@NamedEntityGraph(
    name = &quot;Order.withMember&quot;,  // 엔티티 그래프의 이름
    attributeNodes = @NamedAttributeNode(&quot;member&quot;)  // 연관된 Member 엔티티를 명시
)
@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;    //주문 회원

    // other fields and methods...
}</code></pre>
<p>@NamedEntityGraph 어노테이션을 이용해서 Named 엔티티 그래프를 정의한다.</p>
<ul>
<li>name : 엔티티 그래프의 이름을 정의</li>
<li>attributeNodes : 함께 조회한 속성 선택. @NamedAttributeNode 사용</li>
</ul>
<p>member가 지연로딩으로 설정되어 있어도 엔티티 그래프로 인해서 Order를 조회할 때 연관된 member도 함께 조회할 수 있다.</p>
<h2 id="emfind-에서-엔티티-그래프-사용">em.find() 에서 엔티티 그래프 사용</h2>
<pre><code class="language-java">//em.find() 에서 엔티티 그래프 사용
EntityGraph graph = em.getEntityGraph(&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>
<p>em.getEntityGraph()로 정의한 엔티티 그래프를 찾아온다. 엔티티 그래프는 JPA의 힌트 기능을 이용해서 동작하므로 javax.persistence.fetchgraph를 사용하고 힌트의 값으로 찾아온 엔티티 그래프를 사용하면 된다.</p>
<pre><code class="language-sql">//실행된 SQL
select o.*, m.*
from ORDERS o
inner join Member m
    on o.MEMBER_ID=m.MEMBER_ID
where
    o.ORDER_ID=?</code></pre>
<h2 id="subgraph">subgraph</h2>
<p>subgraph는 연관된 엔티티의 서브 그래프를 명시적으로 정의해서 사용하는 방법이다.
예를 들어 Order -&gt; OrderItem -&gt; Item 까지 조회하는 경우라고 하자. OrderItem은 Order가 관리하는 필드가 맞지만 Item은 OrderItem이 관리하는 필드이다. 이 경우 subgraph를 사용한다.</p>
<pre><code class="language-java">@NamedEntityGraph(name = &quot;Order.withAll&quot;, attributeNodes = {
    @NamedAttributeNode(&quot;member&quot;),
    @NamedAttributeNode(value = &quot;orderItems&quot;, subgraph = &quot;orderItems&quot;)},
    subgraphs = @NamedSubgraph(name = &quot;orderItems&quot;, attributesNodes = {
        @NamedAttributeNode(&quot;item&quot;)})
@Entity
@Table (name = &quot;ORDERS&quot;)
public class Order {
...
}</code></pre>
<p>여기서 item은 Order의 객체 그래프가 아니므로 subgraphs 속성으로 정의했다. @NamedSubgraph를 사용해서 서브 그래프를 정의하고 orderItems라는 이름의 서브 그래프가 item을 조회하도록 정의했다.</p>
<h3 id="사용코드">사용코드</h3>
<pre><code class="language-java">Map hints = new HashMap();
hints.put(&quot;javax.persistence.fetchgraph&quot;, em.getEntityGraph(&quot;Order.withAll&quot;));

Order order = em.find(Order.class, orderId, hints);</code></pre>
<h3 id="실행-sql">실행 SQL</h3>
<pre><code class="language-sql">select o.*, m.*, oi.*, i.*
from ORDERS o
inner join Member m
    on o.Member_ID=m.MEMBER_ID
left outer join
    ORDER_ITEM oi
        on o.ORDER_ID = oi.ORDER_ID
left outer join
    ITEM i
        on oi.ITEM_ID=i.ITEM_ID
where
    o.ORDER_ID=?</code></pre>
<h2 id="jpql에서-엔티티-그래프-사용">JPQL에서 엔티티 그래프 사용</h2>
<p>em.find()에서처럼 힌트를 추가하면 JPQL에서도 엔티티 그래프를 사용할 수 있다.</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>
<h2 id="동적-엔티티-그래프">동적 엔티티 그래프</h2>
<p>엔티티 그래프를 동적으로 구성하려면 createEntityGraph() 메서드를 사용하면 된다.</p>
<pre><code class="language-java">public &lt;T&gt; EntityGraph&lt;T&gt; createEntityGraph(Class&lt;T&gt; rootType);

// 예제
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.put(&quot;javax.persistence.fetchgraph&quot;, graph);

Order order = em.find(Order.class, orderId, hints);</code></pre>
<p>해당 메서드로 동적 그래프(graph)를 만들고 graph.addAttributeNodes(&quot;member&quot;)를 사용해서 Order.member 속성을 엔티티 그래프에 포함시켰다.
또한 graph.addSubgraph(&quot;orderItems&quot;)를 통하여 subgraph 기능도 동적으로 구성하였다.</p>
<h2 id="엔티티-그래프-정리">엔티티 그래프 정리</h2>
<h3 id="root에서-시작">ROOT에서 시작</h3>
<p>엔티티 그래프를 시작하려면 조회하려는 엔티티의 ROOT부터 시작해야 한다. 즉, Order에서 Member의 엔티티의 그래프를 구성하려면 Order 엔티티부터 조회해야 한다.</p>
<h3 id="이미-로딩된-엔티티">이미 로딩된 엔티티</h3>
<p>이미 영속성 컨텍스트에 엔티티가 로딩되어 있으면 엔티티 그래프가 적용되지 않는다. 아직 초기화 되지 않은 프록시에는 엔티티 그래프가 적용된다.</p>
<pre><code class="language-java">Orer 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는 orderId로 조회가 이루어져 있어 엔티티 그래프가 적용되지 않고 처음 조회한 order2와 같은 인스턴스를 반환한다.</p>
<pre><code class="language-java">em.clear(); // 영속성 컨텍스트 초기화
Order order2 = em.find(Order.class, orderId, hints); // 엔티티 그래프 적용 가능</code></pre>
<p>해결방법으로 위처럼 영속성 컨텍스트를 초기화하고 엔티티 그래프 적용하는 방법이 있다.</p>
<h3 id="fetchgraph-loadgraph의-차이">fetchgraph, loadgraph의 차이</h3>
<p>javax.persistence.fetchgraph 힌트를 사용해서 엔티티 그래프를 조회하는 것은 엔티티 그래프에 선택한 속성만 함께 조회한다. 
반면에 javax.persistence.loadgraph 속성은 엔티티 그래프에 선택한 속성뿐만 아니라 글로벌 fetch 모드가 FetchType.EAGER로 설정된 연관관계도 포함해서 함께 조회한다.
즉, fetchgraph는 fetch 모드가 EAGER이여도 무시하고 가져오라고 설정한 속성만 가져오는데 반해 loadgraph는 가져오라고 설정한 속성뿐만 아니라 fetch모드가 EAGER인 속성도 가져와 더 많은 범위의 데이터를 반환한다.</p>
<blockquote>
<p>참고 : 하이버네이트 4.3.10.Final 버전에서는 loadgraph 기능이 em.find()를 사용할 때 정상동작하지만 JPQL을 사용할 때는 정상동작하지 않고 fetchgraph와 같은 방식으로 동작한다.</p>
</blockquote>
<blockquote>
<p>참고의 참고 : 이러한 문제는 이후 버전에서 수정되었으며, 최신 Hibernate 버전에서는 loadgraph 힌트가 JPQL 쿼리에서도 의도한 대로 동작합니다. 그러나 여전히 특정 상황에서 엔티티 그래프와 관련된 문제가 발생할 수 있으므로, 사용 중인 Hibernate 버전의 릴리스 노트를 확인하고, 최신 버전을 사용하는 것이 좋습니다.
예를 들어, Hibernate 6.6.x 버전에서 엔티티 그래프 사용 시 오류가 발생한다는 보고가 있습니다.</p>
</blockquote>
<p>참조 : [자바 ORM 표준 JPA 프로그래밍]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[13. 웹 애플리케이션과 영속성 관리]]></title>
            <link>https://velog.io/@now_here/13.-%EC%9B%B9-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98%EA%B3%BC-%EC%98%81%EC%86%8D%EC%84%B1-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@now_here/13.-%EC%9B%B9-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98%EA%B3%BC-%EC%98%81%EC%86%8D%EC%84%B1-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Sun, 19 Jan 2025 14:27:46 GMT</pubDate>
            <description><![CDATA[<p>스프링이나 J2EE 컨테이너 환경에서 JPA를 사용하면 컨테이너가 트랜잭션과 영속성 컨텍스트를 관리해주므로 쉽게 애플리케이션을 개발할 수 있다. 그러나 정확한 동작방법을 모르면 문제가 발생했을 때 해결하기가 쉽지 않으므로 동작원리를 살펴보자.</p>
<h1 id="트랜잭션-범위의-영속성-컨텍스트">트랜잭션 범위의 영속성 컨텍스트</h1>
<h2 id="스프링-컨테이너의-기본-전략">스프링 컨테이너의 기본 전략</h2>
<p>스프링 컨테이너는 <strong>트랜잭션 범위의 영속성 컨텍스트 전략</strong>을 기본으로 사용한다.
이 전략은 트랜잭션 범위와 영속성 컨텍스트의 생존 범위가 같다는 뜻이다. 즉, 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝났을 때 영속성 컨텍스트를 종료한다. 그리고 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근한다.
스프링 프레임워크를 사용하면 보통 비즈니스 로직을 시작하는 서비스 계층에 @Transactional 을 사용해서 트랜잭션을 시작한다. 외부에서는 단순히 서비스 계층의 메서드를 호출하는 것처럼 보이지만 이 어노테이션이 있으면 호출한 메서드를 실행하기 직전에 스프링의 트랜잭션 AOP가 먼저 동작한다.</p>
<p><img src="https://velog.velcdn.com/images/now_here/post/053559d9-793a-4007-a651-f5f4a8aa2338/image.png" alt="">
스프링 트랜잭션 AOP는 대상 메서드를 호출하기 직전에 트랜잭션을 시작하고 메서드가 정상 종료되면 해당 트랜잭션을 커밋하면서 종료한다. 이 때, 트랜잭션을 커밋하면 JPA는 먼저 영속성 컨텍스트를 플러시해서 변경 내용을 DB에 반영하고 DB 트랜잭션을 커밋한다. 만약 예외가 발생하면 트랜잭션을 롤백하고 종료하는데 이때는 플러시를 호출하지 않는다.</p>
<h2 id="트랜잭션이-같으면-같은-영속성-컨텍스트를-사용한다">트랜잭션이 같으면 같은 영속성 컨텍스트를 사용한다.</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/b67072fb-9f47-483f-b614-5dac22138ef8/image.png" alt="">
<strong>트랜잭션 범위의 영속성 컨텐스트 전략</strong>은 다양한 위치에서 엔티티 매니저를 주입받아도 트랜잭션이 같으면 항상 같은 영속성 컨텍스트를 사용한다. </p>
<h2 id="트랜잭션이-다르면-다른-영속성-컨텍스트를-사용한다">트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다.</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/c8f2c551-bfc0-450c-ae7f-8fcc0fe260bd/image.png" alt=""></p>
<p>여러 스레드에서 동시에 요청이 와서 <strong>같은 엔티티 매니저를 사용해도 트랜잭션에 따라 접근하는 영속성 컨텍스트가 다르다.</strong>
스프링 컨테이너는 스레드마다 각각 다른 트랜잭션을 할당한다. 즉, 같은 엔티티 매니저를 호출해도 접근하는 영속성 컨텍스가 다르므로 멀티스레드 상황에 안전하다.</p>
<h1 id="준영속-상태와-지연-로딩">준영속 상태와 지연 로딩</h1>
<p>앞서 설명했듯이 보통 서비스 계층에서 트랜잭션을 시작하고 종료하므로 영속성 컨텍스트도 서비스 계층에서 종료된다. 따라서 조회한 엔티티는 서비스 계층에서는 영속성 컨텍스트에서 관리되지만 컨트롤러나 뷰 같은 프리젠테이션 계층에서는 준영속 상태가 된다.</p>
<p>예를 들어 뷰를 렌더링할 때 연관된 엔티티도 함께 사용해야 하는데 연관된 엔티티를 <strong>지연 로딩</strong>으로 설정해서 프록시 객체로 조히했다고 가정하자. 아직 초기화하지 않은 프록시 객체를 사용하면 실제 데이터를 불러오려고 초기화를 시도한다. 하지만 준영속 상태는 영속성 컨텍스트가 없으므로 지연로딩을 할 수 없어 문제가 발생한다.</p>
<p>이럴 때 해결하는 방법은 크게 2가지가 있다.</p>
<ul>
<li>뷰가 필요한 엔티티를 미리 로딩해두는 방법</li>
<li>OSIV를 사용해서 엔티티를 항상 영속 상태로 유지하는 방법</li>
</ul>
<p>먼저 뷰가 필요한 엔티티를 미리 로딩해두는 3가지 방법을 살펴보자.</p>
<ul>
<li>글로벌 페치 전략 수정</li>
<li>JPQL 페치 조인</li>
<li>강제로 초기화</li>
</ul>
<h2 id="글로벌-페치-전략-수정">글로벌 페치 전략 수정</h2>
<p>가장 간단한 방법으로 엔티티의 글로벌 페치 전략을 지연 로딩에서 즉시 로딩으로 변경하는 것이다.</p>
<pre><code class="language-java">@Entity
public class Order {

    @Id @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.EAGER)    //즉시 로딩 전략
       private Member member; // 주문 회원
    ...
}

//프레젠테이션 로직
Order order = orderService.findOne(orderId);
Member memer = order.getMember();
member.getName(); // 이미 로딩된 엔티티</code></pre>
<h3 id="글로벌-페치-전략에-즉시-로딩-사용-시-단점">글로벌 페치 전략에 즉시 로딩 사용 시 단점</h3>
<ul>
<li>사용하지 않는 엔티티를 로딩한다.
다른 화면에서 order만 필요해도 member가 조회되어 필요없는 엔티티도 로딩하는 일이 벌어진다.</li>
<li>N+1 문제
엔티티를 조회할 때 연관된 엔티티를 로딩하는 전략이 즉시 로딩이면 데이터베이스에 JOIN 쿼리를 사용해서 한 번에 연관된 엔티티까지 조회한다. 이 때 처음 조회한 데이터 수만큼 다시 SQL을 사용해서 조회하는 것을 N+1 문제라고 한다. (N+1 문제는 JPQL 페치 조인으로 해결할 수 있다.)</li>
</ul>
<h2 id="jpql-페치-조인">JPQL 페치 조인</h2>
<p>JPQL을 호출하는 시점에 함께 로딩할 엔티티를 선택할 수 있는 페치 조인을 알아보자.</p>
<pre><code class="language-sql">//JPQL
select o
from Order o
join fetch o.member

//SQL
select o.*, m.*
from Order o
join Member m on o.MEMBER_ID=m.MEMBER_ID</code></pre>
<p>페치 조인은 조인 명령어 마지막에 fetch를 추가하면 된다. 이렇게 페치 조인을 사용하면 SQL JOIN을 사용해서 페치 조인 대상까지 조회한다. 연관된 엔티티를 이미 로딩했으므로 N+1 문제가 발생하지 않는다.</p>
<h3 id="jpql-페치-조인의-단점">JPQL 페치 조인의 단점</h3>
<p>가장 현실적인 대안이지만 화면에 맞춘 리포지토리 메서드가 증가할 수 있다는 단점이 있다. 예를 들어, 화면 A에서는 order엔티티만 필요하고 화면 B에서는 order, member 엔티티가 필요하다고 하면 엔티티를 모두 지연 로딩으로 설정하고 order만 조회하는 메서드, order와 연관된 member를 페치 조인으로 조회하는 메서드 2가지가 필요하다. 이는 최적화를 했다고 할 수 있지만 뷰와 리포지토리 간에 논리적인 의존관계가 발생한다는 문제점이 있다.</p>
<h2 id="강제로-초기화">강제로 초기화</h2>
<p>영속성 컨텍스트가 살아있을 때 프리젠테이션 계층이 필요한 엔티티를 강제로 초기화해서 반환하는 방법이다.</p>
<pre><code class="language-java">//프록시 강제 초기화 - 글로벌 페치 전략 지연 로딩이라고 가정
class OrderService {
    @Transactional
    public Order findOrder(id) {
        Order order = orderRepository.findOrder(id);
        order.getMember().getName();    // 프록시 강제 초기화
        return order;
    }
}</code></pre>
<p>프록시 객체는 실제 사용하기 전까지 초기화되지 않는다. 그래서 위처럼 필요한 데이터를 order.getMember().getName()을 이용하여 미리 초기화를 해두는 방법이 프록시 강제 초기화이다. 이렇게 강제 초기화를 하면 준영속 상태가 되어도 사용할 수 있다.
그러나 이처럼 뷰에서 필요한 데이터를 서비스 계층이 담당하게 되면 프레젠테이션 계층이 서비스 계층을 침범하는 상황이 벌어지게 된다. 서비스 계층은 비즈니스 로직만을 담당하는 것이 좋으며 프레젠테이션 계층을 위한 프록시 객체 초기화는 분리하는 것이 좋다. 분리하기 위해 FACADE 계층이 그 역할을 담당해 줄 것이다.</p>
<h2 id="facade-계층-추가">FACADE 계층 추가</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/fdf79f86-7a12-43a9-9f9b-f4e180a1b473/image.png" alt="">
프레젠테이션 계층과 서비스 계층 사이에 FACADE 계층을 하나 더 두고 여기에서 프록시 초기화를 담당하게 둔다. 프록시를 초기화하려면 영속성 컨텍스트가 필요하므로 FACADE에서 트랜잭션을 시작해야 한다.</p>
<h3 id="facade-계층의-역할과-특징">FACADE 계층의 역할과 특징</h3>
<ul>
<li>프레젠테이션 계층과 도메인 모델 계층 간의 논리적 의존성을 분리한다.</li>
<li>프레젠테이션 계층에서 필요한 프록시 객체를 초기화한다.</li>
<li>서비스 계층을 호출해서 비즈니스 로직을 실행한다.</li>
<li>리포지토리를 직접 호출해서 뷰가 요구하는 엔티티를 찾는다.</li>
</ul>
<pre><code class="language-java">//FACADE 계층 추가
class OrderFacade {
    @Autowired OrdeerService orderService;

    @Transactional
    public Order findOrder(id) {
        Order order = orderService.findOrder(id);
        //프리젠테이션 계층이 필요한 프록시 객체를 강제로 초기화한다.
        order.getMember().getName();
        return order;
    }
}

class OrderService {

    public Order findOrder(id) {
        return orderRepository.findOrder(id);    
    }
}</code></pre>
<p>FACADE 계층을 만들면서 하나의 계층이 더 추가되었다는 단점과 단순히 서비스 계층을 호출만 하는 위임 코드가 상당히 많을 것이라는 단점이 존재한다.</p>
<h3 id="준영속-상태와-지연-로딩의-문제점">준영속 상태와 지연 로딩의 문제점</h3>
<p>준영속 상태와 지연 로딩 문제를 극복하기 위해 여러 가지 방법을 살펴보았는데 결국 모든 문제는 <strong>엔티티가 프리젠테이션 계층에서 준영속 상태이기 때문에 발생한다.</strong> 영속성 컨텍스트를 뷰까지 살아있게 열어두어 뷰에서도 지연 로딩을 사용할 수 있게 하자. 이것을 OSIV라고 한다.</p>
<hr>
<h1 id="osiv-open-session-in-view">OSIV (Open Session In View)</h1>
<p>OSIV는 영속성 컨텍스트를 뷰까지 열어둔다는 뜻이다. 영속성 컨텍스트가 살아있으면 엔티티는 계속해서 영속 상태로 유지되고 뷰에서도 지연 로딩을 사용할 수 있다.</p>
<h2 id="과거-osiv--요청-당-트랜잭션">과거 OSIV : 요청 당 트랜잭션</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/ff8819d5-a87e-48ff-9841-3dbc3e5b93ef/image.png" alt=""></p>
<p>클라이언트에 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션을 종료하는 방식을 <strong>요청 당 트랜잭션 방식의 OSIV</strong>라고 한다.
영속성 컨텍스트가 처음부터 끝까지 살아있으므로 뷰에서도 지연 로딩을 할 수 있으므로 엔티티를 미리 초기화 할 필요가 없다.</p>
<h3 id="요청-당-트랜잭션-방식의-osiv-문제점">요청 당 트랜잭션 방식의 OSIV 문제점</h3>
<p>해당 방법의 문제점은 컨트롤러나 뷰 같은 프리젠테이션 계층이 엔티티를 변경할 수 있다는 점이다. </p>
<p>예를 들어 보안 상의 이유로 고객의 이름을 &quot;XXX&quot;로 렌더링할 뷰로 넘겨주었다고 하자. 트랜잭션이 프리젠테이션계층에서도 살아있으므로 뷰를 렌더링한 후 트랜잭션이 커밋된다. 즉, 영속성 컨텍스트가 플러시 되고 변경 감지 기능이 동작되어 데이터베이스의 고객 이름이 &quot;XXX&quot;로 변경되는 심각한 문제가 발생한다.
이런 문제를 해결하기 위해서 프리젠테이션 계층에서 엔티티 수정을 막는 3가지 방법이 존재한다.</p>
<ul>
<li>엔티티를 읽기 전용 인터페이스로 제공</li>
<li>엔티티 래핑</li>
<li>DTO만 반환</li>
</ul>
<h3 id="엔티티를-읽기-전용-인터페이스로-제공">엔티티를 읽기 전용 인터페이스로 제공</h3>
<pre><code class="language-java">interface MemberView {
    public String getName();
}

@Entity
class Member implements MemberView {
    ...
}

class MemberService {

    public MemberView getMember(id) {
        return memberRepository.findById(id);
    }
}</code></pre>
<p>실제 엔티티가 존재하지만 서비스 계층에서 읽기 전용 메서드만 있는 MemberView 인터페이스를 반환해서 프리젠테이션 계층에서 엔티티를 수정할 . 수없다.</p>
<h3 id="엔티티-래핑">엔티티 래핑</h3>
<p>읽기 전용 인터페이스를 제공한 것과 마찬가지로 읽기 전용 메서드만 가지고 있는 엔티티를 감싼 객체를 만들고 이것을 프리젠테이션 계층에 반환하는 방법이다.</p>
<h3 id="dtodata-transfer-object-데이터-전송-객체만-반환">DTO(Data Transfer Object, 데이터 전송 객체)만 반환</h3>
<p>가장 전통적인 방법으로 DTO를 생성해서 반환하는 방법이 있다. 하지만 이 방법은 OSIV를 사용하는 장점을 살릴 수 없고 엔티티를 거의 복사한 듯한 DTO 클래스도 하나 더 만들어야 한다.
(DTO는 OSIV와 관계없이 독립적인 데이터 객체로 설계되기 때문에, 뷰 계층에서의 데이터 로딩 및 추가 조회를 지원하지 않기 때문에 DTO는 OSIV가 제공하는 장점(엔티티의 지연 로딩 활용)을 살릴 수 없음)</p>
<p>이런 여러 문제 점 때문에 최근에는 <strong>비즈니스 계층에서만 트랜잭션을 유지하는 방식의 OSIV</strong>를 사용한다.
스프링 프레임워크가 제공하는 OSIV가 바로 이 방식을 사용한다.</p>
<h2 id="스프링-osiv--비즈니스-계층-트랜잭션">스프링 OSIV : 비즈니스 계층 트랜잭션</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/fded7596-7132-41c2-9060-b2de200f3d3b/image.png" alt=""></p>
<p>스프링 프레임워크가 제공하는 OSIV는 &quot;비즈니스 계층에서 트랜잭션을 사용하는 OSIV&quot;이다.</p>
<p>동작 원리는 다음과 같다.
클라이언트 요청이 들어오면 서블릿 필터나, 스프링 인터셉터에서 영속성 컨텍스트를 생성한다. 이 때 트랜잭션은 시작하지 않는다.
서비스 계층에서 트랜잭션을 시작하면 생성한 영속성 컨텍스트에 트랜잭션을 시작한다.
비즈니스 로직을 실행하고 서비스 계층이 끝나면 트랜잭션을 커밋하면서 영속성 컨텍스트를 플러시한다.
이 때, 트랜잭션만 종료하고 영속성 컨텍스트는 살려둔다.
이 후 서블릿 필터나, 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료한다.</p>
<h3 id="트랜잭션-없이-읽기-nontransactional-reads">트랜잭션 없이 읽기 Nontransactional reads</h3>
<ul>
<li>영속성 컨텍스트는 트랜잭션 범위 안에서 엔티티를 조회하고 수정할 수 있다.</li>
<li>영속성 컨텍스트는 트랜잭션 범위 밖에서 엔티티를 조회만 할 수 있다. 프록시를 초기화하는 지연로딩도 조회 기능이다. 이것을 <strong>트랜잭션 없이 읽기</strong>라고 한다.</li>
</ul>
<p>스프링이 제공하는 OSIV는 프리젠테이션 계층에서는 트랜잭션이 없으므로 엔티티를 수정할 수 없지만 트랜잭션 읽기를 사용해서 프리젠테이션 계층에서 지연 로딩 기능을 사용할 수 있다.</p>
<p>또한 프리젠테이션 계층에서 em.flush()를 호출해서 강제로 플러시를 하려고 해도 트랜잭션 범위 밖이므로 데이터를 수정할 수 없다는 예외를 만난다.</p>
<h3 id="스프링-osiv-주의사항">스프링 OSIV 주의사항</h3>
<p>컨트롤러에서 데이터를 변경하고 비즈니스 로직을 실행하는 경우 문제가 생길 수 있다. 아래 예시를 보자.</p>
<pre><code class="language-java">class MemberController {
    public String viewMember(Long id) {
        Member member = memberSevice.getMember(id);
        member.setName(&quot;XXX&quot;);    //보안상의 이유로 고객 이름 가리기

        memberService.biz();    //비즈니스 로직
        return &quot;view&quot;;
    }
}

class MemberService {

    @Transactional
    public void biz() {
        // ..비즈니스 로직 실행
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/now_here/post/ca0a200a-dee2-4bca-9958-99f5db9989de/image.png" alt=""></p>
<ol>
<li>컨트롤러에서 회원 엔티티를 조회하고 이름을 &quot;XXX&quot;로 수정했다.</li>
<li>이 후 biz() 메서드를 실행하여 트랜잭션이 있는 비즈니스 로직을 실행.</li>
<li>트랜잭션 AOP가 동작하면서 영속성 컨텍스트에 트랜잭션을 시작한다. 그리고 biz() 메서드를 실행한다.</li>
<li>biz() 메서드가 끝나면 트랜잭션 AOP는 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시한다. 이때 변경 감지가 동작하면서 회원 엔티티의 수정 사항을 데이터베이스에 반영한다.</li>
</ol>
<p>이는 컨트롤러에서 엔티티를 수정하고 바로 뷰를 호출하는 것이 아니라 수정 수 비즈니스 로직을 실행하기 때문에 일어나는 문제이다. 이 문제를 해결하기 위해서는 간단하게 비즈니스 로직을 먼저 호출하고 마지막에 엔티티를 수정, 뷰를 호출하면 된다.</p>
<pre><code class="language-java">// 수정 로직 - 비즈니스 로직을 먼저 수행
memberService.biz();    //비즈니스 로직 먼저 실행

Member member = memberService.getMember(id);
member.setName(&quot;XXX&quot;);    // 마지막에 엔티티 수정하기</code></pre>
<p>스프링 OSIV는 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있으므로 이런 문제가 발생한다. OSIV를 사용하지 않는 트랜잭션 범위의 영속성 컨텍스트 전략은 트랜잭션의 생명주기와 영속성 컨텍스트의 생명주기가 같으므로 이런 문제가 일어나지 않는다.</p>
<h2 id="osiv-정리">OSIV 정리</h2>
<h3 id="스프링-osiv-특징">스프링 OSIV 특징</h3>
<ul>
<li>스프링 OSIV는 클라이언트의 요청이 들어오면 영속성 엔티티를 생성해서 요청이 끝날 때까지 같은 영속성 컨텍스트를 유지한다. 그래서 한 번 조회한 엔티티는 요청이 끝날 때까지 영속 상태를 유지한다.</li>
<li>엔티티 수정은 트랜잭션이 살아있는 비즈니스 계층에서만 가능하다. 프리젠테이션 계층에서는 지연 로딩을 포함한 조회만 할 수 있다.</li>
</ul>
<h3 id="스프링-osiv-단점">스프링 OSIV 단점</h3>
<ul>
<li>OSIV를 적용하면 같은 영속성 컨텍스트를 여러 트랜잭션이 공유하는 것을 주의해야 한다. 특히 트랜잭션 롤백을 주의해야 하는데 이는 15장에서 더 자세히 살펴본다.</li>
<li>프리젠테이션 계층에서 먼저 엔티티를 수정하고 비즈니스 로직을 수행하면 엔티티가 수정될 수 있다.</li>
<li>프리젠테이션 계층에서 지연 로딩에 의한 SQL이 실행된다. 따라서 성능 튜닝시에 확인해야 할 부분이 넓다.</li>
</ul>
<h3 id="osiv는-만능이-아니다">OSIV는 만능이 아니다.</h3>
<p>OSIV를 사용하면 화면을 출력할 때 엔티티를 유지하면서 객체 그래프를 마음껏 탐색할 수 있다. 그러나 처음부터 JPQL로 필요한 데이터들만 조회해서 DTO로 반환하는 것이 더 나은 해결책일 수 있다.</p>
<h3 id="osiv는-같은-jvm을-벗어난-원격-상황에서는-사용할-수-없다">OSIV는 같은 JVM을 벗어난 원격 상황에서는 사용할 수 없다.</h3>
<p>예를 들어 JSON이나 XML을 생성할 때는 지연로딩을 사용할 수 있지만 원격지인 클라이언트에서 연관된 엔티티를 지연 로딩하는 것은 불가능하다. 결국 클라이언트가 필요한 데이터를 모두 JSON으로 생성해서 반환해야 한다. 보통 Jackson이나 Gson 같은 라이브러리를 사용해서 객체를 JSON으로 변환하는데, 변환 대상 객체로 엔티티를 직접 노출하거나 또는 DTO를 사용해서 노출한다.
이렇게 JSON으로 생성한 API는 한 번 정의하면 수정하기 어려운 외부 API와 언제든지 수정할 수 있는 내부 API로 나눌 수 있다.</p>
<ul>
<li><p>외부 API : 외부에 노출한다. 한 번 정의하면 수정하기 어렵다. 서버와 클라이언트를 동시에 수정하기 어렵다.
ex) 타팀과 협업하기 위한 API, 타 기업과 협업하는 API</p>
</li>
<li><p>내부 API : 외부에 노출하지 않는다. 언제든지 변경할 수 있으며 서버와 클라이언트를 동시에 수정할 수 있다.
ex) 같은 프로젝트에 있는 화면을 구성하기 위한 AJAX 호출</p>
</li>
</ul>
<p>엔티티는 변경되기 쉽기 때문에 엔티티를 JSON 변환 대상 객체로 사용하면 엔티티를 변경할 때 노출하는 JSON API도 함께 변경된다. 따라서 외부 API는 엔티티를 직접 노출하기보다 DTO를 사용하는 것이 안전하다.</p>
<h2 id="너무-엄격한-계층">너무 엄격한 계층</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/2c6532c6-bfaa-47d3-84f0-d05e28cd0810/image.png" alt="">
OSIV를 사용하기 전에는 프리젠테이션 계층에서 사용할 지연 로딩된 엔티티를 미리 초기화 했어야 한다. 또한 초기화는 아직 영속성 컨텍스트가 살아있는 서비스 계층이나 FACADE 계층이 담당했다.
하지만 OSIV를 사용하면 영속성 컨텍스트가 프리젠테이션 계층까지 살아있으므로 미리 초기화할 필요가 없다. 따라서 단순한 엔티티 조회는 컨트롤러에서 리포지토리를 직접 호출해도 아무런 문제가 없다.</p>
<p>참조 : [자바 ORM 표준 JPA 프로그래밍]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[12장 스프링 데이터 JPA]]></title>
            <link>https://velog.io/@now_here/12%EC%9E%A5-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA</link>
            <guid>https://velog.io/@now_here/12%EC%9E%A5-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA</guid>
            <pubDate>Sun, 19 Jan 2025 13:31:28 GMT</pubDate>
            <description><![CDATA[<h1 id="스프링-데이터-jpa-소개">스프링 데이터 JPA 소개</h1>
<p><strong>스프링 데이터 JPA</strong>는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트다.
반복적인 CRUD 작업을 할 때 해결하기 위해 스프링 프레임 워크에서는 <strong>스프링 데이터 JPA</strong>를 이용할 수 있다. 공통 CRUD 인터페이스를 제공하고 리포지토리를 개발할 때 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입해준다. 따라서 개발자는 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있다.
CRUD를 처리하기 위한 공통 메서드는 스프링 데이터 JPA가 제공하는 org.springframework.data.jpa.repository.JpaRepository 인터페이스에 있다.</p>
<h4 id="예제">예제</h4>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    Member findByUserName(String username);
}</code></pre>
<p>위처럼 공통적으로 처리할 수 없는 MemberRepository.findByUserName(...)은 스프링 데이터 JPA에서 메소드 이름을 분석해서 다음 JPQL을 실행한다.</p>
<pre><code class="language-sql">select m from Member m where username =:username</code></pre>
<h2 id="공통-인터페이스-기능">공통 인터페이스 기능</h2>
<p>스프링 데이터 JPA의 공통 인터페이스를 사용하는 가장 간단한 방법은 위에서 보여준 예제처럼 JpaRepository 인터페이스를 상속받는 것이다. 제네릭 엔티티 클래스와 엔티티 클래스가 사용하는 식별자 타입을 지정하여 사용한다.</p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    Member findByUserName(String username);
}</code></pre>
<p>위처럼 JpaRepository 인터페이스를 상속받았을 때 다양한 기능을 사용할 수 있는데, 이를 알아보기 위해 JpaRepository 인터페이스의 계층 구조를 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/now_here/post/d2d1fdaa-acab-47e6-9082-0e909f8da150/image.png" alt=""></p>
<p>스프링 데이터 모듈안에 공통으로 사용하는 인터페이스가 있고 스프링 데이터 JPA가 제공하는 JpaRepository 인터페이스는 여기에 추가로 JPA에 특화된 기능을 제공한다.
참고로 T는 엔티티, ID는 엔티티의 식별자 타입, S는 엔티티와 그 자식 타입을 의미한다.
몇 가지 주요 메서드를 살펴보자.</p>
<ul>
<li>save(S) : 엔티티의 식별자 값이 없으면(null) 새로운 엔티티로 판단해서 EntityManager.persist()를 호출하고 식별자 값이 있으면 이미 있는 엔티티로 판단해서 EntityManager.merge()를 호출한다. 필요하다면 스프링 데이터 JPA 기능을 확장해서 신규 엔티티 판단 전략을 변경할 수 있다.</li>
<li>delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove()를 호출한다.</li>
<li>findOne(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find()를 호출한다.</li>
<li>getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference()를 호출한다.</li>
<li>findAll(...) : 모든 엔티티를 조회한다. 정렬(Sort)이나 페이징(Pageable) 조건을 파라미터로 제공할 수 있다.</li>
</ul>
<h2 id="쿼리-메소드-기능">쿼리 메소드 기능</h2>
<p>스프링 데이터 JPA의 <strong>쿼리 메소드 기능</strong>은 Repository 인터페이스의 메소드 이름을 기반으로 데이터베이스 쿼리를 자동 생성하는 기능이다. 이 기능을 통해 개발자는 복잡한 JPQL 또는 SQL을 작성하지 않고도 데이터베이스와 상호작용하는 쿼리를 생성할 수 있다.</p>
<h3 id="메소드-이름으로-쿼리-생성">메소드 이름으로 쿼리 생성</h3>
<p>findBy, countBy와 같이 <strong>스프링 데이터 JPA 공식 문서</strong>가 제공하는 규칙에 따라서 메소드를 정의하면 해당 메서드를 실행할 때 메소드 이름을 분석해서 JPQL을 생성하고 실행한다.</p>
<p>아래는 이메일과 이름으로 회원을 조회하는 예시이다.</p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    List&lt;Member&gt; findByEmailAndName(String email, String name);
}</code></pre>
<pre><code class="language-sql">//실행 JPQL
select m from Member m where m.email = ?1 and m.name = ?2</code></pre>
<p>*<em>스프링 데이터 JPA 공식 문서가 제공하는 쿼리 생성 기능은 아래와 같다. *</em>
<img src="https://velog.velcdn.com/images/now_here/post/cb0fa63d-33fb-4b7a-a805-6e02f2aca2fd/image.png" alt=""></p>
<p>더 자세한 쿼리 생성 기능은 아래 공식 문서를 참고하자.
<a href="https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html">https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html</a></p>
<h3 id="jpa-namedquery">JPA NamedQuery</h3>
<p>스프링 데이터 JPA는 메소드 이름으로 JPA Naemd 쿼리를 호출하는 기능을 제공한다. JPA Named 쿼리는 쿼리에 이름을 부여해서 사용하는 방법인데, @NamedQuery 어노테이션이나 XML에 쿼리를 정의할 수 있다.
(자세한 내용은 10장에서 설명했으므로 넘어가겠다.)</p>
<pre><code class="language-java">public interface MemberRepository 
        extends JpaRepository&lt;Member, Long&gt; {  // NamedQuery 선언한 Member 도메인 클래스
    List&lt;Member&gt; findByUserame(@Param(&quot;username&quot;) String username);
}</code></pre>
<p>스프링 데이터 JPA는 선언한 &quot;도메인 클래스 + .(점) + 메서드 이름&quot;으로 Named 쿼리를 찾아서 실행한다. 만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략을 사용한다.</p>
<h3 id="query-리포지토리-메소드에-쿼리-정의">@Query, 리포지토리 메소드에 쿼리 정의</h3>
<p>리포지토리 메소드에 직접 쿼리를 정의하려면 @org.springframework.data.jpa.repository.Query 어노테이션을 사용할 수 있다. 실행할 메서드에 정적 쿼리를 직접 작성하므로 <strong>이름 없는 Named 쿼리</strong>라 할 수 있다.JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다는 장점이 있다.</p>
<p>네이티브 SQL을 사용하려면 @Query 어노테이션에 nativeQuery = true 로 설정해서 사용할 수 있다.
이 때, JPQL은 위치 기반 파라미터를 1부터 시작하지만 네이티브 SQL은 0부터 시작한다는 차이점이 있다.</p>
<pre><code class="language-java">
// 메서드에 JPQL 쿼리 작성
public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    @Query(&quot;select m from Member m where m.username = ?1&quot;)
    List&lt;Member&gt; findByUsername(String username);
}

// JPA 네이티브 SQL 지원
public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    @Query(value = &quot;SELECT * FROM MEMBER WHERE USERNAME = ?0&quot;,
            nativeQuery = true)
    List&lt;Member&gt; findByUsername(String username);
}</code></pre>
<h3 id="파라미터-파인딩">파라미터 파인딩</h3>
<p>스프링 데이터 JPA는 위치 기반 파라미터 바인딩과 이름 기반 파라미터 바인딩을 모두 지원한다. 코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하는 것을 추천한다.</p>
<p>이름 기반 파라미터 바인딩을 사용하려면 org.springframework.data.repository.query.Param(파라미터 이름)어노테이션을 사용하면 된다.</p>
<pre><code class="language-java">import org.springframework.data.repository.query.Param

public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    @Query(&quot;select m from Member m where m.username =:name&quot;)
    List&lt;Member&gt; findByUsername(@Param(&quot;naem&quot;)String username);
}</code></pre>
<h3 id="벌크성-수정-쿼리">벌크성 수정 쿼리</h3>
<p>스프링 데이터 JPA에서 벌크성 수정, 삭제 쿼리는 org.springframework.data.jpa.repository.Modifying 어노테이션을 사용하면 된다. 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화하고 싶으면 @Modifying(clearAutomatically = true) 처럼 해당 속성을 true로 설정하면 된다.</p>
<pre><code class="language-java">@Modifying
@Query(&quot;update Product p set p.price = p.price * 1.1
            where p.stockAmount &lt; :stockAmount&quot;)
int bulkPriceUp(@Param(&quot;stockAmount&quot;) String stockAmount);</code></pre>
<pre><code class="language-java">// ProductRepository 예제
public interface ProductRepository extends JpaRepository&lt;Product, Long&gt; {
    @Modifying(clearAutomatically = true)
    @Query(&quot;update Product p set p.price = p.price * 1.1 where p.stockAmount &lt; :stockAmount&quot;)
    int bulkPriceUp(@Param(&quot;stockAmount&quot;) String stockAmount);
}</code></pre>
<h3 id="반환-타입">반환 타입</h3>
<p>스프링 데이터 JPA는 유연한 반환타입을 지원하는데 결과가 한 건 이상이면 컬렉션을 반환하고 단건이면 반환 타입을 지정한다.
이 때, 조회 결과가 없으면 컬렉션은 빈 컬렉션을 반환하고, 단건은 null을 반환한다. 단건 조회에서 2건 이상이 조회되면 javax.persistence.NonUniqueResultException 예외가 발생한다.</p>
<h3 id="페이징과-정렬">페이징과 정렬</h3>
<p>스프링 데이터 JPA는 쿼리 메서드에 페이징과 정렬 기능을 사용할 수 있게 2가지 파라미터를 제공한다.</p>
<ul>
<li>org.springframework.data.domain.Sort: 정렬기능</li>
<li>org.springframework.data.domain.Pageable: 페이징 기능(내부에 Sort 포함)</li>
</ul>
<p>파라미터에 Pageable을 사용하면 반환 타입으로 List나 org.springframework.data.domain.Page를 사용할 수 있다. 이 때, Page를 반환타입으로 지정하면 페이징 기능을 제공하기 위해 검색된 전체 데이터 건수를 조회하는 count 쿼리를 추가로 호출한다.</p>
<pre><code class="language-java">//count 쿼리 사용
Page&lt;Member&gt; findByName(String name, Pageble pageable);

//count 쿼리 사용 안 함
List&lt;Member&gt; findByName(String name, Pageble pageable);

List&lt;Member&gt; findByName(String name, Sort sort);</code></pre>
<h4 id="페이징-예제">페이징 예제</h4>
<p>검색 조건 : 이름이 김으로 시작하는 회원
정렬 조건 : 이름으로 내림차순
페이징 조건 : 첫 번째 페이지, 페이지당 보여줄 데이터는 10건</p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    Page&lt;Member&gt; findByNameStartingWith(String name, Pageble pageble);
}

//페이징 조건과 정렬 조건 설정
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(); //조회된 데이터    </code></pre>
<p>리포지토리에서 정의한 메서드에 두 번째 파라미터 Pageable은 인터페이스다. 따라서 실제 사용할 때는 해당 인터페이스를 구현한 org.springframework.data.domain.PageRequest 객체를 사용한다. PageRequest 생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력한다. 여기에 추가로 정렬정보도 파라미터로 사용할 수 있다. 참고로 페이지는 0부터 시작한다.</p>
<h3 id="힌트">힌트</h3>
<blockquote>
</blockquote>
<p>JPA에서 <strong>쿼리 힌트(Query Hint)</strong>는 특정 쿼리에 대해 JPA 구현체가 사용할 힌트를 제공하는 기능입니다. 주로 성능 최적화나 쿼리 동작 변경을 위해 사용됩니다. 힌트는 JPA 표준이 제공하는 기본 기능이며, 구현체에 따라 지원하는 힌트가 다를 수 있습니다.</p>
<blockquote>
<p>사용 목적
읽기 전용 쿼리 최적화
예: 쿼리 결과를 수정하지 않을 경우, 읽기 전용 힌트를 추가하여 내부적으로 성능 최적화를 할 수 있습니다.
캐시 사용 설정
쿼리 실행 시, 2차 캐시를 사용할지 여부를 설정할 수 있습니다.
JPA 구현체 전용 기능 활용
특정 구현체(예: Hibernate)에서 제공하는 고유한 힌트를 활용할 수 있습니다.</p>
</blockquote>
<p>JPA 쿼리 힌트를 사용하려면 org.springframework.data.jpa.repository.QueryHints 어노테이션을 사용하면 된다. 이것은 SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트다.</p>
<pre><code class="language-java">@QueryHints(value = {@QueryHint(name = &quot;org.hibernate.readOnly&quot;, value = &quot;true&quot;)},
                        forCounting = true)
Page&lt;Member&gt; findByNameStartingWith(String name, Pageble pageble);</code></pre>
<p>forCount 속성은 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리에도 쿼리 힌트를 적용할지를 설정하는 옵션이다. (기본값 true)</p>
<h1 id="사용자-정의-리포지토리-구현">사용자 정의 리포지토리 구현</h1>
<p>스프링 데이터 JPA는 인터페이만 정의하고 구현체는 개발하지 않는다. 하지만 다양한 이유로 직접 메서드를 구현해야 할 때가 있는데 이 때 리포지토리를 직접 구현하면 공통 인터페이스가 제공하는 기능까지 모두 구현해야 한다. 이를 피하기 위해서 필요한 메서드만 구현할 수 있는 방법이 있다.</p>
<p>먼저 직접 구현할 메서드를 위해 사용자 정의 인터페이스를 작성해야 한다. (이름은 자유)</p>
<pre><code class="language-java">// 사용자 정의 인터페이스
public interface MemberRepositoryCustom {
    public List&lt;Member&gt; findMemberCustom();
}    </code></pre>
<p>다음으로 사용자 정의 인터페이스 구현체를 만드는데, 구현 클래스의 이름 규칙이 존재한다. <strong>리포지토리 인터페이스 이름 + Impl</strong>로 작성해야 한다. 이렇게 해야 스프링 데이터 JPA가 사용자 정의 구현 클래스로 인식한다.</p>
<pre><code class="language-java">public class MemberRepositoryImpl implements MemberRepositoryCustom {

    @Override
    public List&lt;Member&gt; findMemberCuston() {
        ...//사용자 정의 구현
    }
}    </code></pre>
<p>마지막으로 리포지토리 인터페이스에서 사용자 정의 인터페이스를 상속받으면 된다.</p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt;, MemberRepositoryCustom {
}</code></pre>
<h1 id="스프링-데이터-jpa와-querydsl-통합">스프링 데이터 JPA와 QueryDSL 통합</h1>
<p>스프링 데이터 JPA는 2가지 방법으로 QueryDSL을 지원한다.</p>
<h2 id="querydslpredicateexecutor-사용">QueryDslPredicateExecutor 사용</h2>
<p>리포지토리에서 QueryDslPredicateExecutor를 상속받으면 된다.
(org.springframework.data.querydsl.QueryDslPredicateExecutor)</p>
<pre><code class="language-java">puiblic interface ItemRepository
    extends JpaRepository&lt;Item, Long&gt;, QueryDslPredicateExecutor&lt;Item&gt; {
}

//사용 예제
QItem item = QIem.item;
Iterable&lt;Item&gt; result = itemRepository.findAll(
    item.name.contains(&quot;장난감&quot;).and(item.price.between(10000,20000)));</code></pre>
<p>QueryDslPredicateExecutor 사용에는 join, fetch를 사용할 수 없다는 한계가 있다. (JPQL에서 이야기하는 묵시적 조인은 가능) 따라서 QueryDSL이 제공하는 다양한 기능을 사용하려면 JPAQuery를 직접 사용하거나 스프링 데이터 JPA가 제공하는 QueryDslRepositorySupport를 사용해야 한다.</p>
<h2 id="querydslrepositorysupport">QueryDslRepositorySupport</h2>
<p>(org.springframework.data.querydsl.QueryDslRepositorySupport)</p>
<h4 id="사용예시---주문내역-검색-기능">사용예시 - 주문내역 검색 기능</h4>
<pre><code class="language-java">// CustomOrderRepository 사용자 정의 리포지토리
public interface CustomOrderRepository {
    public List&lt;Order&gt; search(OrderSearch orderSearch);
}

// QueryDslRepositorySupprot 사용 코드.
public class OrderRepositoryImpl extends QueryDslRepositorySupport
    implements CustomOrderRepository {

    public OrderRepositoryImpl() {
        suprer(Order.class);  //엔티티 클래스 정보
    }

    @Override
    public List&lt;Order&gt; search(OrderSearch ordeerSearch) {

        QOrder order = QOrder.order;
        QMember member = QMember.member;

        JPQLQuery query = from(order);

        if (StringUtils.hasTest(oderSearch.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>
<p>검색 조건에 따라 동적으로 쿼리를 생성한다.
참고로 <strong>생성자에서 QueryDslRepositorySupport에 엔티티 클래스 정보를 넘겨주어야 한다.</strong></p>
<p>참조 : [자바 ORM 표준 JPA 프로그래밍]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[10. 객체지향 쿼리 언어 3 (네이티브 SQL, 객체지향 쿼리 심화)]]></title>
            <link>https://velog.io/@now_here/10.-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%BF%BC%EB%A6%AC-%EC%96%B8%EC%96%B4-3-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C-SQL</link>
            <guid>https://velog.io/@now_here/10.-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%BF%BC%EB%A6%AC-%EC%96%B8%EC%96%B4-3-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C-SQL</guid>
            <pubDate>Sat, 04 Jan 2025 15:38:11 GMT</pubDate>
            <description><![CDATA[<h1 id="네이티브-sql">네이티브 SQL</h1>
<p>다양한 이유로 JPQL을 사용할 수 없을 때 JPA는 SQL을 직접 사용할 수 있는 기능을 제공하는데 이것을 네이티브 SQL이라 한다. JPQL을 사용하면 JPA가 SQL을 생성하고 네이티브 SQL은 이 SQL을 개발자가 직접 정의하는 것이다.
네이티브 SQL을 사용하면 엔티티를 조회할 수 있고 JPA가 지원하는 영속성 컨텍스트의 기능을 그대로 사용할 수 있다. 반면에 JDBC API를 직접 사용하면 단순히 데이터의 나열을 조회할 뿐이다.</p>
<h2 id="네이티브-쿼리-api">네이티브 쿼리 API</h2>
<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>
<h3 id="엔티티-조회">엔티티 조회</h3>
<p>네이티브 SQL은 em.createNativeQuery(SQL, 결과 클래스)를 사용한다. 첫 번째 파라미터에 네이티브 SQL을 입력하고 두 번째 파라미터에는 조회할 엔티티 클래스의 타입을 입력한다. JPQL를 사용할 때와 거의 비슷하지만 실제 데이터베이스 SQL을 사용한다는 것과 위치기반 파라미터만 지원한다는 차이가 있다.</p>
<pre><code class="language-java">//SQL 정의
String sql = &quot;SELECT ID, AGE, NAME, TEAM_ID &quot; + 
            &quot;FROM MEMBER WHERE AGE &gt; ?&quot;;

Query nativeQuery = em.createNativeQuery(sql, Member.class)
    .setParameter(1, 20);

List&lt;Member&gt; resultList = nativeQuery.getResultList();</code></pre>
<p>네이티브 SQL로 SQL만 직접 사용할 뿐 JPQL을 사용하는 방법과 같다. 조회한 엔티티도 영속성 컨텍스트에서 관리된다.</p>
<h4 id="참고">참고</h4>
<p>JPA는 공식적으로 네이티브 SQL에서 위치 기반 파라미터만 지원한다. 하지만 하이버네이트는 네이티브 SQL에 이름 기반 파라미터를 사용할 수 있다. 따라서 하이버네이트 구현체를 사용한다면 예제를 이름 기반 파라미터로 변경해도 동작한다.</p>
<h3 id="값-조회">값 조회</h3>
<pre><code class="language-java">//SQL 정의
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();</code></pre>
<p>이렇게 여러 값으로 조회하려면 em.createNativeQuery(SQL)의 두 번째 파라미터를 사용하지 않으면 된다. JPA는 조회한 값들을 Object[]에 담아서 반환한다. 여기서는 스칼라 값들만 조회했을 뿐이므로 결과를 영속성 컨텍스트가 관리하지 않는다. 마치 JDBC로 데이터를 조회한 것과 비슷하다.</p>
<h3 id="결과-매핑-사용">결과 매핑 사용</h3>
<p>엔티티와 스칼라 값을 함께 조회하는 것처럼 매핑이 복잡해지면 @SqlResultSetMapping을 정의해서 결과 매핑을 사용해야 한다.
회원 엔티티와 회원이 주문한 상품 수를 조회하는 예제를 보자.</p>
<pre><code class="language-java">//SQL 정의
    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;);

    List&lt;Object[]&gt; resultList = nativeQuery.getResultList();
    for (Object[] row : resultList) {
        Member member = (Member) row[0];
        BigInteger orderCount = (BigInteger) row[1];

        System.out.println(&quot;member = &quot; + member);
        System.out.println(&quot;orderCount = &quot; + orderCount);
    }
</code></pre>
<p>em.creteNativeQuery(sql, &quot;memberWithOrderCount&quot;)의 두 번째 파라미터에 결과 매핑 정보의 이름이 사용되었다. 결과 매핑을 정의하는 코드를 보자.</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>ID, AGE, NAME, TEAM_ID는 Member 엔티티와 매핑하고 ORDER_COUNT는 단순히 값으로 매핑한다. </p>
<p>@FieldResult를 사용해서 컬럼명과 필드명을 직접 매핑할 수도 있다. 이 어노테이션을 한 번이라도 사용하면 전체 필드를 @FieldResult로 매핑해야 한다. 
또한 다음과 같이 두 엔티티를 조회하는데 컬럼명이 중복될 때도 사용해야 한다.</p>
<pre><code class="language-sql">SELECT A.ID, B.ID FROM A, B</code></pre>
<p>A, B 엔티티 둘 다 ID라는 필드를 가지고 있어 컬럼명이 충돌한다. 따라서 다음과 같이 별칭을 적절히 사용하고 @FieldResult로 매핑하면 된다.</p>
<pre><code class="language-sql">SELECT
    A.ID AS A_ID
    B.ID AD B_ID
FROM A, B</code></pre>
<h2 id="named-네이티브-sql">Named 네이티브 SQL</h2>
<p>네이티브 SQL도 앞서 설명한 @NamedNativeQuery를 이용해서 Named 쿼리를 사용할 수 있다.</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 {...}

//사용 예제
TypedQuery&lt;Member&gt; nativeQuery =
        em.createNamedQuery(&quot;Member.memberSQL&quot;, Member.class)
            .setParameter(1, 20);</code></pre>
<p>Named 네이티브 쿼리에서 resultSetMapping 속성을 이용해서 조회 결과를 매핑할 대상도 지정할 수있다.</p>
<h2 id="네이티브-sql-정리">네이티브 SQL 정리</h2>
<p>네이티브 SQL도 JPQL처럼 Query, TypeQuery(Named 네이티브 쿼리의 경우에만)를 반환한다. 따라서 JPQL API를 그대로 사용할 수 있어 페이징 처리 API도 적용할 수 있다.</p>
<p>네이티브 SQL은 특정 데이터베이스에 종속적인 쿼리가 증가해서 이식성이 떨어진다. 그러므로 될수록 표준 JPQL을 사용하고 기능이 부족하면 하이버네이트 같은 JPA 구현체가 제공하는 기능을 사용하자. 그래도 안 될 때 네이티브 SQL을 사용하자.
네이티브 SQL로도 부족하다면 MyBatis나 스프링 프레임워크가 제공하는 JdbcTemplate같은 SQL 매퍼와 JPA를 함께 사용하는 것도 고려해보자.</p>
<hr>
<h2 id="스토어드-프로시저-jpa21">스토어드 프로시저 (JPA2.1)</h2>
<p>JPA는 2.1부터 스토어드 프로시저를 지원한다.</p>
<h3 id="스토어드-프로시저">스토어드 프로시저</h3>
<p>스토어드 프로시저(Stored Procedure)는 데이터베이스에서 미리 정의된 SQL 코드 집합을 말합니다. 쉽게 말하면, 자주 사용되는 SQL 쿼리나 연산을 미리 데이터베이스에 저장해두고 필요할 때마다 호출해서 실행할 수 있도록 한 기능입니다.</p>
<h3 id="스토어드-프로시저의-장점">스토어드 프로시저의 장점:</h3>
<ol>
<li><strong>성능 향상</strong>: SQL 코드가 서버에 저장되기 때문에, 매번 쿼리를 실행할 때마다 서버와 클라이언트 간의 데이터 전송을 줄일 수 있습니다.</li>
<li><strong>재사용성</strong>: 자주 쓰는 복잡한 쿼리나 로직을 한 번 작성해두고 여러 번 호출하여 사용할 수 있습니다.</li>
<li><strong>보안</strong>: 직접 SQL을 작성하는 대신, 특정 권한을 가진 사용자만 프로시저를 실행하도록 제한할 수 있습니다.</li>
<li><strong>데이터 무결성 유지</strong>: 프로시저 내에서 정의된 로직에 따라 데이터의 일관성과 무결성을 유지할 수 있습니다.</li>
</ol>
<h3 id="예시">예시:</h3>
<p>예를 들어, 고객 정보에 대한 조회를 자주 하는 경우, 이런 작업을 스토어드 프로시저로 만들어두고, 고객 정보 조회가 필요할 때마다 호출할 수 있습니다.</p>
<pre><code class="language-sql">CREATE PROCEDURE GetCustomerInfo(IN customerId INT)
BEGIN
    SELECT * FROM Customers WHERE id = customerId;
END;</code></pre>
<p>이렇게 만들어진 프로시저는 <code>GetCustomerInfo(1)</code>처럼 호출하여 사용할 수 있습니다. 이 프로시저는 <code>customerId</code> 값을 받아 해당 고객 정보를 반환하는 역할을 합니다.</p>
<h3 id="사용-사례">사용 사례:</h3>
<ul>
<li>대규모 웹 애플리케이션에서 자주 실행되는 복잡한 쿼리를 최적화할 때.</li>
<li>트랜잭션을 여러 단계에 걸쳐 처리해야 할 때.</li>
</ul>
<p>즉, 스토어드 프로시저는 데이터베이스 내에서 효율적이고 안정적으로 데이터를 처리할 수 있는 도구입니다.</p>
<h3 id="스토어드-프로시저-사용">스토어드 프로시저 사용</h3>
<p>입력 값을 두 배로 증가시켜 주는 proc_multiply라는 스토어드 프로시저를 사용해 보자. 이 프로시저는 첫 번째 파라미터로 값을 입력받고 두 번째 파라미터로 결과를 반환한다.</p>
<pre><code class="language-sql">// proc_multiply MySQL 프로시저
DELIMITER //

CREATE PROCEDURE proc_multiply (INOUT inParam INT, INOUT outParam INT)
BEGIN
    SET outParam = inParam * 2;
END    //</code></pre>
<p>JPA로 위 스토어드 프로시저를 호출해보자.</p>
<pre><code class="language-java">    //순서 기반 파라미터 호출
    StoredProcedureQuery spq = 
        em.createStoredProcedureQuery(&quot;proc_multiply&quot;);
    spq.registerStoredProcedureParamter(1, Integer.class, ParameterMode.IN);    //1
    spq.registerStoredProcedureParamter(2, Integer.class, ParameterMode.OUT);    //2

    spq.setParamter(1,100);    //위에 1
    spq.execute();

    Integer out = (Integer) spq.getOutputParameterValue(2);    //위에 2
    System.out.println(&quot;out = &quot; + out); //결과 = 200</code></pre>
<p>스토어드 프로시저를 사용하려면 em.createStoredProcedureQuery() 메소드에 사용할 스토어드 프로시저 이름을 이력하면 된다. 그리고 registeStoredProcedureParameter() 메소드를 사용해서 프로시저에서 사용할 파라미터를 순서, 타입, 파라미터 모드 순으로 정의하면 된다.</p>
<pre><code class="language-java">//사용할 수 있는 ParameterMode
public enum ParameterMode {
    IN,            //INPUT 파라미터
    INOUT,        //INPUT, OUTPUT 파라미터
    OUT,        //OUTPUT 파라미터
    REF_CURSOR    //CURSOR 파라미터
}    </code></pre>
<p>파라미터에 이름을 사용하는 방법도 있는데 아래와 같다.</p>
<pre><code class="language-java">    // 파라미터에 이름 사용
    StoredProcedureQuery spq = 
        em.createStoredProcedureQuery(&quot;proc_multiply&quot;);
    spq.registerStoredProcedureParamter(&quot;inParam&quot;, Integer.class, ParameterMode.IN);    //1
    spq.registerStoredProcedureParamter(&quot;outParam&quot;, Integer.class, ParameterMode.OUT);    //2

    spq.setParamter(&quot;inParam&quot;,100);    //위에 1
    spq.execute();

    Integer out = (Integer) spq.getOutputParameterValue(&quot;outParam&quot;);    //위에 2
    System.out.println(&quot;out = &quot; + out); //결과 = 200</code></pre>
<h3 id="named-스토어드-프로시저-사용">Named 스토어드 프로시저 사용</h3>
<p>스토어드 프로시저 쿼리에 이름을 부여해서 사용하는 것을 <strong>Named 스토어드 프로시저</strong>라 한다.</p>
<pre><code class="language-java">@Entity
@NamedStoredProcedureQuery(
    name = &quot;multiply&quot;,
    procedureName = &quot;proc_multiply&quot;,
    parameters = {
        @StoredProcedureParameter(name = &quot;inParam&quot;, 
            mode = ParameterMode.IN, type = Integer.class),
        @StoredProcedureParameter(name = &quot;outParam&quot;, 
            mode = ParameterMode.OUT, type = Integer.class),
    }
)
public class Member {...}</code></pre>
<p>@NamedStoredProcedureQuery로 정의하고 name 속성으로 이름을 부여하면 된다. procedureName 속성에 실제 호출할 프로시저 이름을 적어주고 @StoredProcedureParameter 사용해서 파라미터 정보를 정의하면 된다. 참고로 둘 이상을 정의하려면 @NamedStoredProcedureQueries를 사용하면 된다.
XML에 정의해서 사용하는 방법도 있다.</p>
<hr>
<h1 id="객체지향-쿼리-심화">객체지향 쿼리 심화</h1>
<h2 id="벌크-연산">벌크 연산</h2>
<p>여러 건을 한 번에 수정하거나 삭제하는 <strong>벌크 연산</strong>에 대해 알아보자.
재고가 10개 미만인 모든 상품의 가격을 10 % 상승시키는 예제를 살펴보자.</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>벌크 연산은 위처럼 executeUpdate() 메소드를 사용한다. 이 메소드는 벌크 연산으로 영향 받은 엔티티 건수를 반환한다. 삭제도 같은 메소드를 사용한다.
(JPA 표준은 아니지만 하이버네이트는 INSERT에도 벌크 연산을 지원한다고 함)</p>
<h3 id="벌크-연산의-주의점">벌크 연산의 주의점</h3>
<p>벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다는 점을 주의해야 한다.
예를 들어 가격이 1000원인 상품 A를 조회한 후 벌크 연산으로 모든 상품의 가격을 10% 인상했다고 해보자. 다시 상품 A를 조회하면 1100원의 가격을 기대하지만 실제로는 1000원이 출력된다.
이는 처음에 조회한 후 상품 A는 영속성 컨텍스트에 관리하기 때문에 가격이 1000원이고 벌크 연산은 직접 데이터베이스에 쿼리를 날리기 때문이다. 따라서 영속성 컨텍스트에 있는 상품 A와 데이터베이스에 있는 상품A의 가격이 다를 수 있다.</p>
<h3 id="벌크-연산-주의점-해결방법">벌크 연산 주의점 해결방법</h3>
<ul>
<li><p>em.refresh() 사용
벌크 연산 후 필요하다면 em.refresh()를 이용해서 데이터베이스에서 상품A를 다시 조회하면 된다.
em.refresh(productA);    //데이터베이스에 상품A 재조회</p>
</li>
<li><p>벌크 연산 먼저 실행
가장 실용적인 해결방법으로 벌크 연산을 가장 먼저 실행하는 방법이 있다.</p>
</li>
<li><p>벌크 연산 수행 후 영속성 컨텍스트 초기화
벌크 연산 후 영속성 컨텍스트를 초기화하면 재조회시 데이터베이스에서 엔티티를 조회하기 때문에 이 방법도 좋은 방법이다.</p>
</li>
</ul>
<p>벌크 연산은 영속성 컨텍스트와 2차 캐시를 무시하고 데이터베이스에 직접 실행하므로 주의해서 사용하자.</p>
<h2 id="영속성-컨텍스트와-jpql">영속성 컨텍스트와 JPQL</h2>
<h3 id="쿼리-후-영속-상태인-것과-아닌-것">쿼리 후 영속 상태인 것과 아닌 것</h3>
<p>JPQL의 조회 대상은 엔티티, 임베디드 타입, 값 타입으로 다양하다. 엔티티를 조회하면 영속성 컨텍스트에서 관리되지만 다른 타입은 그렇지 않다. 예를 들어 임베디드 타입은 조회해서 값을 변경해도 영속성 컨텍스트가 관리하지 않으므로 변경 감지에 의한 수정이 발생하지 않는다. 물론 엔티티를 조회하면 해당 엔티티가 가지고 있는 임베디드 타입은 함께 수정된다.</p>
<pre><code class="language-java">select m from Member m //엔티티 조회 (관리O)
select o.address from Order o //임베디드 타입 조회 (관리 x)
select m.id, m.username from Member m    //단순 필드 조회 (관리x)</code></pre>
<h3 id="jpql로-조회한-엔티티와-영속성-컨텍스트">JPQL로 조회한 엔티티와 영속성 컨텍스트</h3>
<p>JPQL로 데이터베이스에서 조회한 엔티티가 영속성 컨텍스트에 이미 있으면 JPQL로 데이터베이스에서 조회한 결과를 버리고 대신에 영속성 컨텍스트에 있던 엔티티를 반환한다. 이때 식별자 값을 사용해서 비교한다.</p>
<ul>
<li>JPQL로 조회한 엔티티는 영속 상태이다.</li>
<li>영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 기존 엔티티를 반환한다.</li>
</ul>
<p><strong>영속성 컨텍스트는 영속 상태인 엔티티의 동일성을 보장하기 때문에 영속성 컨텍스트에 이미 엔티티가 존재한다면 기존 엔티티는 그대로 두고 새로 검색한 엔티티를 버린다.</strong> em.find()로 조회하든 JPQL을 사용하든 영속성 컨텍스트가 같으면 동일한 엔티티를 반환한다.</p>
<h3 id="find-vs-jpql">find() vs JPQL</h3>
<p>em.find() 메소드는 엔티티를 영속성 컨텍스트에서 먼저 찾고 없으면 데이터베이스에서 찾는다. 따라서 엔티티가 영속성 컨텍스트에 있으면 메모리에서 바로 찾으므로 성능상 이점이 있다.(1차캐시)
하지만 <strong>JPQL은 항상 데이터베이스에 SQL을 실행해서 결과를 조회한다.</strong> 앞서 말한 것처럼 영속성 컨텍스트에 엔티티가 존재하면 기존 엔티티를 반환해서 동일성을 보장하긴 한다.</p>
<h4 id="jpql-특징">JPQL 특징</h4>
<ul>
<li>JPQL은 항상 데이터베이스를 조회</li>
<li>JPQL로 조회한 엔티티는 영속 상태</li>
<li>영속성 컨텍스트에 이미 존재하는 엔티티가 있으면 기존 엔티티를 반환</li>
</ul>
<h2 id="jpql과-플러시-모드">JPQL과 플러시 모드</h2>
<p>(플러시가 기억나지 않으면 3장을 다시 보고 오자)
플러시는 영속성 컨텍스트의 변경내역을 데이터베이스에 동기화하는 것이다. 플러시를 호출하려면 em.flush() 메소드를 직접 사용해도 되지만 보통 플러시 모드FlushMode에 따라 커밋하기 직전이나 쿼리 실행 직전에 자동으로 플러시가 호출된다.</p>
<pre><code class="language-javaa">em.setFlushMode(FlushModeType.AUTO);    //커밋 또는 쿼리 실행 시 플러시 (기본값)
em.setFlushMode(FlushModeType.COMMIT);    //커밋시에만 플러시</code></pre>
<p>플러시 모드는 AUTO가 기본값이다. 따라서 JPA는 트랜잭션 커밋 직전이나 쿼리 실행 직전에 자동으로 플러시를 호출한다. COMMIT옵션은 커밋 시에만 플러시하고 쿼리 실행 시에는 플러시를 호출하지 않는다. 이 옵션은 성능 최적화를 위해 꼭 필요할 때만 사용해야 한다.</p>
<h3 id="쿼리와-플러시-모드">쿼리와 플러시 모드</h3>
<p>JPQL은 영속성 컨텍스트에 있는 데이터를 고려하지 않고 데이터베이스에서 데이터를 조회한다. 따라서 JPQL을 실행하기 전에 플러시를 하지 않으면 문제가 발생할 수 있다. </p>
<pre><code class="language-java">//예제
//가격을 1000원에서 2000원으로 변경
product.setPrice(2000);

//가격이 2000원 상품 조회
Product product2 =
        em.createQuery(&quot;select p from Product p where p.price = 2000&quot;,Product.class)
        .getSingleResult();</code></pre>
<p>예를 들어 상품 가격을 1000원에서 2000원으로 수정했다고 해보자. 이 다음 JPQL로 가격이 2000원인 조회를 했다고 하자. 플러시 모드가 AUTO일 때는 쿼리가 실행하기 전에 영속성 컨텍스트가 플러시 되어 방금 2000원으로 수정한 상품을 조회할 수 있다.
만약 플러시 모드가 COMMIT이였다면 쿼리시에는 플러시를 하지 않으므로 방금 2000원으로 수정한 상품을 조회할 수 없다. 이 때는 JPQL을 사용하기 전에 직접 em.flush()를 호출하거나 setFlushMode()를 설정해 주면 된다.</p>
<pre><code class="language-java">em.setFlushMode(FlushModeType.COMMIT);    //커밋시에만 플러시

//가격을 1000원에서 2000원으로 변경
product.setPrice(2000);

//1. em.flush() //직접 호출

//가격이 2000원 상품 조회
Product product2 =
        em.createQuery(&quot;select p from Product p where p.price = 2000&quot;,Product.class)
        .setFlushMode(FlushModeType.AUTO)    //2. setFlushMode() 설정
        .getSingleResult();</code></pre>
<p>위와 같이 쿼리를 실행하기 전에 주석 1을 풀어서 직접 em.flush()를 호출해도 되고 setFlushMode()에서 AUTO로 설정하면 된다. 이렇게 쿼리에 설정하는 플러시 모드는 엔티티 매니저에 설정하는 플러시 모드보다 우선권을 가진다.</p>
<h3 id="플러시-모드와-최적화">플러시 모드와 최적화</h3>
<p>FlushModeType.COMMIT모드는 트랜잭션을 커밋할 때만 플러시하고 쿼리를 실행할 때는 플러시하지 않는다. 따라서 JPA 쿼리를 사용할 때 영속성 컨텍스트에 있지만 아직 데이터베이스에 반영하지 않은 데이터를 조회할 수 없어 데이터 무결성에 심각한 피해를 줄 수 있다. 다만 플러시가 너무 자주 일어나 성능 최적화가 필요한 경우에 COMMIT을 사용한다.</p>
<p>JPA를 사용하지 않고 JDBC를 직접 사용해서 SQL을 실행할 때도 플러시 모드를 고민해야 한다. JPA를 통하지 않고 JDBC로 쿼리를 직접 실행하면 JPA는 JDBC가 실행한 쿼리를 인식할 방법이 없다. 따라서 별도의 JDBC 호출은 플러시 모드를 AUTO로 설정해도 플러시가 일어나지 않는다. 이때는 JDBC로 쿼리를 실행하기 직전에 em.flush()를 호출해서 영속성 컨텍스트의 내용을 데이터베이스에 동기화하는 것이 안전하다.</p>
<p>참조 : [자바 ORM 표준 JPA 프로그래밍]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[10장 객체지향 쿼리 언어 2 (QueryDSL)]]></title>
            <link>https://velog.io/@now_here/10%EC%9E%A5-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%BF%BC%EB%A6%AC-%EC%96%B8%EC%96%B4-2-QueryDSL</link>
            <guid>https://velog.io/@now_here/10%EC%9E%A5-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%BF%BC%EB%A6%AC-%EC%96%B8%EC%96%B4-2-QueryDSL</guid>
            <pubDate>Fri, 03 Jan 2025 12:37:20 GMT</pubDate>
            <description><![CDATA[<h1 id="querydsl">QueryDSL</h1>
<p>쿼리를 문자가 아닌 코드로 작성해도, 쉽고 간결하며 그 모양도 쿼리와 비슷하게 개발할 수 있게 해주는 오픈소스 프로젝트.
처음에는 HQL(하이버네이트 쿼리언어)을 코드로 작성할 수 있도록 해주는 프로젝트로 시작해서 지금은 JPA, JDO, JDBC, Lucence, Hibernate Search, 몽고DB, 자바 컬렉션 등을 다양하게 지원한다.
QueryDSL은 이름 그대로 데이터를 조회하는데 기능이 특화되어 있다.</p>
<h2 id="querydsl-설정">QueryDSL 설정</h2>
<p><strong>책은 3.6.3버전이라 패키지가 다르고 에러가 다른 등 차이가 있어 5.xx 최신버전으로 설명을 약간 수정했다.</strong></p>
<p><code>pom.xml</code>에 QueryDSL 쿼리 타입을 생성하기 위한 플러그인을 추가하려면, <code>maven-compiler-plugin</code>과 <code>apt-maven-plugin</code>을 함께 설정해야 합니다. <code>apt-maven-plugin</code>은 QueryDSL의 <code>Q</code> 클래스를 생성하는 역할을 하며, <code>maven-compiler-plugin</code>은 컴파일을 제어합니다. 아래는 이를 위한 설정 예시입니다:</p>
<pre><code class="language-xml">&lt;dependencies&gt;
    &lt;!-- QueryDSL JPA 의존성 --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;com.querydsl&lt;/groupId&gt;
        &lt;artifactId&gt;querydsl-jpa&lt;/artifactId&gt;
        &lt;version&gt;5.0.0&lt;/version&gt; &lt;!-- 최신 버전 --&gt;
    &lt;/dependency&gt;

    &lt;!-- QueryDSL APT 의존성 (컴파일 타임에 Q 클래스 생성) --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;com.querydsl&lt;/groupId&gt;
        &lt;artifactId&gt;querydsl-apt&lt;/artifactId&gt;
        &lt;version&gt;5.0.0&lt;/version&gt; &lt;!-- 최신 버전 --&gt;
        &lt;scope&gt;provided&lt;/scope&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;

&lt;build&gt;
    &lt;plugins&gt;
        &lt;!-- APT 플러그인: QueryDSL Q 클래스 생성 --&gt;
        &lt;plugin&gt;
            &lt;groupId&gt;org.codehaus.mojo&lt;/groupId&gt;
            &lt;artifactId&gt;apt-maven-plugin&lt;/artifactId&gt;
            &lt;version&gt;1.1.3&lt;/version&gt;
            &lt;executions&gt;
                &lt;execution&gt;
                    &lt;goals&gt;
                        &lt;goal&gt;process&lt;/goal&gt;
                    &lt;/goals&gt;
                &lt;/execution&gt;
            &lt;/executions&gt;
            &lt;configuration&gt;
                &lt;!-- 생성된 Q 클래스가 src/main/java 폴더에 위치하게 설정 --&gt;
                &lt;outputDirectory&gt;${project.build.directory}/generated-sources/apt&lt;/outputDirectory&gt;
            &lt;/configuration&gt;
        &lt;/plugin&gt;

        &lt;!-- 컴파일러 플러그인: APT 처리 후 Java 컴파일 --&gt;
        &lt;plugin&gt;
            &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
            &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;
            &lt;version&gt;3.8.1&lt;/version&gt;
            &lt;configuration&gt;
                &lt;source&gt;1.8&lt;/source&gt;
                &lt;target&gt;1.8&lt;/target&gt;
                &lt;!-- APT 플러그인에서 생성한 파일을 컴파일 대상으로 포함 --&gt;
                &lt;generatedSourcesDirectory&gt;${project.build.directory}/generated-sources/apt&lt;/generatedSourcesDirectory&gt;
            &lt;/configuration&gt;
        &lt;/plugin&gt;
    &lt;/plugins&gt;
&lt;/build&gt;</code></pre>
<h3 id="설명">설명:</h3>
<ol>
<li><strong><code>apt-maven-plugin</code></strong>: 이 플러그인은 <code>Q</code> 클래스를 생성하는 역할을 하며, 지정된 <code>outputDirectory</code>에 결과를 저장합니다.</li>
<li><strong><code>maven-compiler-plugin</code></strong>: 이 플러그인은 <code>Q</code> 클래스를 생성한 후 그 파일을 실제로 컴파일하여 프로젝트의 클래스 경로에 포함시킵니다.</li>
<li><strong><code>querydsl-jpa</code></strong>: QueryDSL의 핵심 라이브러리로, JPA와 연동하여 쿼리 생성 기능을 제공합니다.</li>
<li><strong><code>querydsl-apt</code></strong>: <code>Q</code> 클래스를 생성하는 라이브러리로, 컴파일 타임에 실행됩니다.</li>
</ol>
<p>(QueryDSL을 사용하려면 엔티티를 기반으로 쿼리 타입이라는 쿼리용 클래스, Q클래스를 생성해야함)</p>
<p>이 설정을 추가한 후 <code>mvn clean install</code>을 실행하면, <code>Q</code> 클래스가 생성되고, 이를 통해 QueryDSL을 사용하여 JPA 기반 쿼리를 작성할 수 있습니다.</p>
<h2 id="querydsl-시작">QueryDSL 시작</h2>
<pre><code class="language-java">        public void queryDSL() {

            EntityManager em = emf.createEntityManager();

            JPAQuery query = new JPAQuery(em);
            QMember qMember = new QMember(&quot;m&quot;); //생성되는 JPQL의 별칭이 m
            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을 사용하려면 com.querydsl.jpa.impl.JPAQuery (queryDSL 5.xx 버전부터 패키지명 변경) 객체를 생성해야 하는데 이때 엔티티 매니저를 생성자에 넘겨준다.
사용할 쿼리 타입(Q)을 생성하는데 생성자에는 별칭을 주면 된다. 이 별칭을 JPQL에서 별칭으로 사용한다.
다음 where, orderBy, list는 코드만 봐도 유추가 가능하다.</p>
<pre><code class="language-sql">//실행 JPQL
select m from Member m
where m.name =?1
order by m.name desc</code></pre>
<h3 id="기본-q-생성">기본 Q 생성</h3>
<p>쿼리 타입(Q)은 사용하기 편리하도록 기본 인스턴스를 보관하고 있다. 하지만 엔티티를 조인하거나 같은 엔티티를 서브쿼리에 사용하면 같은 별칭이 사용되므로 이때는 별칭을 직접 지정해서 사용해야 한다.</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>기본 인스턴스를 사용하면 import static을 이용해서 다음과 같이 사용할 수 있다.</p>
<pre><code class="language-java">import static jpabook.jpashop.domain.Qmember.member;    //기본인스턴스

public void queryDSL() {

    EntityManager em = emf.createEntityManager();

    JPAQuery query = new JPAQuery(em);
    List&lt;Member&gt; members = query.from(member)  // member1로 정적 정의한 QMember 사용
                                .where(member.name.eq(&quot;회원1&quot;))
                                .orderBy(member.name.desc())
                                .list(member);
}
</code></pre>
<h2 id="검색-조건-쿼리">검색 조건 쿼리</h2>
<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>
<pre><code class="language-sql">//실행된 JPQL
select item
from Item item
where item.name = ?1 and item.price &gt; ?2</code></pre>
<p>gt는 greater than의 약자로 queryDSL은 이와 같은 약자를 여러 이용한다. (eq는 equals)</p>
<p>이외에도 where() 에서 사용되는 메소드의 예시를 보자.</p>
<pre><code class="language-java">item.price.between(10000,20000);    //가격이 10000원 ~ 20000원
item.name.contains(&quot;상품1&quot;);            //&quot;상품1&quot;이라는 이름을 포함한 상품
                                    //SQL에서 like &#39;%상품1%&#39; 검색
item.name.startsWith(&quot;최고급&quot;);        //이름이 &quot;최고급&quot;으로 시작하는 상품
                                    //SQL에서 like &#39;최고급%&#39; 검색</code></pre>
<h2 id="결과-조회-쿼리">결과 조회 쿼리</h2>
<p>QueryDSL에서 결과를 조회할 때 사용할 수 있는 다양한 메서드들이 있습니다. 대표적인 메서드로는 <code>uniqueResult()</code>, <code>singleResult()</code>, <code>list()</code> 등이 있습니다. 각 메서드는 쿼리 결과가 하나일지 여러 개일지에 따라 선택하여 사용합니다.</p>
<h3 id="1-list-메서드">1. <code>list()</code> 메서드</h3>
<p><code>list()</code> 메서드는 <strong>여러 개의 결과</strong>를 반환할 때 사용됩니다. 반환되는 결과는 <code>List</code> 형태입니다.</p>
<h4 id="예시">예시:</h4>
<pre><code class="language-java">List&lt;Member&gt; members = query.from(qMember)
                            .where(qMember.name.eq(&quot;회원1&quot;))
                            .orderBy(qMember.name.desc())
                            .fetch(); // list() 메서드와 동일</code></pre>
<p>이 메서드는 결과가 여러 개일 때 유용하며, 조건에 맞는 모든 엔티티를 <code>List</code>로 반환합니다.</p>
<h3 id="2-uniqueresult-메서드">2. <code>uniqueResult()</code> 메서드</h3>
<p><code>uniqueResult()</code> 메서드는 <strong>단 하나의 결과</strong>만 반환할 때 사용됩니다. 이 메서드는 여러 개의 결과가 있을 경우 <code>IllegalStateException</code>을 던집니다.</p>
<h4 id="예시-1">예시:</h4>
<pre><code class="language-java">Member member = query.from(qMember)
                     .where(qMember.username.eq(&quot;kim&quot;))
                     .uniqueResult(); // 단 하나의 결과를 기대</code></pre>
<p>결과가 하나일 것으로 예상할 때 유용하며, 다수의 결과가 나올 경우 예외가 발생합니다.</p>
<h3 id="3-singleresult-메서드">3. <code>singleResult()</code> 메서드</h3>
<p><code>singleResult()</code> 메서드는 결과가 <strong>하나일 때만 정상적으로 결과를 반환</strong>합니다. 이 메서드도 결과가 여러 개일 경우 <code>javax.persistence.NoResultException</code>이나 <code>javax.persistence.NonUniqueResultException</code>을 던질 수 있습니다.</p>
<h4 id="예시-2">예시:</h4>
<pre><code class="language-java">Member member = query.from(qMember)
                     .where(qMember.username.eq(&quot;kim&quot;))
                     .singleResult();</code></pre>
<p>결과가 하나일 때 사용되며, 값이 없으면 <code>NoResultException</code>, 여러 개가 있으면 <code>NonUniqueResultException</code>을 던집니다.</p>
<h3 id="패키지">패키지</h3>
<p><code>QueryDSL</code>에서 결과 조회를 위한 클래스와 메서드는 보통 <code>com.querydsl.jpa.impl.JPAQuery</code> 패키지 내에서 제공됩니다. 따라서 해당 클래스에서 위 메서드들을 사용하여 쿼리 결과를 가져올 수 있습니다.</p>
<h4 id="예시-3">예시:</h4>
<pre><code class="language-java">import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.core.types.dsl.BooleanExpression;

JPAQuery&lt;Member&gt; query = new JPAQuery&lt;&gt;(entityManager);
QMember qMember = QMember.member;

List&lt;Member&gt; members = query.from(qMember)
                             .where(qMember.name.eq(&quot;회원1&quot;))
                             .orderBy(qMember.name.desc())
                             .fetch(); // list() 메서드</code></pre>
<h3 id="요약">요약</h3>
<ul>
<li><code>list()</code>는 여러 개의 결과를 반환하며, <code>List</code>로 결과를 받습니다.</li>
<li><code>uniqueResult()</code>는 단 하나의 결과를 기대할 때 사용하며, 결과가 없거나 여러 개일 경우 예외가 발생합니다.</li>
<li><code>singleResult()</code>는 결과가 정확히 하나여야 할 때 사용되며, 결과가 없거나 여러 개일 경우 예외를 던집니다.</li>
</ul>
<p>각 메서드는 쿼리의 특성과 기대하는 결과에 맞춰 적절히 선택하여 사용할 수 있습니다.</p>
<h2 id="페이징과-정렬">페이징과 정렬</h2>
<p>QueryDSL에서 <strong>페이징(Paging)</strong>과 <strong>정렬(Sorting)</strong>을 설정하는 방법을 알아보겠습니다. 페이징과 정렬은 대량 데이터를 처리할 때 매우 중요하며, QueryDSL을 사용하면 간편하게 구현할 수 있습니다.</p>
<h3 id="1-페이징paging">1. 페이징(Paging)</h3>
<p>페이징을 설정하려면 <code>offset()</code>과 <code>limit()</code> 메서드를 사용하여 조회 범위를 지정합니다. <code>offset()</code>은 조회를 시작할 위치를, <code>limit()</code>은 한 번에 조회할 개수를 설정합니다.</p>
<h4 id="예시-4">예시:</h4>
<pre><code class="language-java">List&lt;Member&gt; members = query.from(qMember)
                            .where(qMember.name.eq(&quot;회원1&quot;))
                            .offset(0) // 시작 위치
                            .limit(10) // 가져올 데이터 수
                            .fetch();</code></pre>
<ul>
<li><code>offset(0)</code>은 첫 번째 데이터부터 시작하도록 설정합니다.</li>
<li><code>limit(10)</code>은 10개의 결과만 가져오도록 설정합니다.</li>
</ul>
<h3 id="2-정렬sorting">2. 정렬(Sorting)</h3>
<p>정렬을 설정하려면 <code>orderBy()</code> 메서드를 사용합니다. <code>orderBy()</code> 안에는 정렬할 필드를 지정하고, 각 필드에 대해 오름차순(ascending) 또는 내림차순(descending)을 선택할 수 있습니다.</p>
<h4 id="예시-5">예시:</h4>
<pre><code class="language-java">List&lt;Member&gt; members = query.from(qMember)
                            .where(qMember.name.eq(&quot;회원1&quot;))
                            .orderBy(qMember.name.asc()) // 오름차순 정렬
                            .offset(0)
                            .limit(10)
                            .fetch();</code></pre>
<p>위 예제에서 <code>qMember.name.asc()</code>는 이름을 오름차순으로 정렬하는 것을 의미합니다. 내림차순으로 정렬하려면 <code>desc()</code>를 사용합니다.</p>
<h4 id="예시-내림차순">예시 (내림차순):</h4>
<pre><code class="language-java">List&lt;Member&gt; members = query.from(qMember)
                            .where(qMember.name.eq(&quot;회원1&quot;))
                            .orderBy(qMember.name.desc()) // 내림차순 정렬
                            .offset(0)
                            .limit(10)
                            .fetch();</code></pre>
<h3 id="페이징과-정렬을-결합한-예시">페이징과 정렬을 결합한 예시:</h3>
<p>페이징과 정렬을 함께 사용할 수도 있습니다. 다음은 이름을 내림차순으로 정렬하고, 첫 번째 페이지(10개의 항목)만 가져오는 예시입니다.</p>
<pre><code class="language-java">List&lt;Member&gt; members = query.from(qMember)
                            .where(qMember.name.eq(&quot;회원1&quot;))
                            .orderBy(qMember.name.desc()) // 이름 내림차순 정렬
                            .offset(0) // 첫 번째 페이지
                            .limit(10) // 10개 항목
                            .fetch();</code></pre>
<h3 id="요약-1">요약</h3>
<ul>
<li><strong>페이징</strong>: <code>offset()</code>과 <code>limit()</code>을 사용하여 데이터를 페이지 단위로 가져옵니다.</li>
<li><strong>정렬</strong>: <code>orderBy()</code>와 <code>asc()</code>, <code>desc()</code>를 사용하여 정렬을 적용합니다.</li>
<li><strong>결합 사용</strong>: 페이징과 정렬을 동시에 적용하여 원하는 데이터만 효율적으로 가져올 수 있습니다.</li>
</ul>
<p>이렇게 QueryDSL을 사용하면 페이징과 정렬을 간단하게 설정하여 데이터를 효율적으로 조회할 수 있습니다.</p>
<h3 id="결과-조회가-list가-아니라-fetch로-변경-4xx-버전부터">결과 조회가 list()가 아니라 fetch()로 변경 (4.xx 버전부터)</h3>
<p>책에서는 list()로 결과를 받았지만 위의 예시를 보면 fetch()를 사용했다. 이에 대한 설명은 다음과 같다.</p>
<p>fetch() 메서드는 QueryDSL 4.x 버전부터 도입된 메서드로, 결과를 실제로 조회하는 역할을 합니다. 이전 3.x 버전에서는 fetch() 대신 list()나 uniqueResult() 같은 메서드를 사용하여 결과를 얻었습니다.
QueryDSL 4.x 이상 버전에서는 결과 조회 시 <code>fetch()</code> 메서드를 사용하는 것이 표준입니다. 이전에는 <code>list()</code>와 <code>uniqueResult()</code>와 같은 메서드를 사용했으나, <code>fetch()</code> 메서드는 그 역할을 대체하며 더 직관적인 결과 조회 방식을 제공합니다. </p>
<h3 id="차이점">차이점:</h3>
<ul>
<li><strong><code>list()</code></strong>: 결과가 여러 개일 때 사용합니다. <code>list()</code>는 <code>fetch()</code> 메서드로 대체되었습니다.</li>
<li><strong><code>uniqueResult()</code></strong>: 결과가 하나만 있을 때 사용합니다. 결과가 하나가 아니면 예외가 발생합니다. <code>fetchOne()</code>이 그 역할을 합니다.</li>
<li><strong><code>fetch()</code></strong>: 4.x 버전에서는 <code>list()</code>와 같은 역할을 하며, 쿼리 실행 후 결과 리스트를 반환합니다.</li>
</ul>
<p>즉, 최신 버전에서는 <code>fetch()</code> 메서드를 사용하면 모든 결과 조회를 처리할 수 있기 때문에, 별도로 <code>list()</code>나 <code>uniqueResult()</code> 같은 메서드를 사용할 필요가 없습니다.</p>
<p>예시:</p>
<pre><code class="language-java">List&lt;Member&gt; members = query.from(qMember)
                             .where(qMember.name.eq(&quot;회원1&quot;))
                             .orderBy(qMember.name.desc())
                             .fetch(); // 결과 조회</code></pre>
<p>따라서 최신 버전에서는 <code>fetch()</code>를 사용하는 것이 일반적입니다.</p>
<h2 id="그룹-groubby-having">그룹 groubBy, having</h2>
<p>간단한 예시로 <code>groupBy</code>와 <code>having</code>을 사용하는 방법을 설명하겠습니다.</p>
<h3 id="예시-groupby와-having"><strong>예시: <code>groupBy</code>와 <code>having</code></strong></h3>
<pre><code class="language-java">QMember qMember = QMember.member;

List&lt;Tuple&gt; result = query.from(qMember)
                          .groupBy(qMember.age)
                          .having(qMember.age.gt(30))  // 나이가 30살 초과인 그룹만 선택
                          .list(qMember.age, qMember.count());</code></pre>
<ul>
<li><strong><code>groupBy(qMember.age)</code></strong>: <code>age</code>를 기준으로 그룹화합니다.</li>
<li><strong><code>having(qMember.age.gt(30))</code></strong>: 그룹화된 결과에서 나이가 30살 초과인 그룹만 필터링합니다.</li>
<li><strong><code>list(qMember.age, qMember.count())</code></strong>: <code>age</code>별로 몇 명이 있는지 카운트합니다.</li>
</ul>
<p>이 예제에서는 <code>groupBy</code>를 사용해 나이를 기준으로 그룹을 나눈 후, <code>having</code>을 사용해 나이가 30살 이상인 그룹만 필터링하고 있습니다.</p>
<h3 id="fetch-말고-list-사용-이유">fetch() 말고 list() 사용 이유</h3>
<p><code>fetch()</code>와 <code>list()</code>의 차이는 QueryDSL의 API에서 결과를 반환하는 방식에 따라 다릅니다. </p>
<h3 id="list와-fetch의-차이점"><code>list()</code>와 <code>fetch()</code>의 차이점</h3>
<ul>
<li><p><strong><code>list()</code></strong>: <code>groupBy</code>나 <code>having</code>을 사용할 때는 보통 <code>list()</code>를 사용합니다. <code>groupBy</code>로 그룹화된 결과를 반환하려면 <code>list()</code>가 가장 적합합니다. <code>groupBy</code>는 집계 결과를 반환하기 때문에 <code>Tuple</code>로 결과를 받습니다.</p>
</li>
<li><p><strong><code>fetch()</code></strong>: <code>fetch()</code>는 <code>Query</code> 실행 시 여러 개의 결과를 한 번에 처리하는 방식입니다. <code>fetch()</code>는 반환 결과가 <code>List&lt;T&gt;</code> 형태로 여러 개의 엔티티를 반환할 때 사용됩니다. <code>fetch()</code>는 <code>Query</code>에서 반환되는 결과가 <strong>단일 객체</strong>가 아닌 <strong>복수의 객체</strong>일 때 적합합니다.</p>
</li>
</ul>
<h3 id="groupby와-having에-list-사용-이유"><code>groupBy</code>와 <code>having</code>에 <code>list()</code> 사용 이유</h3>
<ul>
<li><code>groupBy</code>와 <code>having</code>은 <strong>집계</strong>를 의미하는 기능입니다. 결과는 여러 컬럼으로 그룹화된 결과를 반환하므로, <strong>여러 값</strong>을 포함한 <code>Tuple</code> 형태로 반환됩니다. <code>list()</code>는 여러 값이 포함된 결과를 처리하는 데 적합합니다.</li>
</ul>
<p>따라서 <code>groupBy</code>나 <code>having</code>은 결과가 <strong>여러 항목</strong>을 포함한 <strong>집계된 결과</strong>를 반환하므로 <code>fetch()</code> 대신 <code>list()</code>를 사용하는 것이 더 자연스럽습니다.</p>
<hr>
<h2 id="조인">조인</h2>
<p>조인(Join)은 관계형 데이터베이스에서 여러 테이블을 결합하는 방법을 말합니다. 데이터베이스 쿼리에서 조인을 사용하면, 여러 테이블에서 관련된 데이터를 함께 조회할 수 있습니다.</p>
<h3 id="주요-조인-유형">주요 조인 유형</h3>
<ol>
<li><p><strong>내부 조인(Inner Join)</strong>: </p>
<ul>
<li>기본적인 조인 방식으로, 두 테이블의 조건에 맞는 행만 반환합니다. 예를 들어, <code>Member</code>와 <code>Team</code> 테이블이 있을 때, 특정 <code>Member</code>가 속한 <code>Team</code> 정보를 조회하고 싶다면, 내부 조인을 사용하여 두 테이블에서 조건을 만족하는 데이터만 반환합니다.<pre><code class="language-java">JPAQuery query = new JPAQuery(em);
QMember qMember = QMember.member;
QTeam qTeam = QTeam.team;
List&lt;Member&gt; members = query.from(qMember)
                           .innerJoin(qMember.team, qTeam)
                           .where(qMember.name.eq(&quot;John&quot;))
                           .fetch();</code></pre>
</li>
</ul>
</li>
<li><p><strong>외부 조인(Outer Join)</strong>:</p>
<ul>
<li><strong>왼쪽 외부 조인(Left Outer Join)</strong>: 왼쪽 테이블의 모든 행을 반환하고, 오른쪽 테이블에서 조건을 만족하지 않는 행은 <code>NULL</code> 값을 가집니다. 예를 들어, 모든 <code>Member</code>를 조회하되, 해당하는 <code>Team</code>이 없는 <code>Member</code>도 포함시키고 싶을 때 사용합니다.</li>
<li><strong>오른쪽 외부 조인(Right Outer Join)</strong>: 오른쪽 테이블의 모든 행을 반환하고, 왼쪽 테이블에서 조건을 만족하지 않는 행은 <code>NULL</code> 값을 가집니다.<pre><code class="language-java">List&lt;Member&gt; members = query.from(qMember)
                           .leftJoin(qMember.team, qTeam)
                           .where(qTeam.name.isNull())
                           .fetch();</code></pre>
</li>
</ul>
</li>
<li><p><strong>크로스 조인(Cross Join)</strong>:</p>
<ul>
<li>두 테이블의 모든 행을 조합하여 반환합니다. 즉, 테이블의 첫 번째 행과 두 번째 테이블의 모든 행을 결합하여 결과를 만듭니다. 일반적으로 크로스 조인은 매우 큰 결과를 생성할 수 있기 때문에 잘 사용되지 않습니다.<pre><code class="language-java">List&lt;Tuple&gt; result = query.from(qMember)
                        .crossJoin(qTeam)
                        .fetch();</code></pre>
</li>
</ul>
</li>
<li><p><strong>자기 조인(Self Join)</strong>:</p>
<ul>
<li>같은 테이블을 조인하는 방식입니다. 예를 들어, 하나의 <code>Employee</code> 테이블에서 상사와 부하 직원 관계를 조회할 때 사용됩니다.<pre><code class="language-java">QEmployee qEmployee1 = QEmployee.employee;
QEmployee qEmployee2 = new QEmployee(&quot;manager&quot;);
List&lt;Employee&gt; employees = query.from(qEmployee1)
                               .leftJoin(qEmployee1.manager, qEmployee2)
                               .fetch();</code></pre>
</li>
</ul>
</li>
</ol>
<h3 id="querydsl에서의-조인">QueryDSL에서의 조인</h3>
<p>QueryDSL에서는 <code>join()</code>, <code>leftJoin()</code>, <code>innerJoin()</code> 등을 사용하여 조인을 구현할 수 있습니다. 각 메서드는 각각의 조인 유형을 지정할 수 있으며, 조인에 대한 추가 조건도 설정할 수 있습니다.</p>
<ol>
<li><p><strong>Inner Join</strong>:</p>
<pre><code class="language-java">JPAQuery query = new JPAQuery(em);
QMember qMember = QMember.member;
QTeam qTeam = QTeam.team;
List&lt;Member&gt; members = query.from(qMember)
                             .innerJoin(qMember.team, qTeam)
                             .fetch();</code></pre>
</li>
<li><p><strong>Left Join</strong>:</p>
<pre><code class="language-java">List&lt;Member&gt; members = query.from(qMember)
                             .leftJoin(qMember.team, qTeam)
                             .fetch();</code></pre>
</li>
</ol>
<h3 id="조인-조건-추가">조인 조건 추가</h3>
<p>조인에서 추가적인 조건을 설정할 수 있습니다. 예를 들어, <code>ON</code> 절을 사용해 조인 조건을 명확히 할 수 있습니다.</p>
<pre><code class="language-java">List&lt;Member&gt; members = query.from(qMember)
                             .leftJoin(qMember.team, qTeam).on(qTeam.name.eq(&quot;Team A&quot;))
                             .fetch();</code></pre>
<p>이와 같은 방법으로 QueryDSL에서는 다양한 방식의 조인을 사용하여 데이터를 조회할 수 있습니다.</p>
<h3 id="on-절-join-조건"><code>ON</code> 절 (Join 조건)</h3>
<p><code>ON</code> 절은 조인의 조건을 정의할 때 사용됩니다. 이 절은 <strong>내부 조인(Inner Join)</strong>, <strong>외부 조인(Outer Join)</strong> 등에서 유용하게 사용됩니다. 특히, 외부 조인에서는 <code>ON</code> 절을 사용하여 조건을 명시적으로 설정할 수 있으며, 이는 <code>WHERE</code> 절을 사용할 때와 다른 결과를 초래할 수 있습니다.</p>
<ul>
<li><p><strong>사용 예시</strong>:  
<code>ON</code> 절을 사용하면, 두 테이블 간의 조건을 보다 세밀하게 조정할 수 있습니다.</p>
<pre><code class="language-java">List&lt;Member&gt; members = query.from(qMember)
                             .leftJoin(qMember.team, qTeam).on(qTeam.name.eq(&quot;Team A&quot;))
                             .fetch();</code></pre>
</li>
<li><p><strong>특징</strong>:  </p>
<ul>
<li><code>ON</code> 절은 조인의 조건을 설정하는 데 사용되며, <strong><code>WHERE</code> 절과 다르게 필터링을 하지 않습니다</strong>. <code>WHERE</code> 절은 결과를 필터링하지만, <code>ON</code> 절은 조인하는 테이블 간의 관계를 정의합니다.</li>
<li>외부 조인의 경우, <code>ON</code> 절을 사용하여 조건을 설정하면 더 유연한 조인을 만들 수 있습니다. 예를 들어, <code>ON</code> 절에서 조건을 설정하면 외부 조인 결과로 <code>NULL</code> 값도 포함될 수 있습니다.</li>
</ul>
</li>
</ul>
<h3 id="페치-조인-fetch-join">페치 조인 (Fetch Join)</h3>
<p><strong>페치 조인</strong>은 연관된 엔티티들을 즉시 로딩(Eager Loading) 방식으로 가져오는 기법입니다. 페치 조인을 사용하면 관련 엔티티들을 한 번의 쿼리로 가져오므로, 별도로 추가 쿼리를 실행하는 지연 로딩(Lazy Loading)을 방지할 수 있습니다.</p>
<ul>
<li><p><strong>사용 예시</strong>:</p>
<pre><code class="language-java">List&lt;Member&gt; members = query.from(qMember)
                             .leftJoin(qMember.team, qTeam).fetch()
                             .where(qMember.name.eq(&quot;John&quot;))
                             .fetch();</code></pre>
<p>여기서 <code>fetch()</code> 메서드는 <code>Member</code> 엔티티와 <code>Team</code> 엔티티를 한 번에 불러오는 데 사용됩니다. 이 방법은 연관된 엔티티들을 즉시 로딩하고, SQL 쿼리의 수를 줄여 성능을 향상시킬 수 있습니다.</p>
</li>
<li><p><strong>특징</strong>:  </p>
<ul>
<li><strong>지연 로딩 회피</strong>: 페치 조인은 연관된 엔티티들을 즉시 로딩하므로 추가적인 쿼리 실행 없이 한 번에 모든 데이터를 조회할 수 있습니다.</li>
<li><strong>성능 개선</strong>: 연관된 엔티티를 여러 번 조회하는 대신, 한 번의 쿼리로 모든 데이터를 불러오기 때문에 성능을 크게 개선할 수 있습니다.</li>
<li><strong>주의사항</strong>: 페치 조인을 사용할 때, 엔티티 간에 중복된 데이터가 발생할 수 있습니다. 예를 들어, <code>Member</code> 엔티티와 연관된 <code>Team</code> 엔티티를 페치 조인하면 <code>Member</code>가 여러 번 중복되어 결과에 포함될 수 있습니다. 이를 피하려면 <code>distinct()</code>나 다른 방법을 사용해야 할 수 있습니다.</li>
</ul>
</li>
</ul>
<h3 id="on-절과-페치-조인의-차이"><code>ON</code> 절과 페치 조인의 차이</h3>
<ul>
<li><strong><code>ON</code> 절</strong>: 주로 외부 조인에서 조인 조건을 세밀하게 제어할 수 있게 해줍니다. 예를 들어, 외부 조인에서 특정 조건을 만족하는 데이터만 연결하여 불러오고 싶을 때 사용합니다.</li>
<li><strong>페치 조인</strong>: 연관된 엔티티들을 즉시 로딩하여 별도의 추가 쿼리를 실행하지 않도록 해주는 기법으로, 성능을 최적화하는 데 유용합니다. </li>
</ul>
<p>두 개념은 서로 다르지만, 함께 사용될 수도 있습니다. 예를 들어, 외부 조인과 페치 조인을 결합하여 관련 엔티티를 한 번에 로딩하면서, 조건을 <code>ON</code> 절을 사용하여 세밀하게 조정할 수 있습니다.</p>
<h2 id="서브-쿼리">서브 쿼리</h2>
<p>서브쿼리는 JPQL이나 QueryDSL에서 복잡한 쿼리를 작성할 때 <strong>다른 쿼리의 결과를 조건이나 값으로 사용하는 쿼리</strong>입니다. QueryDSL에서는 서브쿼리를 생성하기 위해 <strong>com.querydsl.jpa.JPAExpressions</strong> 클래스를 사용하며, 예전 버전에서 언급된 <strong>com.mysema.query.jpa.JPASubQuery</strong>는 더 이상 사용되지 않습니다.</p>
<hr>
<h3 id="querydsl에서-서브쿼리-생성">QueryDSL에서 서브쿼리 생성</h3>
<p>QueryDSL의 최신 버전에서는 <strong>JPAExpressions</strong>를 사용하여 서브쿼리를 작성합니다.</p>
<h4 id="서브쿼리-예시">서브쿼리 예시</h4>
<pre><code class="language-java">import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.JPAExpressions;

// 메인 엔티티와 서브쿼리용 엔티티
QMember member = QMember.member;
QMember subMember = new QMember(&quot;subMember&quot;);

// 서브쿼리 작성
JPAQuery&lt;Member&gt; query = new JPAQuery&lt;&gt;(entityManager);
List&lt;Member&gt; result = query.select(member)
    .from(member)
    .where(member.age.eq(
        JPAExpressions.select(subMember.age.max())
            .from(subMember)
    ))
    .fetch();</code></pre>
<h4 id="생성된-jpql">생성된 JPQL</h4>
<pre><code class="language-sql">SELECT m.*
FROM Member m
WHERE m.age = (
    SELECT MAX(sub_m.age)
    FROM Member sub_m
);</code></pre>
<h3 id="서브쿼리-주요-구성-요소">서브쿼리 주요 구성 요소</h3>
<ol>
<li><p><strong>JPAExpressions</strong></p>
<ul>
<li>서브쿼리 생성에 필요한 빌더 클래스.</li>
<li><code>select</code>, <code>from</code>, <code>where</code> 등을 사용하여 서브쿼리 작성.</li>
</ul>
</li>
<li><p><strong>독립된 Q 객체</strong></p>
<ul>
<li>서브쿼리에서 사용하는 엔티티와 메인 쿼리의 엔티티가 동일한 경우, <strong>다른 별칭</strong>을 가진 Q 객체를 만들어야 충돌을 방지할 수 있습니다.</li>
<li>예: <code>new QMember(&quot;subMember&quot;)</code></li>
</ul>
</li>
</ol>
<h3 id="서브쿼리의-사용-목적">서브쿼리의 사용 목적</h3>
<ul>
<li><strong>조건 쿼리</strong>: 특정 값이 서브쿼리의 결과와 일치하는지 비교.</li>
<li><strong>집계 함수</strong>: MAX, MIN, AVG 같은 집계 결과를 사용.</li>
<li><strong>중첩 필터링</strong>: 서브쿼리에서 필터링된 결과를 메인 쿼리에 전달.</li>
</ul>
<h3 id="주의점">주의점</h3>
<ol>
<li><strong>SQL 제약</strong>: 서브쿼리는 SQL 실행 시 성능에 영향을 미칠 수 있습니다. 가능한 경우, <strong>JOIN</strong>을 고려하여 쿼리를 단순화하세요.</li>
<li><strong>QueryDSL 버전 차이</strong>: QueryDSL 3.x 이하에서는 <code>JPASubQuery</code>를 사용했지만, 현재는 <code>JPAExpressions</code>로 변경되었습니다.</li>
</ol>
<h3 id="더-알아보기">더 알아보기</h3>
<ul>
<li><strong>QueryDSL 서브쿼리 공식 문서</strong>: <a href="https://querydsl.com">https://querydsl.com</a></li>
<li>JPA 표준 서브쿼리 관련 내용은 <strong>JPQL 문서</strong>에서 확인 가능합니다.</li>
</ul>
<h2 id="여러-컬럼-반환과-튜플">여러 컬럼 반환과 튜플</h2>
<p>QueryDSL에서 여러 컬럼을 반환하거나 튜플 형태로 결과를 조회하는 방법은 <strong>com.querydsl.core.Tuple</strong>을 활용합니다. 이 방식은 특정 엔티티가 아닌 여러 컬럼 값이나 계산된 값을 동시에 조회할 때 유용합니다.</p>
<hr>
<h3 id="여러-컬럼-반환-예제">여러 컬럼 반환 예제</h3>
<h4 id="예제-코드">예제 코드</h4>
<pre><code class="language-java">import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.core.Tuple;

// Q 클래스 생성
QMember member = QMember.member;

// QueryDSL 쿼리 작성
JPAQuery&lt;Tuple&gt; query = new JPAQuery&lt;&gt;(entityManager);
List&lt;Tuple&gt; results = query.select(member.id, member.username, member.age)
    .from(member)
    .where(member.age.gt(20))
    .fetch();

// 결과 처리
for (Tuple tuple : results) {
    Long id = tuple.get(member.id);
    String username = tuple.get(member.username);
    Integer age = tuple.get(member.age);

    System.out.println(&quot;ID: &quot; + id + &quot;, Username: &quot; + username + &quot;, Age: &quot; + age);
}</code></pre>
<h4 id="생성되는-jpql">생성되는 JPQL</h4>
<pre><code class="language-sql">SELECT member.id, member.username, member.age
FROM Member member
WHERE member.age &gt; 20;</code></pre>
<hr>
<h3 id="주요-클래스와-메서드">주요 클래스와 메서드</h3>
<ol>
<li><p><strong><code>com.querydsl.core.Tuple</code></strong></p>
<ul>
<li>여러 컬럼을 반환할 때 사용하는 객체.</li>
<li><code>get()</code> 메서드를 사용해 결과를 추출합니다.</li>
<li>반환 타입은 컬럼의 데이터 타입에 따라 달라짐.</li>
</ul>
</li>
<li><p><strong><code>select()</code></strong></p>
<ul>
<li>반환할 컬럼을 나열하며, 복수 개의 필드를 선택할 수 있음.</li>
</ul>
</li>
<li><p><strong><code>fetch()</code></strong></p>
<ul>
<li>쿼리를 실행하고 결과를 리스트로 반환.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="여러-컬럼-반환을-사용하는-이유">여러 컬럼 반환을 사용하는 이유</h3>
<ol>
<li><strong>복수 필드 조회</strong>: 엔티티 전체 대신 필요한 필드만 조회하여 성능 최적화.</li>
<li><strong>계산된 값</strong>: SQL에서 집계 함수나 연산 결과를 조회 가능.</li>
<li><strong>엔티티와 비엔티티 데이터 혼합 조회</strong>: 엔티티 필드와 계산된 값을 함께 반환할 수 있음.</li>
</ol>
<hr>
<h3 id="튜플을-활용할-때의-주의사항">튜플을 활용할 때의 주의사항</h3>
<ol>
<li><strong>직접적인 엔티티 반환이 아님</strong>: 튜플은 엔티티가 아닌 컬럼 값만 포함.</li>
<li><strong>컬럼 접근 방식</strong>: Q 클래스의 필드를 기반으로 <code>get()</code> 메서드 사용.</li>
<li><strong>타입 안정성</strong>: 컬럼의 데이터 타입에 따라 적절히 처리해야 함.</li>
</ol>
<hr>
<h3 id="예제-튜플과-집계-함수">예제: 튜플과 집계 함수</h3>
<pre><code class="language-java">List&lt;Tuple&gt; results = query.select(member.team.name, member.age.avg())
    .from(member)
    .groupBy(member.team.name)
    .fetch();

for (Tuple tuple : results) {
    String teamName = tuple.get(member.team.name);
    Double avgAge = tuple.get(member.age.avg());

    System.out.println(&quot;Team: &quot; + teamName + &quot;, Avg Age: &quot; + avgAge);
}</code></pre>
<h4 id="생성되는-jpql-1">생성되는 JPQL</h4>
<pre><code class="language-sql">SELECT team.name, AVG(member.age)
FROM Member member
GROUP BY team.name;</code></pre>
<hr>
<h3 id="대안-dto-반환">대안: DTO 반환</h3>
<p>Tuple 대신 DTO를 사용하는 방식도 권장됩니다. QueryDSL의 <code>Projections</code> API를 활용하면 특정 DTO로 결과를 매핑할 수 있습니다.</p>
<pre><code class="language-java">List&lt;MemberDTO&gt; results = query.select(
        Projections.constructor(MemberDTO.class, member.id, member.username, member.age))
    .from(member)
    .fetch();</code></pre>
<hr>
<h3 id="queryfrom-vs-queryselect">query.from() VS query.select()</h3>
<blockquote>
<p>query.from()이 아니라 query.select()을 사용하네? 버전 바뀌면서 바뀐부분이야?</p>
</blockquote>
<p>QueryDSL의 메서드 호출 순서는 버전에 따라 약간의 차이가 있지만, <code>query.select()</code>와 <code>query.from()</code>의 순서는 기본적인 QueryDSL 사용 방식에 따라 달라지는 부분입니다. </p>
<hr>
<h3 id="1-querydsl-3x-이전">1. QueryDSL 3.x 이전</h3>
<p>과거 QueryDSL 3.x 버전에서는 <code>from()</code>을 먼저 호출하고 이후에 <code>select()</code> 없이 바로 결과를 처리하거나 필터링하는 방식이 일반적이었습니다. 예를 들어:</p>
<pre><code class="language-java">List&lt;Member&gt; members = query.from(qMember)
    .where(qMember.username.eq(&quot;회원1&quot;))
    .orderBy(qMember.username.desc())
    .list(qMember);</code></pre>
<p>이 방식에서는 <code>list()</code> 메서드가 결과를 조회하는 핵심이었고, <code>select()</code>를 별도로 지정하지 않아도 <code>from()</code>의 대상을 기본적으로 반환 대상으로 처리했습니다.</p>
<hr>
<h3 id="2-querydsl-4x-이후">2. QueryDSL 4.x 이후</h3>
<p>QueryDSL 4.x 이후 버전에서는 <strong><code>fetch()</code></strong>가 도입되고, <code>select()</code>를 명시적으로 호출하여 반환할 필드를 지정하는 방식으로 변경되었습니다. 최신 방식에서는 다음과 같이 작성합니다:</p>
<pre><code class="language-java">List&lt;Member&gt; members = query.select(qMember)
    .from(qMember)
    .where(qMember.username.eq(&quot;회원1&quot;))
    .orderBy(qMember.username.desc())
    .fetch();</code></pre>
<p>이 방식은 명시적으로 반환 대상을 설정하므로 코드 가독성과 의도를 명확히 하는 데 기여합니다.</p>
<hr>
<h3 id="변경-이유">변경 이유</h3>
<ul>
<li><strong>가독성 향상</strong>: 반환할 필드를 명확히 지정함으로써 쿼리 의도를 더 쉽게 파악 가능.</li>
<li><strong>표준화</strong>: 다양한 반환 대상(Tuple, DTO, 특정 필드 등)을 일관되게 처리할 수 있도록 통합.</li>
<li><strong>결과 조회 방식 변경</strong>: <code>list()</code>, <code>uniqueResult()</code> 등의 메서드가 사라지고, <code>fetch()</code>, <code>fetchOne()</code>, <code>fetchFirst()</code> 등의 새로운 결과 조회 API로 통일.</li>
</ul>
<hr>
<h3 id="결론">결론</h3>
<p><code>select()</code>와 <code>fetch()</code>를 사용하는 최신 방식은 QueryDSL 4.x 이후의 표준입니다. 따라서, <code>from()</code>만 사용하는 방식은 이전 버전의 스타일이며, 최신 버전을 사용하는 경우에는 <code>select()</code>를 명시적으로 사용해야 합니다. </p>
<h3 id="참고-사항">참고 사항</h3>
<ul>
<li>QueryDSL 3.x의 경우 <code>querydsl-jpa</code>와 <code>querydsl-apt</code> 설정 방식이 달라질 수 있습니다.</li>
<li>최신 문법은 공식 <a href="http://www.querydsl.com/documentation/">QueryDSL 문서</a>를 참고하세요.</li>
</ul>
<h2 id="빈-생성-bean-pupulation">빈 생성 Bean pupulation</h2>
<p>쿼리 결과를 엔티티가 아닌 특정 객체로 받고 싶으면 <strong>빈 생성</strong> 기능을 사용한다. 
(책에서는 com.mysema.query.types.Projections를 사용한다고 하는데 queryDSL 버전이 업 되면서 사용 안 한다고 함)</p>
<p>아래는 <code>ItemDTO</code>와 실제 <code>Item</code> 엔티티에서 <code>name</code> 필드를 <code>username</code>으로 매핑하여 사용하는 QueryDSL 예제입니다. 여기서는 <code>as()</code> 메서드를 활용해 필드 이름을 별칭으로 지정하여 DTO로 매핑합니다.</p>
<h3 id="item-엔티티-itemdto"><code>Item</code> 엔티티, ItemDTO</h3>
<pre><code class="language-java">@Entity
public class Item {

    @Id @GeneratedValue
    private Long id;

    private String name;
    private int price;

    // Getter, Setter, Constructor
    public Item() {}

    public Item(String name, int price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

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

    public int getPrice() {
        return price;
    }

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

//ItemDTO
public class ItemDTO {

    private String username;
    private int price;

    public ItemDTO() {}

    public ItemDTO(String username, int price) {
        this.username = username;
        this.price = price;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}</code></pre>
<hr>
<h3 id="querydsl-예제-dto-매핑-및-별칭-사용">QueryDSL 예제: DTO 매핑 및 별칭 사용</h3>
<h3 id="1-프로퍼티-접근-projectionsbean">1. 프로퍼티 접근 (Projections.bean)</h3>
<pre><code class="language-java">public void queryWithAlias(EntityManager em) {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    QItem qItem = QItem.item;

    // 프로퍼티 접근 (Setter)
    List&lt;ItemDTO&gt; results = queryFactory
        .select(Projections.bean(
            ItemDTO.class,
            qItem.name.as(&quot;username&quot;), // name 필드를 username으로 매핑
            qItem.price
        ))
        .from(qItem)
        .where(qItem.price.gt(1000)) // 예시 조건: 가격이 1000 이상인 경우
        .fetch();

    for (ItemDTO dto : results) {
        System.out.println(&quot;Username: &quot; + dto.getUsername() + &quot;, Price: &quot; + dto.getPrice());
    }
}</code></pre>
<hr>
<h3 id="주요-포인트">주요 포인트</h3>
<ol>
<li><p><strong><code>Projections.bean()</code> 사용</strong>:</p>
<ul>
<li>DTO 매핑 시 프로퍼티 기반 매핑(Setter 사용)을 위해 <code>bean()</code> 메서드를 사용했습니다.</li>
<li>DTO의 프로퍼티와 엔티티 필드 이름이 다를 경우 <code>as(&quot;alias&quot;)</code>를 이용해 별칭을 지정합니다.</li>
</ul>
</li>
<li><p><strong>별칭(<code>as</code>) 사용</strong>:</p>
<ul>
<li><code>qItem.name.as(&quot;username&quot;)</code>로 <code>name</code> 필드를 DTO의 <code>username</code> 프로퍼티에 매핑.</li>
<li>이 방식은 DTO에서 엔티티 필드 이름과 다른 이름을 사용할 때 유용합니다.</li>
</ul>
</li>
<li><p><strong>조건</strong>:</p>
<ul>
<li><code>where(qItem.price.gt(1000))</code>로 조건을 추가. 예제에서는 가격이 1000 이상인 항목만 필터링.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="실행-결과-예시">실행 결과 (예시)</h3>
<p><code>Item</code> 엔티티에 다음 데이터가 있다고 가정:</p>
<table>
<thead>
<tr>
<th>ID</th>
<th>name</th>
<th>price</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>&quot;ItemA&quot;</td>
<td>1500</td>
</tr>
<tr>
<td>2</td>
<td>&quot;ItemB&quot;</td>
<td>900</td>
</tr>
</tbody></table>
<p>출력:</p>
<pre><code>Username: ItemA, Price: 1500</code></pre><p>이 방식은 QueryDSL의 DTO 매핑 기능을 활용해 직관적이고 간결한 쿼리 작성을 가능하게 합니다.</p>
<hr>
<h3 id="2-필드-직접-접근-projectionsfields">2. <strong>필드 직접 접근 (<code>Projections.fields</code>)</strong></h3>
<p>이 방식은 DTO 클래스에 Getter/Setter 없이 필드에 직접 값을 할당합니다.</p>
<pre><code class="language-java">public void queryWithFieldsProjection(EntityManager em) {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    QItem qItem = QItem.item;

    List&lt;ItemDTO&gt; results = queryFactory
        .select(Projections.fields(
            ItemDTO.class,
            qItem.name.as(&quot;username&quot;), // 필드 이름 매핑
            qItem.price
        ))
        .from(qItem)
        .where(qItem.price.gt(1000)) // 가격 1000 이상 필터링
        .fetch();

    for (ItemDTO dto : results) {
        System.out.println(&quot;Username: &quot; + dto.getUsername() + &quot;, Price: &quot; + dto.getPrice());
    }
}</code></pre>
<ul>
<li><strong>장점</strong>: DTO 클래스에서 Setter 메서드가 없어도 동작합니다.</li>
<li><strong>필드 이름 매핑</strong>: <code>as(&quot;alias&quot;)</code>를 사용해 DTO 필드 이름과 엔티티 필드 이름이 다를 경우 별칭을 설정합니다.</li>
</ul>
<hr>
<h3 id="2-생성자-기반-매핑-projectionsconstructor">2. <strong>생성자 기반 매핑 (<code>Projections.constructor</code>)</strong></h3>
<p>이 방식은 DTO의 생성자를 호출하여 값을 전달합니다. 생성자에 필요한 모든 필드를 명시적으로 지정해야 합니다.</p>
<pre><code class="language-java">public void queryWithConstructorProjection(EntityManager em) {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    QItem qItem = QItem.item;

    List&lt;ItemDTO&gt; results = queryFactory
        .select(Projections.constructor(
            ItemDTO.class,
            qItem.name,   // 생성자 첫 번째 파라미터
            qItem.price   // 생성자 두 번째 파라미터
        ))
        .from(qItem)
        .where(qItem.price.gt(1000)) // 가격 1000 이상 필터링
        .fetch();

    for (ItemDTO dto : results) {
        System.out.println(&quot;Username: &quot; + dto.getUsername() + &quot;, Price: &quot; + dto.getPrice());
    }
}</code></pre>
<ul>
<li><strong>장점</strong>: DTO가 불변 객체(Immutable)여야 하거나 모든 값을 생성자에서 설정해야 할 경우 사용됩니다.</li>
<li><strong>주의사항</strong>: DTO에 정의된 생성자 파라미터 순서와 타입이 쿼리에서 지정한 순서와 정확히 일치해야 합니다.</li>
</ul>
<hr>
<h3 id="3-프로퍼티-접근-projectionsbean">3. <strong>프로퍼티 접근 (<code>Projections.bean</code>)</strong></h3>
<p>이미 위에서 보여준 예제입니다. Setter 메서드를 통해 값을 할당하는 방식입니다.</p>
<pre><code class="language-java">public void queryWithBeanProjection(EntityManager em) {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    QItem qItem = QItem.item;

    List&lt;ItemDTO&gt; results = queryFactory
        .select(Projections.bean(
            ItemDTO.class,
            qItem.name.as(&quot;username&quot;), // 프로퍼티 매핑
            qItem.price
        ))
        .from(qItem)
        .where(qItem.price.gt(1000))
        .fetch();

    for (ItemDTO dto : results) {
        System.out.println(&quot;Username: &quot; + dto.getUsername() + &quot;, Price: &quot; + dto.getPrice());
    }
}</code></pre>
<hr>
<h3 id="각-방식의-비교">각 방식의 비교</h3>
<table>
<thead>
<tr>
<th>방식</th>
<th>특징</th>
<th>사용 사례</th>
</tr>
</thead>
<tbody><tr>
<td><code>Projections.bean</code></td>
<td>Setter 메서드를 통해 값을 설정. 필드 이름 매핑 필요.</td>
<td>DTO 필드 이름과 엔티티 필드 이름이 다른 경우. 일반적인 Setter 기반 클래스.</td>
</tr>
<tr>
<td><code>Projections.fields</code></td>
<td>필드에 직접 값을 할당. Getter/Setter 필요 없음.</td>
<td>DTO 필드에 바로 값을 설정하는 경우. Setter를 사용하지 않으려는 경우.</td>
</tr>
<tr>
<td><code>Projections.constructor</code></td>
<td>생성자를 호출하여 값을 설정. 파라미터 순서와 타입이 일치해야 함.</td>
<td>DTO가 불변 객체이거나 모든 값이 생성자에서만 설정되는 경우.</td>
</tr>
</tbody></table>
<hr>
<h4 id="distinct">DISTINCT</h4>
<p>distinct는 다음과 같이 사용한다.</p>
<pre><code class="language-java">query.distinct().from(item)...</code></pre>
<p>QueryDSL에서 <code>distinct()</code>를 사용하는 방식은 현재 버전에서도 여전히 동일하게 동작합니다. QueryDSL의 API는 <code>distinct()</code> 메서드를 통해 JPQL의 <code>DISTINCT</code> 키워드를 추가할 수 있도록 설계되어 있습니다.</p>
<p>예를 들어:</p>
<pre><code class="language-java">List&lt;String&gt; result = queryFactory
    .select(item.name)
    .distinct() // DISTINCT 키워드 추가
    .from(item)
    .fetch();</code></pre>
<p>위 코드는 <code>SELECT DISTINCT</code> 쿼리를 생성합니다.</p>
<hr>
<h3 id="변경점에-대한-설명">변경점에 대한 설명</h3>
<p><code>distinct()</code> 메서드 사용 자체는 버전 3.x와 5.x 사이에서 변화하지 않았습니다. 다만, <strong>QueryDSL 버전 4.x 이상부터 <code>fetch()</code> 사용이 권장되며</strong> 더 이상 <code>list()</code> 등의 메서드를 사용하지 않는 점이 관련 문법 변화입니다.</p>
<h3 id="querydsl-5x에서의-사용">QueryDSL 5.x에서의 사용</h3>
<p>현재 최신 QueryDSL에서도 <code>distinct()</code>를 사용하는 방법은 위와 같으며, 아래처럼 페이징, 정렬, 서브쿼리 등과 함께 사용이 가능합니다.</p>
<pre><code class="language-java">List&lt;ItemDTO&gt; result = queryFactory
    .select(Projections.bean(ItemDTO.class, item.name, item.price))
    .distinct()
    .from(item)
    .where(item.price.gt(1000))
    .orderBy(item.name.asc())
    .fetch();</code></pre>
<hr>
<h3 id="요약-2">요약</h3>
<ol>
<li><code>query.distinct()</code>의 사용법은 현재 버전에서도 동일합니다.</li>
<li>QueryDSL 4.x 이후부터는 <code>fetch()</code>를 사용해 데이터를 조회하는 것이 권장됩니다.</li>
<li>QueryDSL의 다른 기능과 <code>distinct()</code>를 조합해 사용할 수 있습니다.</li>
</ol>
<p>버전에 따라 기본적인 동작 방식은 유지되므로 최신 버전에서도 이와 같은 방법으로 활용 가능합니다.</p>
<h2 id="수정-삭제-배치-쿼리">수정, 삭제 배치 쿼리</h2>
<p>QueryDSL에서 <strong>수정(Update)와 삭제(Delete) 배치 쿼리</strong>는 대량의 데이터 변경 작업을 효율적으로 처리하기 위한 방법으로, JPQL이나 SQL의 배치 쿼리와 유사한 방식으로 동작합니다.</p>
<p>QueryDSL에서는 수정 및 삭제 배치 작업을 수행할 때 <code>com.querydsl.jpa.impl.JPAUpdateClause</code>와 <code>JPADeleteClause</code> 클래스를 사용합니다.</p>
<hr>
<h3 id="1-수정update-배치-쿼리">1. <strong>수정(Update) 배치 쿼리</strong></h3>
<p>수정 배치 쿼리는 여러 행의 데이터를 한 번에 변경하는 작업입니다. QueryDSL에서 <code>JPAUpdateClause</code>를 사용하여 수행됩니다.</p>
<h4 id="예제-코드-1">예제 코드:</h4>
<pre><code class="language-java">long updatedCount = new JPAUpdateClause(entityManager, qItem)
    .where(qItem.price.lt(1000)) // 가격이 1000보다 작은 항목만
    .set(qItem.price, 2000)      // 가격을 2000으로 업데이트
    .execute();</code></pre>
<h4 id="생성되는-sql">생성되는 SQL:</h4>
<pre><code class="language-sql">UPDATE item
SET price = 2000
WHERE price &lt; 1000;</code></pre>
<ul>
<li><strong><code>set</code> 메서드</strong>: 특정 필드 값을 설정.</li>
<li><strong><code>where</code> 조건</strong>: 어떤 데이터를 수정할지 결정.</li>
<li><strong><code>execute</code> 메서드</strong>: 배치 작업 실행, 영향을 받은 행 수를 반환.</li>
</ul>
<hr>
<h3 id="2-삭제delete-배치-쿼리">2. <strong>삭제(Delete) 배치 쿼리</strong></h3>
<p>삭제 배치 쿼리는 조건에 맞는 데이터를 한 번에 삭제합니다. QueryDSL에서 <code>JPADeleteClause</code>를 사용하여 수행됩니다.</p>
<h4 id="예제-코드-2">예제 코드:</h4>
<pre><code class="language-java">long deletedCount = new JPADeleteClause(entityManager, qItem)
    .where(qItem.price.lt(500)) // 가격이 500보다 작은 항목만 삭제
    .execute();</code></pre>
<h4 id="생성되는-sql-1">생성되는 SQL:</h4>
<pre><code class="language-sql">DELETE FROM item
WHERE price &lt; 500;</code></pre>
<ul>
<li><strong><code>where</code> 조건</strong>: 어떤 데이터를 삭제할지 결정.</li>
<li><strong><code>execute</code> 메서드</strong>: 배치 작업 실행, 영향을 받은 행 수를 반환.</li>
</ul>
<hr>
<h3 id="3-주의사항">3. <strong>주의사항</strong></h3>
<ul>
<li><strong>영속성 컨텍스트와의 동기화</strong>: <ul>
<li>배치 쿼리는 영속성 컨텍스트를 무시하고 직접 데이터베이스를 변경합니다.</li>
<li>따라서, <strong>영속성 컨텍스트에 있는 데이터와 데이터베이스의 상태가 불일치</strong>할 수 있으므로, 배치 쿼리 실행 후에는 <code>EntityManager.clear()</code>를 호출하여 영속성 컨텍스트를 초기화하는 것이 권장됩니다.</li>
</ul>
</li>
</ul>
<h4 id="예시-6">예시:</h4>
<pre><code class="language-java">entityManager.clear();</code></pre>
<ul>
<li><strong>트랜잭션</strong>: 배치 쿼리는 데이터베이스에 직접 반영되므로 트랜잭션 안에서 실행해야 데이터 무결성을 유지할 수 있습니다.</li>
</ul>
<hr>
<h3 id="4-실제-사용-예">4. <strong>실제 사용 예</strong></h3>
<p>수정 및 삭제 배치 쿼리는 대량의 데이터 처리, 데이터 정리, 조건 기반 삭제 작업 등에 자주 사용됩니다. 성능 향상을 위해 엔티티 객체를 개별적으로 변경하는 대신 한 번의 쿼리로 처리할 수 있다는 점이 주요 장점입니다.</p>
<h2 id="동적-쿼리">동적 쿼리</h2>
<p>QueryDSL에서 <strong>동적 쿼리(Dynamic Query)</strong>는 조건에 따라 쿼리를 유연하게 구성하는 방법입니다. 동적 쿼리는 주로 사용자가 선택하는 조건에 따라 쿼리 내용이 달라지거나, 조건이 있을 때만 쿼리를 추가하는 방식으로 처리됩니다.</p>
<p>예전에는 <code>BooleanBuilder</code>를 많이 사용하여 조건을 동적으로 구성했지만, 최신 버전의 QueryDSL에서는 이를 다른 방식으로 처리할 수 있습니다. <code>BooleanBuilder</code>는 여러 조건을 논리적으로 결합하는 데 유용한 도구였지만, 이제는 <code>BooleanExpression</code>이나 <code>Predicate</code>를 직접 사용하거나 <code>where()</code> 메서드를 체이닝하는 방식으로 동적 쿼리를 구성할 수 있습니다.</p>
<h3 id="동적-쿼리-구성-방법">동적 쿼리 구성 방법:</h3>
<ol>
<li><p><strong><code>BooleanExpression</code> 사용</strong>:
<code>BooleanExpression</code>은 조건을 동적으로 추가할 때 유용한 QueryDSL의 기본 조건식입니다. 이를 통해 조건을 설정하고, 여러 조건을 조합할 수 있습니다.</p>
<p>예시:</p>
<pre><code class="language-java">QMember qMember = QMember.member;
BooleanExpression usernameEq = qMember.username.eq(&quot;kim&quot;);
BooleanExpression ageGoe = qMember.age.goe(30);

// 동적 쿼리 구성
JPAQuery query = new JPAQuery&lt;&gt;(entityManager);
List&lt;Member&gt; members = query.select(qMember)
                            .from(qMember)
                            .where(usernameEq.and(ageGoe)) // 조건을 결합
                            .fetch();</code></pre>
</li>
</ol>
<p><code>goe</code>는 QueryDSL에서 사용되는 메서드로, <code>greaterThanEqual</code>의 약어입니다. 이 메서드는 &quot;이상&quot; 조건을 나타내며, 주어진 값보다 크거나 같은 값인지 확인합니다. 예를 들어, <code>qMember.age.goe(30)</code>은 <code>age</code> 필드가 30 이상인 경우를 찾는 조건을 의미합니다.</p>
<p>즉, <code>goe</code>는 <strong>greater than or equal to</strong>를 표현하며, JPA에서 사용할 수 있는 범위 조건 중 하나입니다. 이와 유사한 다른 조건들도 존재합니다:</p>
<ul>
<li><code>gt</code>: greater than (초과)</li>
<li><code>lt</code>: less than (미만)</li>
<li><code>loe</code>: less than or equal to (이하)</li>
</ul>
<p>이 메서드는 숫자뿐만 아니라 날짜와 문자열 비교에도 사용할 수 있습니다.</p>
<ol start="2">
<li><p><strong>조건에 따라 <code>where()</code> 체이닝</strong>:
조건이 있을 때만 <code>where()</code> 절을 추가하여 쿼리의 조건을 유동적으로 변경할 수 있습니다. 이 방식은 코드가 깔끔하고 이해하기 쉽습니다.</p>
<p>예시:</p>
<pre><code class="language-java">JPAQuery query = new JPAQuery&lt;&gt;(entityManager);
QMember qMember = QMember.member;

// 동적 조건
BooleanBuilder builder = new BooleanBuilder();
if (username != null) {
    builder.and(qMember.username.eq(username));
}
if (age != null) {
    builder.and(qMember.age.goe(age));
}

// 쿼리 실행
List&lt;Member&gt; members = query.select(qMember)
                            .from(qMember)
                            .where(builder)  // 동적 조건 추가
                            .fetch();</code></pre>
</li>
<li><p><strong><code>Predicate</code> 사용</strong>:
<code>Predicate</code>는 쿼리에서 조건을 표현할 때 사용되며, Java의 <code>java.util.function.Predicate</code> 인터페이스를 기반으로 동적 조건을 작성할 수 있습니다. <code>Predicate</code>는 <code>BooleanExpression</code>보다 좀 더 유연하게 쿼리 조건을 작성할 수 있습니다.</p>
<p>예시:</p>
<pre><code class="language-java">QMember qMember = QMember.member;

Predicate predicate = qMember.username.eq(&quot;kim&quot;).and(qMember.age.goe(30));

List&lt;Member&gt; members = new JPAQuery&lt;&gt;(entityManager)
                        .select(qMember)
                        .from(qMember)
                        .where(predicate)
                        .fetch();</code></pre>
</li>
</ol>
<h3 id="동적-쿼리의-장점">동적 쿼리의 장점:</h3>
<ul>
<li><strong>유연성</strong>: 조건에 따라 쿼리 구조를 유동적으로 변경할 수 있어 다양한 상황에 대응할 수 있습니다.</li>
<li><strong>성능 향상</strong>: 조건을 동적으로 추가하므로 불필요한 쿼리 조건을 피할 수 있어 성능을 최적화할 수 있습니다.</li>
<li><strong>가독성</strong>: 코드에서 조건을 명확하게 분리하고, 조건을 체이닝하거나 빌더 패턴을 사용할 수 있어 코드가 더 직관적이고 유지보수가 쉬워집니다.</li>
</ul>
<h3 id="결론-1">결론:</h3>
<p>QueryDSL의 최신 버전에서는 <code>BooleanBuilder</code> 대신 <code>BooleanExpression</code>이나 <code>Predicate</code>를 사용하여 동적 쿼리를 구성합니다. 이를 통해 유연하고 확장 가능한 쿼리 생성이 가능하며, 코드가 간결하고 가독성이 좋습니다.</p>
<h2 id="메소드-위임-delegate-methods">메소드 위임 Delegate methods</h2>
<p><code>@QueryDelegate</code>는 QueryDSL에서 특정 엔티티에 대한 커스텀 메서드를 생성하는 기능입니다. 이 어노테이션을 사용하면, 특정 엔티티 클래스에 메서드 위임을 정의할 수 있습니다. 이를 통해, 엔티티 클래스에서 쿼리 로직을 더 간결하고 재사용 가능한 형태로 만들 수 있습니다.</p>
<h3 id="메서드-위임-예시">메서드 위임 예시:</h3>
<p>예를 들어, <code>Item</code> 엔티티에 대한 <code>isExpensive</code>라는 조건을 쿼리에서 쉽게 사용할 수 있도록 메서드를 위임한다고 할 때, 다음과 같은 방식으로 사용할 수 있습니다.</p>
<pre><code class="language-java">@QueryDelegate(Item.class)
public static BooleanExpression isExpensive(QItem item, Integer price) {
    return item.price.gt(price); // 가격이 특정 값 이상인 항목을 찾는 메서드
}</code></pre>
<p>위와 같은 방식으로 <code>@QueryDelegate</code>를 사용하면, <code>QItem</code> 클래스에서 <code>isExpensive</code> 메서드가 자동으로 생성됩니다. 이 메서드는 <code>QItem</code> 객체에서 조건을 직접 작성하는 것처럼 사용할 수 있습니다.</p>
<h3 id="qitem-클래스에-생성된-메서드">QItem 클래스에 생성된 메서드:</h3>
<p><code>@QueryDelegate</code>를 사용하면 <code>QItem</code> 클래스에 아래와 같은 메서드가 생성됩니다.</p>
<pre><code class="language-java">public class QItem extends EntityPathBase&lt;Item&gt; {
    // 기존 필드들

    public final NumberPath&lt;Integer&gt; price = createNumber(&quot;price&quot;, Integer.class);

    public QItem(String variable) {
        super(Item.class, forVariable(variable));
    }

    // QueryDelegate로 생성된 메서드
    public BooleanExpression isExpensive(Integer price) {
        return this.price.gt(price); // 가격이 특정 값 이상인 조건
    }
}</code></pre>
<p>예전 버전의 QueryDSL에서는 com.mysema.query.types.expr.BooleanExpression을 명시적으로 사용해야 했습니다. 하지만 최신 버전에서는 기본적으로 BooleanExpression을 임포트할 수 있기 때문에, 패키지 이름을 생략할 수 있습니다.
(com.querydsl.core.types.dsl.BooleanExpression을 임포트 필요)</p>
<h3 id="사용-방법">사용 방법:</h3>
<p>이렇게 생성된 <code>isExpensive</code> 메서드는 쿼리에서 다음과 같이 사용될 수 있습니다.</p>
<pre><code class="language-java">QItem item = QItem.item;
JPAQuery&lt;?&gt; query = new JPAQuery&lt;Void&gt;(em);

List&lt;Item&gt; expensiveItems = query.from(item)
                                 .where(item.isExpensive(100))  // 100 이상인 가격 조건
                                 .fetch();</code></pre>
<h3 id="요약-3">요약:</h3>
<ul>
<li><code>@QueryDelegate</code>는 QueryDSL에서 엔티티에 대해 커스텀 쿼리 메서드를 정의할 수 있게 해줍니다.</li>
<li><code>QItem</code>과 같은 Q타입 클래스에 자동으로 생성된 메서드를 사용하여 쿼리 로직을 더 깔끔하고 재사용 가능하게 만듭니다.</li>
<li><code>isExpensive</code>와 같은 메서드는 <code>QItem</code> 클래스에서 조건을 직접 다룰 수 있게 하며, 쿼리 작성이 더 직관적이고 편리해집니다.</li>
</ul>
<p>이 기능은 QueryDSL을 좀 더 효율적으로 사용할 수 있는 방법 중 하나입니다.</p>
<p>참조 : [자바 ORM 표준 JPA 프로그래밍]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[10장. 객체지향 쿼리 언어 (JPQL)]]></title>
            <link>https://velog.io/@now_here/10%EC%9E%A5.-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%BF%BC%EB%A6%AC-%EC%96%B8%EC%96%B4</link>
            <guid>https://velog.io/@now_here/10%EC%9E%A5.-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%BF%BC%EB%A6%AC-%EC%96%B8%EC%96%B4</guid>
            <pubDate>Sat, 28 Dec 2024 10:40:39 GMT</pubDate>
            <description><![CDATA[<h1 id="객체지향-쿼리-소개">객체지향 쿼리 소개</h1>
<p>JPA에서는 식별자를 통해 em.find() 메서드로 엔티티를 조회하고, 객체 그래프 탐색을 통해서 연관된 객체를 조회한다. 그러나 특정 조건을 걸어서 조회할 때는 모든 엔티티를 조회하고 검색하는 것은 현실성이 없다. 실제 데이터는 객체가 아닌 데이터베이스에 있기 때문에 이런 문제가 발생한다. ORM을 사용하면 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 개발하므로 검색도 테이블이 아닌 엔티티 객체를 대상으로 하는 방법이 필요하다. 그래서 객체를 중심으로 데이터를 다루는 언어가 필요한데, 이를 <strong>객체지향 쿼리</strong>라고 한다.</p>
<p>객체지향 쿼리 중 하나인 JPQL을 알아보고 JPA에서 공식적으로 지원하는 JPQL을 편하게 사용하도록 도와주는 Crieteria 쿼리, 네이티브 SQL을 먼저 간단하게 알아보자. </p>
<hr>
<h2 id="jpql-java-persistence-query-language">JPQL (Java Persistence Query Language)</h2>
<ul>
<li>테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리
문법은 SQL과 비슷하고 ANSI 표준 SQL이 제공하는 기능을 유사하게 지원한다.</li>
<li>SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않음
JPQL을 사용하면 JPA가 적절한 SQL로 만들어 데이터베이스를 조회하고 엔티티 객체를 생성해서 반환한다.
JPQL은 SQL을 추상화하기 때문에 데이터베이스 방언(Dialect)만 변경하면 JPQL을 변경하지 않아도 데이터베이스를 변경할 수 있다.</li>
</ul>
<h3 id="jpql-사용-예시">JPQL 사용 예시</h3>
<pre><code class="language-java">//쿼리 생성
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).getResultList();</code></pre>
<p>회원 이름이 &#39;kim&#39;인 엔티티를 조회한다. 생성한 jpql에서 m.username은 테이블의 컬럼명이 아닌 엔티티 객체의 필드명이다. em.createQuery() 메서드에 실행할 JPQL과 반환할 엔티티의 클래스 타입(Member.class)를 넘겨주고 getResultList() 메서드를 실행하면 JPA는 JPQL을 SQL로 변환해서 데이터베이스를 조회하고 조회한 결과를 Member 엔티티로 생성해서 반환한다.</p>
<p>이 때, Member 클래스가 id, age, name을 필드로 가지고 Team 클래스와 연관관계를 가지고 있다고 하면 다음과 같은 SQL이 생성된다.</p>
<pre><code class="language-sql">SELECT m.id, m.age, m.name, m.team_id
FROM Member m
WHERE m.name = &#39;kim&#39;
</code></pre>
<h2 id="criteria-쿼리">Criteria 쿼리</h2>
<ul>
<li>Criteria는 JPA에서 제공하는 객체지향 쿼리를 생성하기 위한 API로, 동적 쿼리를 타입 세이프하게 작성할 수 있도록 지원한다.</li>
</ul>
<h3 id="타입세이프-typesafe">타입세이프 (TypeSafe)</h3>
<p>Criteria는 문자가 아닌 코드로 JPQL을 작성할 수 있다. 
JPQL은 위에서 봤던 것처럼 문자로 작성하기 때문에 오타가 나도 컴파일 오류가 일어나지 않는다. (예를 들어 &quot;select m Membee m&quot;) 다만 해당 쿼리가 실행되는 런타임 시점에 오류가 발생하게 되는데 Criteria는 JPQL을 문자가 아닌 코드로 작성하기 때문에 컴파일 단계에서 오류를 잡을 수 있다. </p>
<h3 id="criteria-예시">Criteria 예시</h3>
<pre><code class="language-java">//jpql
// &quot;select m from Member as m where m.username = &#39;kim&#39;&quot;

//Criteria 사용준비
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>
<h3 id="메타모델-api">메타모델 API</h3>
<p>자바가 제공하는 어노테이션 프로세서(Annotation Processor)기능을 사용하면 어노테이션을 분석해서 클래스를 생성할 수 있다. JPA는 이 기능을 사용해서 Member엔티티 클래스로부터 Member_라는 Criteria 전용 클래스를 생성하는데 이를 메티 모델이라고 한다. 메타 모델을 이용하면 &quot;username&quot;부분도 코드로 작성할 수 있다.
m.get(&quot;username&quot;) -&gt; m.get(Member_.username)</p>
<h2 id="querydsl-소개">QueryDSL 소개</h2>
<p>JPA에서 공식 지원하지 않지만 JPQL을 편하게 작성하도록 도와주는 빌더 역할을 하는 오픈소스 프로젝트이다.
코드 기반이면서 단순하고 사용하기 쉽다.</p>
<h3 id="querydsl예시">QueryDSL예시</h3>
<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>
<h2 id="네이티브-sql-소개">네이티브 SQL 소개</h2>
<p>JPA는 SQL을 직접 사용할 수 있게 지원하는데 이것을 <strong>네이티브 SQL</strong>이라고 한다.
다음과 같은 상황에서 네이티브 SQL을 사용한다.</p>
<ul>
<li>특정 데이터베이스에 의존하는 기능을 사용해야 할 때</li>
<li>SQL은 지원하지만 JPQL이 지원하지 않는 기능을 사용할 때</li>
</ul>
<p>네이티브 SQL은 특정 데이터베이스에 의존하는 SQL을 작성해야 하는 단점이 있어서 데이테버에시를 변경하면 네이티브 SQL도 변경해야 한다.</p>
<h3 id="네이티브-sql-예시">네이티브 SQL 예시</h3>
<pre><code class="language-java">String sql = &quot;SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = &#39;kim&#39;&quot;;
List&lt;Member&gt; resultList = 
    em.createNativeQuery(sql, Member.class).getResultList();</code></pre>
<h2 id="jdbc-직접-사용-sql-매퍼-프레임워크-사용">JDBC 직접 사용, SQL 매퍼 프레임워크 사용</h2>
<p>JPA는 JDBC 커넥션을 획득하는 API를 제공하지 않으므로 구현체가 제공하는 방법을 사용해야 한다.
(이 방법은 궁금하면 검색해 보자)</p>
<p>JDBC를 직접 사용하든 마이바티스 같은 SQL 매퍼와 사용하든 모두 JPA를 <strong>우회</strong>해서 데이터베이스에 접근한다. 이처럼 JPA를 우회하기 때문에 JPA가 전혀 인식을 못하게 되는데 이는 영속성 컨텍스트와 데이터베이스를 불일치 상태로 만들어 데이터 무결성을 훼손할 수 있다.
이런 이슈를 해결하기 위해 JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해서 데이터베이스와 영속성 컨텍스트를 동기화하면 된다.
스프링 프레임워크는 AOP를 적절히 활용해서 JPA를 우회해서 데이터베이스에 접근하는 메서드를 호출할 때마다 영속성 컨텍스트를 플러시하면 위의 문제를 해결할 수 있다.</p>
<hr>
<h1 id="102-jpql">10.2 JPQL</h1>
<h2 id="기본-문법과-쿼리-api">기본 문법과 쿼리 API</h2>
<p>JPQL도 SELECT, UPDATE, DELETE 문이 있다. INSERT는 EntityManager.persist() 메서드를 사용하므로 없다.</p>
<pre><code class="language-java">//JPQL 문법
select_문 :: = 
    select_절
    from_절
    [where_절]
    [groupby_절]
    [having_절]
    [orderby_절]

update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]</code></pre>
<p>JPQL에서 update, delete는 벌크연산인데 뒤에서 자세히 다룬다.</p>
<h2 id="select-문">SELECT 문</h2>
<pre><code class="language-java">SELECT m FROM Member AS m where m.username = &#39;Hello&#39;</code></pre>
<ul>
<li><strong>대소문자 구분</strong>
엔티티와 속성은 대소문자로 구분한다. SELECT, FROM 같은 JPQL 키워드는 대소문자를 구분하지 않는다.</li>
<li><strong>엔티티 이름</strong>
JPQL에서 사용하는 Member는 클래스 명이 아니라 엔티티 명이다. 엔티티 명은 @Entity(name=&#39;xxx&#39;)에서 name 속성으로 지정한다. 이 때 엔티티명을 지정하지 않으면 클래스명을 기본값으로 한다.</li>
<li><strong>별칭은 필수</strong>
JPQL은 위에서 사용한 것 처럼 Member As m 과 같이 별칭을 반드시 사용해야 하며, 속성도 별칭을 이용해서 사용해야 한다. AS 는 생략해도 된다.</li>
</ul>
<h3 id="typequery-query">TypeQuery, Query</h3>
<p>JPQL을 실행하려면 쿼리 객체를 만들어야 하는데 다음과 같이 두 개의 객체를 사용한다.</p>
<ul>
<li>TypeQuery : 반환하는 타입이 명확한 경우</li>
<li>Query : 반환하는 타입이 명확하지 않은 경우</li>
</ul>
<pre><code class="language-java">TypeQuery&lt;Member&gt; query = 
    em.creqteQuery(&quot;SELECT m FROM Member m&quot;, Member.class);

Query query = 
    em.createQuery(&quot;SELECT m.username, m.age from Member m&quot;);</code></pre>
<p>예제코드를 보면 반환타입을 Member.class로 지정한 경우 TypeQuery를 사용하였다. 반면에 String 타입인 username과 Integer타입인 age를 반환하는 경우 Query를 사용한디. 이처럼 SELECT 절에서 여러 엔티티나 컬럼을 선택할 때는 반환할 타입이 명확하지 않으므로 Query 객체를 사용해야 한다.
Query 객체는 조회 대상이 둘 이상이면 Object[] 를 반환하고 조회 대상이 하나면 Object를 반환한다.</p>
<h3 id="결과-조회">결과 조회</h3>
<p>다음 메서드를 호출하면 실제 쿼리를 실행해서 데이터베이스를 조회한다.</p>
<ul>
<li>query.getResultList() : 결과를 List 컬렉션으로 반환한다. 결과가 없으면 빈 컬렉션을 반환한다.</li>
<li>query.getSingleResult() : 결과가 정확히 하나일 때 사용한다.
결과가 없거나 둘 이상이면 예외가 발생한다.</li>
</ul>
<h2 id="파라미터-바인딩">파라미터 바인딩</h2>
<p>JDBC는 위치 기반 파라미터 바인딩만 지원하지만 JPQL은 이름 기반 파라미터 바인딩도 지원한다.</p>
<h3 id="이름-기준-파라미터-named-parameters">이름 기준 파라미터 (Named parameters)</h3>
<p>파라미터를 이름으로 구분하는 바인딩으로 앞에 :를 사용한다.</p>
<h3 id="위치-기준-파라미터-positional-parameters">위치 기준 파라미터 (Positional parameters)</h3>
<p> ? 다음에 위치 값을 주면 되며, 위치 값은 1부터 시작한다.</p>
<pre><code class="language-java"> String usernameParam = &quot;User1&quot;;

 // 이름 기준 파라미터
 TypedQuery&lt;Member&gt; query =
     em.createQuery(&quot;SELECT m FROM Member m where m.username = :username&quot;, Member.class)

List&lt;Member&gt; resultList = query.setParameter(&quot;username&quot;, usernameParam)
                                .getResultList();

// 위치 기준 파라미터
List&lt;Member&gt; members = em.createQuery(&quot;SELECT m FROM Member m where m.username = ?1&quot;, Member.class)
                            .setParaemter(1, usernaemParam)
                            .getResultList();
</code></pre>
<h2 id="프로젝션-projection">프로젝션 (Projection)</h2>
<p> SELECT절에 조회할 대상을 지정하는 것을 프로젝션이라 하며, [SELECT {프로젝션 대상} FROM]으로 대상을 선택한다.</p>
<h3 id="엔티티-프로젝션">엔티티 프로젝션</h3>
<pre><code class="language-java"> SELECT m FROM Member m            //회원
 SELECT m.team FROM Member m     //팀</code></pre>
<p> 회원과 팀을 조회하는데 둘 다 엔티티를 프로젝션 대상으로 사용했다. 이렇게 조회한 <strong>엔티티는 영속성 컨텍스트에서 관리된다.</strong></p>
<h3 id="임베디드-타입-프로젝션">임베디드 타입 프로젝션</h3>
<p> 값 타입의 일종인 임베디드 타입으로도 프로젝션을 사용할 수 있는데, 임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다.
 예를 들어 Order 안에 Address 라는 임베디드 타입을 바로 프로젝션으로 사용할 수 없다는 것인데 다음과 같이 사용해야 올바르다.</p>
<pre><code class="language-java"> String query = &quot;SELECT o.address FROM Order o&quot;;
 List&lt;Address&gt; addresses = em.creqteQuery(query, Address.class)
                                 .getResultList();

//실행되는 SQL
select
    order.city,
    order.street,
    order.zipcode
from Orders order</code></pre>
<p>임베디드 타입은 값 타임이므로 이렇게 직접 조회한 <strong>임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.</strong></p>
<h3 id="스칼라-타입-프로젝션">스칼라 타입 프로젝션</h3>
<p>숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라 한다.</p>
<pre><code class="language-java">//예시 중복제거 DISTINCT
SELECT DISTINCT username FROM Member m

// 통계 쿼리
Double orderAmountAvg = em.createQuery(&quot;SELECT AVC(o.orderAmount) FROM Order o&quot;, Double.classs)
                            .getSingleResult();</code></pre>
<h3 id="여러-값-조회">여러 값 조회</h3>
<p>프로젝션에 여러 값을 선택하면 TypeQuery를 사용할 수 없고 Query 객체를 사용해야 한다. 스칼라 타입뿐만 아니라 엔티티 타입도 여러 값을 함께 조회할 수 있다.
이 때, <strong>한 엔티티는 영속성 컨텍스트에서 관리된다.</strong></p>
<h3 id="new-명령어">NEW 명령어</h3>
<p>여러 필드를 프로젝션으로 조회할 경우 Query를 사용했다. 하지만 UserDTO처럼 의미있는 객체로 변환해서 사용하면 간편하게 사용할 수 있는데 예시를 보자.</p>
<pre><code class="language-java">public class UserDTO {

    private String username;
    private int age;

    //생성자
    public UserDTO(String username, int age) {
        this.username = uesrname;
        this.age = age;
    }
}

// NEW 명령어
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 명령어를 사용하면 반환받을 클래스를 지정할 수 있는데 이 클래스의 생성자에 JQPL 조회 결과를 넘겨줄 수 있다. 또한 NEW 명령어를 사용한 클래스로 TypedQuery 를 사용할 수 있어 지루한 객체 변환 작업을 줄일 수 있다.</p>
<p>NEW 명령어를 사용하면 다음과 같은 주의사항이 있다.</p>
<ul>
<li>패키지 명을 포함한 전체 클래스 명을 입력해야 한다.</li>
<li>순서와 타입이 일치하는 생성자가 필요하다.</li>
</ul>
<h2 id="페이징-api">페이징 API</h2>
<p>데이터베이스마다 페이징을 처리하는 SQL 문법이 다르다. JPA에서는 페이징을 다음 두 API로 추상화하였다.</p>
<ul>
<li>setFirstResult(int startPosition): 조회 시작 위치(0부터 시작)</li>
<li>setMaxResults(int maxResult) : 조회할 데이터 수</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);    // 11번째부터 데이터를 조회한다.
query.setMaxResults(20);    // 11번째부터 20개를 조회한다. (즉, 11~30번 데이터)
query.getResultList();</code></pre>
<p>데이터베이스마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은 데이터베이스 방언 (Dialect) 덕분이다. 
데이터베이스별 페이징 쿼리가 궁금하면 검색해보자.</p>
<p>페이징 SQL을 더 최적화하고 싶다면 JPA가 제공하는 페이징 API가 아닌 네이티브 SQL을 직접 사용해야 한다.</p>
<h2 id="집합과-정렬">집합과 정렬</h2>
<pre><code class="language-SQL">select
    COUNT(m),    //회원 수, 결과수를 구한다. 반환타입 Long
    SUM(m.age),    //나이 합, 숫자타입만 사용가능. 반환타입 정수 -&gt; Long, 소수 -&gt; Double,// BigInteger, BigDecimal 합은 해당 타입으로 반환
    AVG(m.age),    //평균나이, 평균값을 구한다. 숫자타입만 사용가능하며 반환타입 Double
    MAX(m.age),    //최대 나이, 최대값을 구한다. 문자, 숫자, 날짜 등에 사용
    MIN(m.age), //최소 나이, 최소값을 구한다. 문자, 숫자, 날짜 등에 사용</code></pre>
<h3 id="집함-함수-참고사항">집함 함수 참고사항</h3>
<ul>
<li>NULL 값은 무시하므로 통계에 잡히지 않는다.(DISTINCT가 정의되어 있어도 무시됨)</li>
<li>만약 값이 없는데 SUM, AVG, MAX, MIN 함수를 사용하면 NULL, COUNT는 0을 반환한다.</li>
<li>DISTINCT를 집함 함수 안에 사용해서 중복된 값을 제거하고 나서 집합을 구할 수 있다. 
( select COUNT( DISTINCT m.age) from Member m</li>
<li>DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원하지 않는다.</li>
</ul>
<h3 id="group-by-having">GROUP BY, HAVING</h3>
<p>GROUP BY : 특정 그룹끼리 묶어서 조회할 때 사용.
HAVING : GROUP BY 로 묶은 그룹에서 특정 조건으로 조회할 때 사용.</p>
<pre><code class="language-java">select t.name, COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age)
from Member m LEFT JOIN m.team t
GROUP BY t.name
HAVING AVG(m.age) &gt;= 10</code></pre>
<h3 id="정렬-order-by">정렬 (ORDER BY)</h3>
<p>ORDER BY 는 결과를 정렬할 때 사용한다</p>
<pre><code class="language-java">select m from Member m order by m.age DESC, m.username ASC</code></pre>
<h2 id="jpql-조인">JPQL 조인</h2>
<h3 id="내부조인">내부조인</h3>
<p>JPQL도 SQL조인과 비슷하게 조인을 지원한다. INNER JOIN 사용 시 INNER는 생략해도 된다.
회원과 팀을 내부조인하여 &quot;팀A&quot;라는 팀에 소속된 회원을 조회하는 JPQL을 살펴보자.</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>
<pre><code class="language-sql">//실행 SQL
SELECT
    M.ID AS ID,
    M.AGE AS AGE,
    M.TEAM_ID AS TEAM_ID,
    M.NAME AS NAME
FROM
    MEMBER M INNER JOIN TEAM T ON M.TEAM_ID=T.ID
WHERE
    T.NAME=?    </code></pre>
<p>JPQL 조인의 가장 큰 특징은 조인할 때 m.team 과 같이 <strong>연관 필드</strong>(다른 엔티티와 연관관계를 가지기 위해 사용하는 필드)를 사용한다는 것이다.</p>
<h3 id="외부조인">외부조인</h3>
<p>JPQL의 외부조인은 기능상 SQL의 외부조인과 같으며 LEFT OUTER JOIN을 사용하는데 OUTER는 생략해도 된다.</p>
<pre><code class="language-java">//외부조인 JPQL
SELECT m
FROM Member m LEFT &amp;#91;OUTER&amp;#93; JOIN m.team t
WHERE t.name = :teamName</code></pre>
<pre><code class="language-java">String jpql = &quot;SELECT m FROM Member m LEFT JOIN m.team t WHERE t.name = :teamName&quot;;
TypedQuery&lt;Member&gt; query = em.createQuery(jpql, Member.class);
query.setParameter(&quot;teamName&quot;, &quot;TeamA&quot;);
List&lt;Member&gt; result = query.getResultList();</code></pre>
<pre><code class="language-sql">//실행 SQL
SELECT
    M.ID AS ID,
    M.AGE AS AGE,
    M.TEAM_ID AS TEAM_ID,
    M.NAME AS NAME
FROM
    MEMBER M LEFT OUTER JOIN TEAM T ON M.TEAM_ID = T.ID
WHERE
    T.NAME ?</code></pre>
<h3 id="컬렉션-조인">컬렉션 조인</h3>
<p>일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라 한다.</p>
<ul>
<li>[회원 -&gt; 팀] 으로의 조인은 다대일 조인이면서 <strong>단일 값 연관 필드(m.team)</strong>을 사용한다.</li>
<li>[팀 -&gt; 회원] 으로의 조인은 일대다 조인이면서 <strong>컬렉션 값 연관 필드(m.members)</strong>를 사용한다.</li>
</ul>
<pre><code class="language-sql">SELECT t, m FROM Team t LEFT JOIN t.members m</code></pre>
<p>위에 t LEFT JOIN t.members 부분으로 팀과 팀이 보유한 회원목록을 <strong>컬렉션 값 연관 필드</strong>로 외부 조인했다.</p>
<h3 id="세타-조인">세타 조인</h3>
<p>WHERE절을 이용해 세타조인을 사용할 수 있으며, 세타 조인은 <strong>내부 조인</strong>만 지원한다.
세타 조인을 사용하면 전혀 관계없는 엔티티도 조인할 수 있다.</p>
<pre><code class="language-java">//회원 이름이 팀 이름과 똑같은 사람 수를 구하는 예시
//JPQL
select count(m) from Member m, Team t
where m.username = t.name

//SQL
SELECT COUNT(M.ID)
FROM
    MEMBER M CROSS JOIN TEAM T
WHERE
    M.USERNAME=T.NAME</code></pre>
<h3 id="세타-조인-부가-설명">세타 조인 부가 설명</h3>
<p>세타 조인(Theta Join)은 관계형 데이터베이스에서 <strong>조인 조건을 지정해 두 개 이상의 테이블을 결합하는 방식</strong>입니다. 이 조인의 가장 큰 특징은 단순히 <strong>등가 조건(=)</strong>에만 의존하지 않고, <strong>비교 연산자(=, &lt;, &gt;, &lt;=, &gt;=, &lt;&gt;, etc.)</strong>를 사용하여 다양한 조건으로 데이터를 결합할 수 있다는 점입니다. </p>
<p>SQL 문장에서 세타 조인은 일반적으로 <code>WHERE</code> 절을 사용해 구현됩니다.</p>
<h3 id="세타-조인의-특징">세타 조인의 특징</h3>
<ul>
<li>두 테이블의 카르테시안 곱(모든 조합)을 생성한 후, 주어진 조건에 따라 결과를 필터링합니다.</li>
<li>조인 조건으로 사용할 수 있는 연산자는 다음과 같습니다:<ul>
<li><code>=</code>, <code>&lt;</code>, <code>&gt;</code>, <code>&lt;=</code>, <code>&gt;=</code>, <code>&lt;&gt;</code> 등</li>
</ul>
</li>
<li>세타 조인의 결과는 조인 조건을 만족하는 모든 레코드 쌍으로 구성됩니다.</li>
</ul>
<h3 id="예제">예제</h3>
<h4 id="테이블-구조">테이블 구조</h4>
<ol>
<li><p><strong>Employees</strong> 테이블:</p>
<pre><code>EmpID | Name   | DeptID
------|--------|-------
1     | Alice  | 10
2     | Bob    | 20
3     | Charlie| 30</code></pre></li>
<li><p><strong>Departments</strong> 테이블:</p>
<pre><code>DeptID | DeptName
-------|---------
10     | HR
20     | IT
30     | Finance</code></pre></li>
</ol>
<h4 id="세타-조인-비교-연산자-사용">세타 조인: 비교 연산자 사용</h4>
<pre><code class="language-sql">SELECT e.Name, d.DeptName
FROM Employees e, Departments d
WHERE e.DeptID &gt;= d.DeptID;</code></pre>
<h4 id="결과">결과</h4>
<pre><code>Name     | DeptName
---------|----------
Alice    | HR
Alice    | IT
Alice    | Finance
Bob      | IT
Bob      | Finance
Charlie  | Finance</code></pre><h3 id="세타-조인의-유형">세타 조인의 유형</h3>
<p>세타 조인은 특정 연산 조건에 따라 여러 방식으로 확장될 수 있습니다:</p>
<ol>
<li><p><strong>등가 조인(Equal Join)</strong>: 조건이 <code>=</code>인 경우(내부 조인의 기본 형태).</p>
<pre><code class="language-sql">SELECT e.Name, d.DeptName
FROM Employees e
JOIN Departments d
ON e.DeptID = d.DeptID;</code></pre>
</li>
<li><p><strong>비등가 조인(Non-Equi Join)</strong>: 조건이 <code>= 이외</code>의 연산자를 사용할 때.</p>
<pre><code class="language-sql">SELECT e.Name, d.DeptName
FROM Employees e
JOIN Departments d
ON e.DeptID &gt; d.DeptID;</code></pre>
</li>
</ol>
<h3 id="주의사항">주의사항</h3>
<ul>
<li>세타 조인은 <strong>비효율적</strong>일 수 있습니다. 특히, 조인 조건이 효율적으로 작성되지 않으면 카르테시안 곱이 불필요하게 커질 수 있습니다.</li>
<li>최신 SQL에서는 <strong>명시적 조인(Explicit Join)</strong> 문법(<code>JOIN ON</code>)이 더 권장됩니다. 이는 가독성과 유지보수성을 향상시키며, 세타 조인도 이 문법으로 구현 가능합니다.</li>
</ul>
<h3 id="활용-사례">활용 사례</h3>
<ul>
<li>테이블 간의 특정 조건(예: 범위 비교)을 통해 데이터를 결합해야 할 때 유용합니다.</li>
<li>SQL 쿼리 작성 시, 조인 조건이 단순 등가 조건 이상으로 복잡한 경우 세타 조인을 사용할 수 있습니다.</li>
</ul>
<hr>
<h3 id="join-on-절jpa21">JOIN ON 절(JPA2.1)</h3>
<p>JPA 2.1부터 조인할 때 ON 절을 지원한다. ON 절을 사용하면 <strong>조인 대상을 필터링</strong>하고 조인할 수 있다. 내부 조인의 ON절은 WHERE 절을 사용할 때와 결과가 같으므로 보통 ON절은 <strong>외부 조인</strong>에서만 사용한다.</p>
<pre><code class="language-java">//모든 회원을 조회하면서 회원과 연관된 팀을 조회, 이 때 팀이름이 A인 팀만 조회
//JPQL
select m,t from Member m
left join m.team t on t.name = &#39;A&#39;

//SQL
SELECT m.*, t.* FROM Member m
LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name=&#39;A&#39;
</code></pre>
<h2 id="페치-조인-fetch-join">페치 조인 fetch join</h2>
<p>페치조인은 SQL에서 이야기하는 조인의 종류가 아닌 JPQL에서 성능 최적화를 위해 제공하는 기능이다. 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능인데 join fetch 명령어로 사용한다.
또한 페치조인은 연관 필드에 별칭을 허용하지 않는다.(하이버네이트는 허용한다고 한다...)</p>
<ul>
<li>JPA 문법 (fetch join)
[ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로</li>
</ul>
<h3 id="엔티티-페치-조인">엔티티 페치 조인</h3>
<p>페치 조인을 사용하여 회원을 조회하는데 연관된 팀도 같이 조회해보자.</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>
<pre><code class="language-sql">//실행 SQL
SELECT
    M.*, T.*
FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID=T.ID</code></pre>
<p>엔티티 페치 조인 JPQL에서 select m으로 회원 엔티티만 선택했는데 실행 SQL을 보면 회원과 연관된 팀도 같이 조회되는 것을 확인할 수 있다.</p>
<p>회원과 팀을 지연 로딩으로 설정했다고 가정해보자. 회원을 조회할 때 페치 조인을 사용해서 팀도 함께 조회했으므로 연관된 팀 엔티티는 프록시가 아닌 실제 엔티티다. 따라서 연관된 팀을 사용해도 지연 로딩이 일어나지 않는다. 그리고 프록시가 아닌 실제 엔티티이므로 회원 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할 수 있다.</p>
<h3 id="컬렉션-페치-조인">컬렉션 페치 조인</h3>
<p>일대다 관계 컬렉션을 페치 조인해보자.</p>
<pre><code class="language-java">String jpql = &quot;select t from Team t join fetch t.members where t.name = &#39;팀A&#39;&quot;
List&lt;Team&gt; teams = em.createQuery(jpql, Team.class).getResultList();</code></pre>
<pre><code class="language-sql">//실행 SQL
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=TEAM_ID
WHERE T.NAME = &#39;팀A&#39;</code></pre>
<p><img src="https://velog.velcdn.com/images/now_here/post/60edc13d-4d02-4ac4-abe9-514728056339/image.png" alt="">
Team 테이블에서 &#39;팀A&#39;는 하나지만 MEMBER 테이블과 조인하면서 결과가 증가해 &#39;팀A&#39;가 2건 조회된다.
따라서 컬렉션 페치 조인 결과 객체에서 teams 결과 예제를 보면 주소가 같은 &#39;팀A&#39;를 2건 가지게 된다.</p>
<h3 id="페치-조인과-distinct">페치 조인과 DISTINCT</h3>
<p>SQL의 DISTINCT는 중복된 결과를 제거하는 명령어이다. JPQL에서 DISTINCT 명령어는 SQL에 DISTINCET를 추가하는 것은 물론이고 애플리케이션에서 한 번 더 중복을 제거한다.</p>
<pre><code class="language-java">select distinct t
from Team t join fetch t.members
where t.name = &#39;팀A&#39;</code></pre>
<p>SQL에 DISTINCT를 추가되어도 각 로우가 다르므로 SQL의 DISTINCT는 효과가 없다. 애플리케이션에서 distinct 명령어를 보고 중복된 데이터를 걸러내어 select distinct t를 통해서 팀 엔티티의 중복을 제거한다.</p>
<h3 id="페치-조인과-일반-조인의-차이">페치 조인과 일반 조인의 차이</h3>
<pre><code class="language-java">//일반 조인
select t
from Team t join t.members m
where t.name = &#39;팀A&#39;</code></pre>
<pre><code class="language-sql">//실행된 SQL
SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = &#39;팀A&#39;</code></pre>
<p>JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다. 따라서 팀 엔티티만 조회하고 연관된 컬렉션은 조회하지 않는다.</p>
<p>만약 회원 컬렉션을 지연 로딩으로 설정하면 프록시나 아직 초기화하지 않은 컬렉션 래퍼를 반환한다. 즉시 로딩으로 설정하면 회원 컬렉션을 즉시 로딩하기 위해 쿼리를 한 번 더 실행한다.</p>
<pre><code class="language-java">//페치 조인
select t
from Team t join fetch t.members
where t.name = &#39;팀A&#39;</code></pre>
<pre><code class="language-sql">//실행된 SQL
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = &#39;팀A&#39;</code></pre>
<p>페치조인을 사용하면 위처럼 연관된 엔티티(Members)의 데이터도 가져온다. </p>
<h3 id="페치-조인의-특징">페치 조인의 특징</h3>
<ul>
<li>SQL 호출 횟수 줄여줌
페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어 SQL 호출 횟수를 즐여 성능을 최적화할 수 있다.</li>
<li>글로벌 로딩전략보다 우선한다.
엔티티에 직접 적용하는 로딩 전략은 애플리케이션 전체에 영향을 미치므로 글로벌 로딩 전략이라 한다.
글로벌 로딩전략을 지연로딩으로 설정해도 JPQL에서 페치 조인을 사용하면 페치 조인을 적용해서 함께 조회한다.
글로벌 로딩 전략은 될 수 있으면 지연 로딛을 사용하고 최적화가 필요할 때 페치 조인을 적용하는 것이 효과적이다.
@OneToMany(fetch = FetchType.LAZY)    //글로벌 로딩 전략</li>
<li>지연로딩 발생 x
페치 조인을 사용하면 연관된 엔티티를 쿼리 시점에 조회하므로 지연 로딩이 발생하지 않는다. 따라서 준영속 상태에서도 객체 그래프를 탐색할 수 있다.</li>
</ul>
<h3 id="페치-조인의-한계">페치 조인의 한계</h3>
<ul>
<li>페치 조인 대상에는 별칭을 줄 수 없다.
페치 조인에는 별칭을 줄 수 없어 SELECT, WHERE절, 서브 쿼리에 페치 조인 대상을 사용할 수 없다.
하이버네이트 말고 다른 구현체에서 별칭을 구현하는 것도 있는데, 별칭을 잘못 사용하면 연관된 데이터 수가 달라져서 데이터 무결성이 깨질 수 있다.</li>
<li>둘 이상의 컬렉션을 페치할 수 없다.
구현체에 따라 되기도 하는데 [컬렉션 * 컬렉션]의 카테시안 곱이 만들어지므로 주의해야 한다.</li>
<li>컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
컬렉션(일대다)이 아닌 단일 값 연관 필드(일대일, 다대일)들은 페치 조인을 사용해도 페이징 API를 사용할 수 있다.
하이버네이트에서 컬렉션을 페치 조인하고 페이징 API를 사용하면 경고 로그를 남기면서 메모리에서 페이징 처리를 한다. 데이터가 적으면 상관없지만 데이터가 많으면 성능 이슈와 메모리 초과 예외가 발생할 수 있어 위험하다.</li>
</ul>
<p>페치 조인은 SQL 한 번으로 연관된 여러 엔티티를 조회할 수 있어서 성능 최적화에 상당히 유용하다. 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이지만 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면 다른 방법이 더 효과적이다. 이와 같은 경우 억지로 페치 조인을 사용하기보다 여러 테이블에서 필요한 필드들만 조회해서 DTO로 변환하는 것이 더 효과적이다.</p>
<h2 id="경로-표현식">경로 표현식</h2>
<ul>
<li>.(점)을 찍어 객체 그래프를 탐색하는 것</li>
</ul>
<h3 id="경로-표현식-용어-정리">경로 표현식 용어 정리</h3>
<p><strong>상태필드 state field</strong> :단순히 값을  저장하기 위한 필드(필드 or 프로퍼티)
<strong>연관필드 association field</strong> : 연관관계를 위한 필드, 임베디드 타입 포함(필드 or 프로퍼티)
    - 단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티
    - 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션</p>
<p>상태 필드는 단순히 값을 저장하는 필드이고 연관 필드는 객체 사이의 연관관계를 맺기 위해 사용하는 필드이다.</p>
<h4 id="예시">예시</h4>
<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>
<h3 id="경로-표현식과-특징">경로 표현식과 특징</h3>
<ul>
<li>상태 필드 경로 : 경로 탐색의 끝으로 더는 탐색할 수 없다.</li>
<li>단일 값 연관 경로 : <strong>묵시적으로 내부 조인</strong>이 일어난다. 단일 값 연관 경로는 계속 탐색할 수 있다.</li>
<li>컬렉션 값 연관 경로 : <strong>묵시적으로 내부 조인</strong>이 일어난다. 더는 탐색할 수 없다. 단, FROM 절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색가능하다.</li>
</ul>
<h3 id="단일-값-연관-경로-탐색">단일 값 연관 경로 탐색</h3>
<pre><code class="language-java">//jpql
select o.member from Order o</code></pre>
<pre><code class="language-sql">//실행 SQL
select m.*
from Orders o
    inner join Member m on o.member_id=m.id</code></pre>
<p>o.member를 통해 주문에서 회원으로 단일 값 연관 필드로 경로 탐색을 했다. 단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부 조인이 일어난다. 이것을 묵시적 조인이라 한다.</p>
<ul>
<li>명시적 조인 : JOIN을 직접 적어주는 것 (SELECT m FROM Member m JOIN m.team t)</li>
<li>묵시적 조인 : 경로 표현식에 의해 묵시적으로 조인이 일어나는 것. <strong>내부 조인</strong>만 가능.
(SELECT m.team FROM Member m)</li>
</ul>
<h4 id="복잡한-예시">복잡한 예시</h4>
<pre><code class="language-java">//JPQL
select o.member.team
from Order o
where o.product.name = &#39;A&#39; and o.address.city = &#39;JEJU&#39;;</code></pre>
<pre><code class="language-sql">//실행 SQL
select t.*
from Order o
inner join Member m on o.member_id=m.id
inner join Team t on m.team_id=t.id
inner join Product p on o.product_id=p.id
where p.name=&#39;A&#39; and o.city=&#39;JEJU&#39;</code></pre>
<p>주문상품이 &#39;A&#39;이고 배송지가 &#39;JEJU&#39;인 회원이 소속된 팀을 조회하는 내용이다. SQL을 보면 총 3번의 조인이 발생했다. o.address처럼 임베디드 타입에 접근하는 것도 단일 값 연관 경로 탐색이지만 주문 테이블에 이미 포함되어 있으므로 조인이 발생하지 않는다.</p>
<h3 id="컬렉션-값-연관-경로-탐색">컬렉션 값 연관 경로 탐색</h3>
<p>컬렉션에서 경로 탐색을 허용하지 않는다. 만약 경로 탐색을 하고 싶다면 조인을 사용해서 새로운 별칭을 획득해야 한다.</p>
<pre><code class="language-sql">select t.members.username from Team t //실패
select m.username from Team t join t.members m    //성공</code></pre>
<h3 id="경로-탐색을-사용한-묵시적-조인-시-주의사항">경로 탐색을 사용한 묵시적 조인 시 주의사항</h3>
<ul>
<li>항상 내부 조인이다.</li>
<li>컬렉션은 경로 탐색의 끝이다. 컬렉션에서 경로 탐색을 진행하려면 명시적으로 조인해서 별칭을 획득해야 한다.</li>
<li>경로 탐색은 주로 SELECT, WHERE절(다른 곳에서도 사용됨)에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM 절에 영향을 준다.</li>
</ul>
<p>묵시적 조인이 일어나면 파악하기 어렵다는 단점이 있으므로 명시적 조인을 사용하도록 하자.</p>
<h2 id="서브-쿼리">서브 쿼리</h2>
<p>JPQL도 SQL처럼 서브 쿼리를 지원한다. 그러나 제약이 존재하는데 서브쿼리를 WHERE, HAVING절에서만 사용가능하고 SELECT, FROM절에서 사용하지 못 한다.
(HQL은 SELECT 절의 서브 쿼리 허용. 일부 JPA 구현체는 FROM 절의 서브 쿼리도 지원한다고 한다.)</p>
<h4 id="예시-1">예시</h4>
<p>한 건이라도 주문한 고객 조회</p>
<pre><code class="language-sql">select m from Member m
where (select count(o) from Order o where m = o.member) &gt; 0

//다른 방법
select m from Member m
where m.order.size &gt; 0</code></pre>
<h3 id="서브-쿼리-함수">서브 쿼리 함수</h3>
<h4 id="exists">EXISTS</h4>
<p>[NOT] EXISTS (subquery)</p>
<ul>
<li>서브쿼리에 결과가 존재하면 참. (NOT은 반대)</li>
</ul>
<h4 id="all--any--some">{ALL | ANY | SOME}</h4>
<p>{ALL | ANY | SOME} (subquery)</p>
<ul>
<li>비교 연산자와 같이 사용{= | &gt; | &gt;= | &lt; | &lt;= | &lt;&gt;}
ALL : 조건 모두 만족하면 참
ANY, SOME : 조건을 하나라도 만족하면 참</li>
</ul>
<h4 id="in">IN</h4>
<p>[NOT] IN (subquery)</p>
<ul>
<li>서브쿼리의 결과 중 하나라도 가은 것이 있으면 참
select t from Team t
where t IN (select t2 From Team t2 JOIN t2.members m2 where m2.age &gt;=20)</li>
</ul>
<h2 id="조건식">조건식</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/21b3949e-3c3e-4559-aa76-1499295df89a/image.png" alt="">
<img src="https://velog.velcdn.com/images/now_here/post/e0b56ffd-2ba5-4038-bef0-97c7780a38d0/image.png" alt=""></p>
<h3 id="연산자-우선-순위">연산자 우선 순위</h3>
<ol>
<li>경로 탐색 연산 (.)</li>
<li>수학 연산 : +, -(단항 연산자), *,/, +,-</li>
<li>비교 연산 : =, &gt;, &gt;=, &lt;=, &lt;&gt; (다름), [NOT] BETWEEN, [NOT] LIKE, [NOT]IN, IS [NOT] NULL, IS [NOT] EMPTY, [NOT] MEMBER [OF], [NOT] EXISTS</li>
<li>논리 연산 : NOT, AND, OR</li>
</ol>
<h3 id="between">Between</h3>
<p>문법 : X [NOT] BETWEEN A AND B
설명 : X는 A ~ B 사이의 값이면 참 (A, B 값 포함)
select m from Memer m
where m.age between 10 and 20
-&gt; 나이가 10~20인 회원 조회</p>
<h3 id="null-비교식">NULL 비교식</h3>
<p>문법 : {단일값 경로 | 입력 파라미터 } IS [NOT] NULL
설명 : NULL인지 비교. NULL 은 = 으로 비교할 수 없다.</p>
<p>where m.username is null
-&gt; where null = null //거짓</p>
<h3 id="컬렉션-식">컬렉션 식</h3>
<p>컬렉션에만 사용하는 특별한 기능. 컬렉션은 컬렉션 식 이외에 다른 식은 사용 불가능</p>
<h3 id="빈-컬렉션-비교-식">빈 컬렉션 비교 식</h3>
<p>문법 : {컬렉션 값 연관 경로} IS [NOT] EMPTY
설명 : 컬렉션에 값이 비었으면 참</p>
<pre><code class="language-sql">//JPQL : 주문이 하나라도 있는 회원 조회
select m from Member m
where m.orders is not empty

//실행된 SQL
select m.* from Member m
where
    exists (
        select o.id
        from Orders o
        where m.id=o.member_id
    )</code></pre>
<h3 id="컬렉션의-멤버-식">컬렉션의 멤버 식</h3>
<p>문법 : {엔티티나 값} [NOT] MEMBER [OF] {컬렉션 값 연관 경로}
설명 : 엔티티나 값이 컬렉션에 포함되어 있으면 참
select t from Team t
where :memberParam member of t.members</p>
<h3 id="스칼라-식">스칼라 식</h3>
<p>스칼라는 숫자, 문자, 날짜, case, 엔티티 타입(엔티티의 타입 정보)같은 가장 기본적인 타입들을 말한다.</p>
<h3 id="문자함수">문자함수</h3>
<p><img src="https://velog.velcdn.com/images/now_here/post/1870d41a-1ab5-4612-9c0c-186489205288/image.png" alt=""></p>
<h3 id="수학함수">수학함수</h3>
<p><img src="https://velog.velcdn.com/images/now_here/post/36c35cb7-5b48-4a14-8390-35288b1a62bf/image.png" alt=""></p>
<h3 id="날짜함수">날짜함수</h3>
<p>데이터베이스의 현재 시간을 조회한다.</p>
<ul>
<li>CURRENT_DATE : 현재 날짜</li>
<li>CURRENT_TIME : 현재 시간</li>
<li>CURRENT_TIMESTAMP : 현재 날짜 시간</li>
</ul>
<p>하이버네이트는 날짜 타입에서 년, 월, 일, 시간, 분, 초 값을 구하는 기능을 지원한다.
YEAR, MONTH, DAY, HOUR, MINUTE, SECOND</p>
<h4 id="예">예</h4>
<pre><code class="language-sql">select year(CURRENT_TIMESTAMP), month(CURRENT_TIMESTAMP), day(CURRENT_TIMESTAMP)
from Member</code></pre>
<h3 id="case-식">CASE 식</h3>
<p>특정 조건에 따라 분기할 때 CASE식을 사용한다.</p>
<h4 id="기본-case">기본 CASE</h4>
<pre><code class="language-sql">CASE
    {WHEN &lt;조건식&gt; THEN &lt;스칼라식&gt; } +
    ELSE &lt;스칼라식&gt;
END

//ex
select
    case when m.age &lt;= 10 then &#39;학생요금&#39;
        when m.age &gt;= 60 then &#39;경로요금&#39;
        else &#39;일반요금&#39;
    end
from Member m</code></pre>
<h4 id="심플-case">심플 CASE</h4>
<p>심플 CASE는 조건식을 사용할 수 없지만 문법이 간단하다. JAVA의 switch case 문과 비슷하다.</p>
<pre><code class="language-sql">CASE &lt;조건대상&gt;
    {WHEN &lt;스칼라식1&gt; THEN &lt;스칼라식2&gt; } +
    ELSE &lt;스칼라식&gt;
END

//ex
select
    case t.name
        when &#39;팀A&#39; then &#39;인센티브110%&#39;
        when &#39;팀B&#39; then &#39;인센티브120%&#39;
        else &#39;인센티브105%&#39;
    end
from Team t</code></pre>
<h4 id="coalesce">COALESCE</h4>
<pre><code class="language-sql">//문법 : COALESCE(&lt;스칼라식&gt; {,&lt;스칼라식&gt;}+)
//설명 : 스칼라식을 차례대로 조회해서 null이 아니면 반환한다.

//예 ) m.username이 null이면 &#39;이름 없는 회원&#39; 반환
select coalesce(m.username, &#39;이름 없는 회원&#39;) from Member m</code></pre>
<h4 id="nullif">NULLIF</h4>
<pre><code class="language-sql">//문법 : NULLIF(&lt;스칼라식&gt;, &lt;스칼라식&gt;)
//설명 : 두 값이 같으면 null을 반환하고 다르면 첫 번째 값을 반환한다. 집함 함수는 null을 포함하지 않으므로 보통 집합 함수와 함께 사용한다.

//예 ) 사용자 이름이 &#39;관리자&#39;면 null을 반환하고 나머지는 본인의 이름을 반환
select NULLIF(m.username, &#39;관리자&#39;) from Member m</code></pre>
<h2 id="다형성-쿼리">다형성 쿼리</h2>
<p>JPQL로 부모 엔티티를 조회하면 자식 엔티티도 함께 조회된다. Item의 자식 엔티티로 Book, Album, Movie가 있다고 하자.</p>
<pre><code class="language-java">@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = &quot;DTYPE&quot;)
public abstract class Item {...}

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

//Album, Movie 생략

// Item 조회
List resultList = em.createQuery(&quot;select i from Item i&quot;).getResultList();</code></pre>
<pre><code class="language-sql">//단일 테이블 전략(InheritanceType.SINGLE_TABLE) 실행 SQL
SELECT * FROM ITEM

//조인 전략(InheritanceType.JOINED) 실행 SQL
SELECT
    i.ITEM_ID, i.DTYPE, i.name, i.price, i.stockQuantity,
    b.author, b.isbn,
    a.artist, a.etc,
    m.actor, m.director
FROM
    Item i
left outer join
    Book b on i.ITEM_ID=b.ITEM_ID
left outer join
    Album a on i.ITEM_ID=a.ITEM_ID
left outer join
    Movie m on i.ITEM_ID=m.ITEM_ID</code></pre>
<h3 id="type">TYPE</h3>
<p>TYPE은 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 주로 사용</p>
<pre><code class="language-sql">//Item 중 Book, Movie 조회
//JPQL
select i from Item i
where type(i) IN (Book, Movie)

//SQL
SELECT i FROM Item i
WHERE i.DTYPE in (&#39;B&#39;, &#39;M&#39;)</code></pre>
<h3 id="treatjpa-21">TREAT(JPA 2.1)</h3>
<p>TREAT는 JPA2.1에 추가된 기능인데 자바의 타입 캐스팅과 비슷하다. 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다. JPA 표준은 FROM, WHERE절에서 사용할 수 있지만, 하이버네이트는 SELECT 절에서도 TREAT를 사용할 수 있다.</p>
<pre><code class="language-sql">//JPQL
select i from Item i where treat(i as Book).author = &#39;kim&#39;

//SQL
select i.* from Item i
where
    i.DTYPE=&#39;B&#39;
    and i.author=&#39;kim&#39;</code></pre>
<p>JPQL을 보면 treat를 사용해서 부모 타입 Item을 자식 타입 Book으로 다뤄 author 필드에 접근하고 있다.</p>
<h2 id="기타-정리">기타 정리</h2>
<ul>
<li>enum은 = 비교 연산만 지원</li>
<li>임베디드 타입은 비교를 지원하지 않는다.</li>
</ul>
<h3 id="empty-string">EMPTY STRING</h3>
<p>JPA표준은 &#39;&#39;와 같이 길이 0인 빈 문자열을 Empty String으로 정했지만 데이터베이스에 따라 &#39;&#39;을 NULL로 사용하기도 해서(대표적으로 오라클) 확인하고 사용해야 한다.</p>
<h3 id="null-정의">NULL 정의</h3>
<h3 id="1-and-연산">1. <strong>AND 연산</strong></h3>
<table>
<thead>
<tr>
<th>값 1</th>
<th>값 2</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>TRUE</td>
<td>TRUE</td>
<td>TRUE</td>
</tr>
<tr>
<td>TRUE</td>
<td>FALSE</td>
<td>FALSE</td>
</tr>
<tr>
<td>TRUE</td>
<td>NULL</td>
<td>NULL</td>
</tr>
<tr>
<td>FALSE</td>
<td>TRUE</td>
<td>FALSE</td>
</tr>
<tr>
<td>FALSE</td>
<td>FALSE</td>
<td>FALSE</td>
</tr>
<tr>
<td>FALSE</td>
<td>NULL</td>
<td>FALSE</td>
</tr>
<tr>
<td>NULL</td>
<td>TRUE</td>
<td>NULL</td>
</tr>
<tr>
<td>NULL</td>
<td>FALSE</td>
<td>FALSE</td>
</tr>
<tr>
<td>NULL</td>
<td>NULL</td>
<td>NULL</td>
</tr>
</tbody></table>
<h3 id="2-or-연산">2. <strong>OR 연산</strong></h3>
<table>
<thead>
<tr>
<th>값 1</th>
<th>값 2</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>TRUE</td>
<td>TRUE</td>
<td>TRUE</td>
</tr>
<tr>
<td>TRUE</td>
<td>FALSE</td>
<td>TRUE</td>
</tr>
<tr>
<td>TRUE</td>
<td>NULL</td>
<td>TRUE</td>
</tr>
<tr>
<td>FALSE</td>
<td>TRUE</td>
<td>TRUE</td>
</tr>
<tr>
<td>FALSE</td>
<td>FALSE</td>
<td>FALSE</td>
</tr>
<tr>
<td>FALSE</td>
<td>NULL</td>
<td>NULL</td>
</tr>
<tr>
<td>NULL</td>
<td>TRUE</td>
<td>TRUE</td>
</tr>
<tr>
<td>NULL</td>
<td>FALSE</td>
<td>NULL</td>
</tr>
<tr>
<td>NULL</td>
<td>NULL</td>
<td>NULL</td>
</tr>
</tbody></table>
<h3 id="3-not-연산">3. <strong>NOT 연산</strong></h3>
<table>
<thead>
<tr>
<th>값</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>TRUE</td>
<td>FALSE</td>
</tr>
<tr>
<td>FALSE</td>
<td>TRUE</td>
</tr>
<tr>
<td>NULL</td>
<td>NULL</td>
</tr>
</tbody></table>
<hr>
<h2 id="엔티티-직접-사용">엔티티 직접 사용</h2>
<h3 id="기본-키-값">기본 키 값</h3>
<p>객체 인스턴스는 참조 값으로 식별하고 테이블 로우는 <strong>기본 키</strong> 값으로 식별한다. 따라서 JQPL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본 키값을 사용한다.</p>
<pre><code class="language-sql">select count(m.id) from Member m    // 엔티티의 아이디를 사용
select count(m) from Member m        // 엔티티를 직접 사용

//실행 SQL 동일
select count(m.id) as cnt
from Member m</code></pre>
<h3 id="외래-키-값">외래 키 값</h3>
<p>외래 키 값을 사용하는 예제를 보자. 특정 팀에 소속된 회원을 조회해보자. </p>
<pre><code class="language-java">Team team = em.find(Team.class, 1L);

String qlString = &quot;select m from Member m where m.team = :team&quot;;
List resultList = em.creqteQuery(qlString)
                    .setParameter(&quot;team&quot;, team)
                    .getResultList();</code></pre>
<pre><code class="language-sql">// 실행 SQL
select m.*
from Member m
where m.team_id=? (팀 파라미터의 ID 값)                          </code></pre>
<p>m.team.id를 보면 Member와 Team 간에 묵시적 조인이 일어날 것 같지만 MEMBER 테이블이 team_id 외래 키를 가지고 있으므로 묵시적 조인은 일어나지 않는다.</p>
<h2 id="named-쿼리--정적-쿼리">Named 쿼리 : 정적 쿼리</h2>
<ul>
<li>동적 쿼리 :JPQL을 문자로 완성해서 직접 넘기는 것을 동적쿼리라고 한다. 런타임에 특정 조건에 따라 JPQL을 동적으로 구성할 수 있다.</li>
<li>정적 쿼리 : 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용하는 것을 정적쿼리, Named 쿼리라고 한다. Named 쿼리는 한 번 정의하면 변경할 수 없다.</li>
</ul>
<p>Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해 둔다. 따라서 오류를 빨리 확인할 수있고, 사용하는 시점에는 파싱된 결과를 재사용하므로 성능상 이점도 있다. 그리고 Named 쿼리는 변하지 않는 정적 SQL이 생성되므로 데이터베이스의 조회 성능 최적화에 도움이 된다.</p>
<h3 id="named-쿼리---어노테이션-정의">Named 쿼리 - 어노테이션 정의</h3>
<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 {
    ...
}    

// @NamedQuery 사용
List&lt;Member&gt; resultList = em.createNamedQuery(&quot;Member.findByUsername&quot;, Member.class)
                            .setParameter(&quot;username&quot;, &quot;회원1&quot;)
                            .getResultList();</code></pre>
<p>어노테이션을 통해 Named 쿼리를 사용하려면 em.createNamedQuery() 메서드에 Named 쿼리 이름을 입력하면 된다.</p>
<h3 id="named-쿼리를-xml에-정의">Named 쿼리를 XML에 정의</h3>
<p>어노테이션을 사용하는 것이 더 직관적이지만 XML에 정의하는 것이 더 편리하다고 한다.
자바 언어로 멀티라인 문자를 다루는 것이 귀찮기 때문에 그런데 추가적으로 아래와 같은 장점도 있다.</p>
<p>Named 쿼리를 XML에서 정의하는 것이 더 편리할 수 있는 이유는 다음과 같습니다:</p>
<h3 id="1-애플리케이션-코드와-쿼리의-분리"><strong>1. 애플리케이션 코드와 쿼리의 분리</strong></h3>
<ul>
<li>XML에 쿼리를 정의하면 애플리케이션 코드와 쿼리가 분리됩니다.  <ul>
<li>이로 인해 쿼리 변경이 필요할 때 코드를 수정하지 않아도 됩니다.  </li>
<li>유지보수가 더 쉬워지고, 특히 쿼리가 자주 변경되는 경우 유용합니다.</li>
</ul>
</li>
</ul>
<h3 id="2-배포-없이-변경-가능"><strong>2. 배포 없이 변경 가능</strong></h3>
<ul>
<li>대부분의 경우 XML은 재배포 없이 수정이 가능합니다.  <ul>
<li>애플리케이션 실행 중에도 XML 설정 파일을 수정할 수 있으며, 이를 통해 동적인 쿼리 변경이 가능합니다.  </li>
<li>코드에 포함된 쿼리를 수정하려면 재컴파일 및 재배포가 필요합니다.</li>
</ul>
</li>
</ul>
<h3 id="3-쿼리-관리의-중앙화"><strong>3. 쿼리 관리의 중앙화</strong></h3>
<ul>
<li>XML은 쿼리를 한 곳에 모아서 관리할 수 있습니다.  <ul>
<li>이는 대규모 프로젝트에서 쿼리 관리와 검색을 더 용이하게 합니다.  </li>
<li>코드에 분산된 쿼리를 찾는 것보다 훨씬 직관적입니다.</li>
</ul>
</li>
</ul>
<h3 id="4-버전-관리-및-협업에-유리"><strong>4. 버전 관리 및 협업에 유리</strong></h3>
<ul>
<li>XML 파일은 독립적인 리소스이므로, 코드와는 별도로 버전 관리가 가능합니다.  <ul>
<li>개발자 간 협업 시 코드 충돌을 줄일 수 있습니다.</li>
</ul>
</li>
</ul>
<h3 id="5-설정-및-환경-종속성-처리-용이"><strong>5. 설정 및 환경 종속성 처리 용이</strong></h3>
<ul>
<li>XML 파일은 특정 환경(예: 개발, 테스트, 운영)에 따라 다른 쿼리를 쉽게 적용할 수 있습니다.  <ul>
<li>프로파일링 또는 조건부 로드가 가능해, 다중 환경에 적응할 수 있습니다.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="단점도-고려해야-함"><strong>단점도 고려해야 함</strong></h3>
<ul>
<li>XML은 쿼리를 별도 파일에 작성해야 하므로, 코드와 연계성을 유지하는 데 주의가 필요합니다.</li>
<li>IDE의 자동 완성 및 타입 검증 같은 기능이 쿼리에 대해 제한될 수 있습니다.</li>
</ul>
<h4 id="간단예시">간단예시</h4>
<pre><code class="language-xml">//META-INF/ormMember.xml 에 정의한 Named 쿼리
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;entity-mappings xmlns=&quot;http://xmlns.jcp.org/xml/ns/persistence/orm&quot; version=&quot;2.1&quot;&gt;
    &lt;named-query name=&quot;Member.findByUsernameAndStatus&quot;&gt;
        &lt;query&gt;
            &lt;![CDATA[
                SELECT m
                FROM Member m
                WHERE m.username = :username
                AND m.status = :status
            ]]&gt;
        &lt;/query&gt;
    &lt;/named-query&gt;
&lt;/entity-mappings&gt;</code></pre>
<p>ormMember.xml을 인식하도록 META-INF/persitence.xml에 다음 코드를 추가해야 한다.</p>
<pre><code class="language-xml">&lt;persistence-unit name=&quot;jpabook&quot;&gt;
      &lt;mapping-file&gt;META-INF/ormMember.xml&lt;/mapping-file&gt;
      ...</code></pre>
<h4 id="환경에-따른-설정">환경에 따른 설정</h4>
<p>만약 XML과 어노테이션에 같은 설정이 있으면 <strong>XML이 우선권</strong>을 가진다. 예를 들어 같은 이름의 Named 쿼리가 있으면 XML에 정의한 것이 사용된다. 따라서 애플리케이션이 운영 환경에 따라 다른 쿼리를 실행해야 한다면 각 환경에 맞춘 XML을 준비해 두고 XML만 변경해서 배포하면 된다.</p>
<p>참조 : [자바 ORM 표준 JPA 프로그래밍]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[9장 값 타입]]></title>
            <link>https://velog.io/@now_here/9%EC%9E%A5-%EA%B0%92-%ED%83%80%EC%9E%85</link>
            <guid>https://velog.io/@now_here/9%EC%9E%A5-%EA%B0%92-%ED%83%80%EC%9E%85</guid>
            <pubDate>Sun, 15 Dec 2024 14:02:13 GMT</pubDate>
            <description><![CDATA[<p>JPA의 데이터 타입은 크게 <strong>엔티티 타입</strong>과 <strong>값 타입</strong>으로 나눌 수 있다.</p>
<ul>
<li>엔티티 타입 : @Entity로 정의하는 객체</li>
<li>값 타입 : 자바 기본 타입이나 객체
기본 값 타입(basic value type) : 자바 기본 타입, 래퍼 클래스, String
임베디드 타입 (embedded type) : 복합값 타입
컬렉션 값 타입 collection value type</li>
</ul>
<p>엔티티 타입은 식별자를 통해 지속해서 추적할 수 있지만, 값 타입은 식별자가 없고 숫자나 문자같은 속성만 있으므로 추적할 수 없다.</p>
<hr>
<h1 id="기본값-타입">기본값 타입</h1>
<p>Member 엔티티는 id 라는 식별자 값도 가지고 생명주기도 있지만 값 타입인 name, age 속성은 식별자 값도 없고 생명주기도 회원 엔티티에 의존한다. 그래서 회원 엔티티 인스턴스를 제거하면 name, age 값도 제거된다. 그리고 값 타입은 공유하면 안 된다.</p>
<hr>
<h1 id="임베디드-타입복합-값-타입">임베디드 타입(복합 값 타입)</h1>
<p>새로운 값 타입을 직접 정의해서 사용할 수 있는데, JPA에서는 이것을 <strong>임베디드 타입emvedded type</strong>이라고 한다. 중요한 것은 직접 정의한 임베디드 타입도 int, String 처럼 값 타입이라는 것이다.</p>
<p>회원이 상세한 데이터를 그대로 가지고 있는 것은 객체지향적이지 않으며 응집력만 떨어뜨린다. 대신에 근무 기간, 주소 같은 타입이 있다면 코드가 더 명확해질 것이다. [근무기간], [집 주소]를 가지도록 임베디드 타입을 사용하자.</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.Date startDate;
    @Temporal(TemporalType.DATE) java.util.Date endDate;
    //
    public boolean isWork(Date date) {
        //.. 값 타입을 위한 메서드를 정의할 수 있다.
    }
}

// 주소 임베디드 타입
@Embeddable
public class Address {

    @Column(name = &quot;city&quot;)  // 매핑할 컬럼 정의 가능
    private String city;
    private String street;
    private String zipcode;
    //..
}
</code></pre>
<p><img src="https://velog.velcdn.com/images/now_here/post/001c96aa-6f6e-4ec4-a4eb-c1cf1bd7a77e/image.png" alt=""></p>
<p>새로 정의한 값 타입들은 재사용할 수 있고 응집도도 아주 높다. 또한 Period.isWork()처럼 해당 값 타입만 사용하는 의미 있는 메서드도 만들 수 있다.
임베디드 타입을 사용하려면 두 가지 어노테이션을 사용해야 하며 둘 중에 하나는 생략해도 된다.</p>
<ul>
<li>@Embeddable : 값 타입을 정의하는 곳에 표시</li>
<li>@Embedded :  값 타입을 사용하는 곳에 표시</li>
</ul>
<p>임베디드 타입은 기본 생성자가 필수다.
또한 임베디드 타입을 포함한 모든 값 타입은 엔티티의 생명주기에 의존하므로 엔티티와 임베디드 타입의 관계를 UML로 표현하면 <strong>컴포지션(composition) 관계</strong>가 된다.</p>
<p>임베디드 타입은 엔티티의 값일 뿐이다. 따라서 값이 속한 엔티티의 테이블에 매핑한다. 예제에서 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다. 임베디드 타입 덕분에 객체와 테이블을 세밀하게 매핑하는 것이 가능하다. 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더많다.</p>
<h2 id="임베디드-타입과-연관관계">임베디드 타입과 연관관계</h2>
<p>임베디드 타입은 값 타입을 포함하거나 엔티티를 참조할 수 있다.</p>
<h2 id="attributeoverride--속성-재정의">@AttributeOverride : 속성 재정의</h2>
<p>임베디드 타입에 정의한 매핑정보를 재정의하려면 엔티티에 @AttributeOverride를 사용하면 된다. 예를 들어 회원에게 주소를 하나 더 추가해보자.</p>
<pre><code class="language-java">@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @Embedded Address homeAddress;  // 집 주소
    @Embedded Address companyAddress;  // 회사 주소

    //
}    </code></pre>
<p>이처럼 주소에 집 주소와 회사 주소를 추가하였다. 이런 경우 테이블에 매핑하는 컬럼명이 중복되어 문제가 발생한다.
@AttributeOverride를 사용하여 매핑정보를 재정의해야 한다.</p>
<pre><code class="language-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(name = &quot;COMPANY_ZIPCODE&quot;)),
    })    
    Address companyAddress;  // 회사 주소
    //
}    </code></pre>
<p>이렇게 변경하면 생성된 테이블에서 재정의한대로 변경된 것을 확인할 수 있다.</p>
<pre><code class="language-sql">CREATE TABLE MEMBEER (
    COMPANY_CITY varchar(255),
    COMPANY_STREET varchar(255),
    COMPANY_ZIPCODE varchar(255),
    city varchar(255),
    street varchar(255),
    zipcode varchar(255),
    ...</code></pre>
<ul>
<li>@AttributeOverrides는 엔티티에 설정해야 한다. 임베디드 타입이 임베디드 타입을 가지고 있어도 엔티티에 설정해야 한다.</li>
</ul>
<h2 id="임베디드-타입과-null">임베디드 타입과 null</h2>
<p>임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이 된다. 예를 들어 Address를 null로 설정하면, city, street, zipcode가 모두 null로 설정된다.</p>
<hr>
<h1 id="값-타입과-불변-객체">값 타입과 불변 객체</h1>
<h2 id="값-타입-공유-참조">값 타입 공유 참조</h2>
<p>임베디드 타입은 여러 엔티티에서 공유하면 위험하다.</p>
<pre><code class="language-java">member1.setHomeAddress(new Address(&quot;OldCity&quot;));
Address address = member.getHomeAddress();

address.setCity(&quot;NewCity&quot;);    //회원1의 address 값을 공유해서 사용
member2.setHomeAddress(address);</code></pre>
<p>회원2에 새로운 주소를 할당하려고 회원1의 address를 그대로 참조해서 사용했다. 회원2의 주소만 &quot;NewCity&quot;로 바뀔것 같지만 회원1의 주소도 &quot;NewCity&quot;로 변경되어 버린다. 회원1과 회원2가 같은 address 인스턴스를 참조했기 때문이다. 영속성 컨텍스트는 회원1과 회원2의 city속성이 변경된 것으로 판단되어 각각 UPDATE SQL을 실행한다.</p>
<h2 id="값-타입-복사">값 타입 복사</h2>
<p>위에서 본 문제를 해결하기 위해서 값(인스턴스)을 복사해서 사용해야 한다. 예를 들어, clone() 를 만들어 자신을 복사해서 반환하고 이를 이용해서 새로운 Address를 만드는 것이다.
문제는 객체 타입이다. 객체에 값을 대입하면 항상 참조 값을 전달한다.</p>
<pre><code class="language-java">Address a = new Address(&quot;Old&quot;);
Address b = a;    //객체 타입은 항상 참조 값을 전달
b.setCity(&quot;New&quot;);</code></pre>
<p>Address b=a에서 a가 참조하는 인스턴스의 참조 값을 b에 넘겨주어 같은 인스턴스를 공유 참조한다.
물론 앞서 말한 것처럼, 각체를 대입할 때마다 인스턴스를 복사해서 대입하면 공유 참조를 피할 수 있다.
<strong>그러나 문제는 복사하지 않고 원본의 참조 값을 직접 넘기는 것을 막을 방법이 없다는 것이다.</strong>
객체의 공유 참조는 피할 수 없다. 따라서 근복적인 해결책이 필요한데 가장 단순한 방법은 객체의 값을 수저하지 못하게 막으면 된다. 예를 들어 Address 객체의 setCity() 같은 수정자 메서드를 모두 제거하는 것이다. 이렇게 하면 공유 참조를 해도 값을 변경하지 못하므로 부작용의 발생을 막을 수 있다.</p>
<h2 id="불변-객체">불변 객체</h2>
<p>값 타입은 부작용 없이 사용해야 한다. 그러기 위해서 객체를 불변하게 만들면 값을 수정할 수 없으므로 부작용을 원천 차단할 수 있다.
한 번 만들면 절대 변경할 수 없는 객체를 <strong>불변 객체</strong>라고 한다. 불변 객체의 값은 조회할 수 있지만 수정할 수 없다.
불변 객체를 구현하는 가장 간단한 방법은 생성자로만 값을 설정하고 수정자를 만들지 않는 것이다. Integer, String은 자바가 제공하는 대표적인 불변 객체이다.</p>
<hr>
<h1 id="값-타입의-비교">값 타입의 비교</h1>
<ul>
<li>동일성 Identity 비교 : 인스턴스의 참조 값을 비교 ( == 사용)</li>
<li>동등성 Equivalence 비교 : 인스턴스의 값을 비교 ( equals() 사용)</li>
</ul>
<p><strong>값 타입은 비록 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야 한다.</strong> 따라서 값 타입을 비교할 때는 동등성비교를 해야한다. 물론 값 타입의 equals() 메서드를 재정의해야 한다. 재정의할 때 보통 모든 필드의 값을 비교하도록 구현한다. 자바에서 equals()를 재정의하면 hashCode()도 재정의해야지 해시를 사용하는 컬렉션(HashSet, HashMap) 사용시 안전하다.</p>
<hr>
<h1 id="값-타입-컬렉션">값 타입 컬렉션</h1>
<p>값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection, @CollectionTable 을 사용하면 된다.</p>
<pre><code class="language-java">@Entity
public class Member {

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

    @Embedded
    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; faovriteFoods = 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;();
    //
}

@Embeddable
public class Address {

    @Column
    private String city;
    private String street;
    private String zipcode;
    //
}</code></pre>
<p><img src="https://velog.velcdn.com/images/now_here/post/915bf7bc-0b49-40fb-b4c9-606e970422bb/image.png" alt=""></p>
<p>favoriteFoods는 기본값 타입인 String을 컬렉션으로 가진다. 이것을 데이터베이스 테이블로 매핑해야 하는데
관계형 데이터베이스의 테이블은 컬럼안에 컬렉션을 포함할 수 없다. 따라서 별도의 테이블을 추가하고 @ColumnTablee을 사용해서 추가한 테이블을 매핑해야 한다. 그리고 favoriteFoods처럼 값으로 사용되는 컬럼이 하나면 @Column을 사용해서 컬럼명을 지정할 수 있다.
addressHistory는 임베디드 타입인 Address를 컬렉션으로 가진다. 이것도 마찬가지로 별도의 테이블을 사용해야 한다. 그리고 테이블 매핑정보는 @AttributeOverride를 사용해서 재정의할 수 있다.</p>
<p>값 타입 컬렉션은 영속성 전이(Cascade) + 고아객체 제거(ORPHAN REMOVE) 기능을 필수로 가진다고 볼 수 있다.
값 타입 컬렉션도 조회할 때 fetch 전략을 선택할 수 있는데 LAZY가 기본이다.</p>
<h2 id="값-타입-컬렉션-수정">값 타입 컬렉션 수정</h2>
<pre><code class="language-java">        Member member = em.find(Member.class, 1L);

        //1. 임베디드 값 타입 수정
        member.setHomeAddress(new Address(&quot;새로운도시&quot;, &quot;신도시1&quot;, &quot;123456&quot;));

        //2. 기본값 타입 컬렉션 수정
        Set&lt;String&gt; favoriteFoods = member.getFavoriteFoods();
        favoriteFoods.remove(&quot;탕수육&quot;);
        favoriteFoods.add(&quot;치킨&quot;);

        //3. 임베디드 값 타입 컬렉션 수정
        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>
<ol>
<li><p><strong>임베디드 값 타입 수정</strong>
homeAddress 임베디드 값 타입은 MEMBER 테이블과 매핑했으므로 MEMBER 테이블만 UPDATE한다. Member엔티티 수정하는 것과 같다.</p>
</li>
<li><p><strong>기본값 타입 컬렉션 수정</strong>
탕수육을 치킨으로 변경하려면 탕수육을 제거하고 치킨을 추가해야 한다. 자바의 String 타입은 수정할 수 없다.</p>
</li>
<li><p><strong>임베디드 값 타입 컬렉션 수정</strong>
값 타입은 불변해야 한다. 따라서 컬렉션에서 기본 주소를 삭제하고 새로운 주소를 등록했다. 참고로 값 타입은 equals, hashcode를 꼭 구현해야 한다.</p>
</li>
</ol>
<h2 id="값-타입-컬렉션의-제약사항">값 타입 컬렉션의 제약사항</h2>
<p>값 타입 컬렉션에 보관된 값 타입들은 별도의 테이블에 보관된다. 따라서 여기에 보관된 값 타입의 값이 변경되면 데이터베이스에 있는 원본 데이터를 찾기 어렵다는 문제가 있다. 이런 문제로 인해 JPA 구현체들은 값 타입 컬렉션에 변경 사항이 발생하면, 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장한다.</p>
<p>따라서 실무에서는 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신에 일대다 관계를 고려해야 한다. 추가로 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다. 따라서 데이터베이스 기본 키제약 조건으로 인해 컬럼에 null을 입력할 수 없고, 같은 값을 중복해서 저장할 수 없는 제약도 있다.
지금까지 설명한 문제를 해결하려면 값 타입 컬렉션을 사용하는 대신에 새로운 엔티티를 만들어서 일대다 관계로 설정하면 된다. 여기에 추가로 영속성 전이(Cascade) + 고아객체 제거(ORPHAN REMOVE) 기능을 적용하면 값 타입 컬렉션처럼 사용할 수 있다.</p>
<pre><code class="language-java">@Entity
public class AddressEntity {

    @Id @GeneratedValue
    private Long id;

    @Embedded Address address;
    //


}

// 설정 코드
    @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>
<hr>
<h1 id="참고">참고</h1>
<h2 id="1-컴포지션이-관계가-뭔데엔티티와-임베디드-타입의-관계">1. 컴포지션이 관계가 뭔데?(엔티티와 임베디드 타입의 관계)</h2>
<p><strong>컴포지션(Composition) 관계</strong>는 객체지향 설계에서 두 클래스 간의 강한 의존 관계를 나타내는 관계 유형입니다. UML(Unified Modeling Language) 다이어그램에서 사용되는 이 용어는 <strong>&quot;전체와 부분&quot;</strong> 간의 관계를 모델링합니다.  </p>
<h3 id="1-컴포지션의-특징">1. <strong>컴포지션의 특징</strong></h3>
<ul>
<li><p><strong>전체와 부분의 강한 결합</strong>: </p>
<ul>
<li>&quot;전체(Whole)&quot; 객체가 삭제되면, &quot;부분(Part)&quot; 객체도 함께 삭제됩니다.  </li>
<li>예: <code>엔티티와 값 타입</code>의 관계에서 엔티티가 삭제되면 값 타입도 함께 소멸합니다.</li>
</ul>
</li>
<li><p><strong>독립적인 존재 불가</strong>:  </p>
<ul>
<li>&quot;부분&quot; 객체는 &quot;전체&quot; 객체에 속해야 하며, &quot;전체&quot; 객체 없이는 독립적으로 존재할 수 없습니다.  </li>
<li>예: 사람이 없으면 심장(Heart) 객체가 존재할 이유가 없는 것과 같습니다.</li>
</ul>
</li>
<li><p><strong>UML 표기</strong>:</p>
<ul>
<li>컴포지션 관계는 UML에서 <strong>실선과 채워진 다이아몬드</strong>로 표시됩니다.  </li>
<li>예:  <pre><code class="language-plaintext">┌─────────┐        ┌──────────┐
│   집    │────◆────│   방     │
└─────────┘        └──────────┘</code></pre>
&quot;집&quot;이 삭제되면 &quot;방&quot;도 함께 삭제됩니다.</li>
</ul>
</li>
</ul>
<h3 id="2-컴포지션과-어그리게이션aggregation-비교">2. <strong>컴포지션과 어그리게이션(Aggregation) 비교</strong></h3>
<ul>
<li><strong>컴포지션(Composition)</strong>: 강한 결합. 전체와 부분은 생명주기를 공유.</li>
<li><strong>어그리게이션(Aggregation)</strong>: 약한 결합. 전체와 부분은 독립적인 생명주기.  <ul>
<li>UML에서는 비어 있는 다이아몬드로 어그리게이션을 표시합니다.</li>
</ul>
</li>
</ul>
<table>
<thead>
<tr>
<th>특징</th>
<th>컴포지션</th>
<th>어그리게이션</th>
</tr>
</thead>
<tbody><tr>
<td>생명주기 의존성</td>
<td>전체 삭제 → 부분도 삭제</td>
<td>전체 삭제 → 부분은 독립적으로 유지</td>
</tr>
<tr>
<td>예</td>
<td>사람과 심장</td>
<td>자동차와 타이어</td>
</tr>
<tr>
<td>UML 표기</td>
<td>채워진 다이아몬드</td>
<td>비어 있는 다이아몬드</td>
</tr>
</tbody></table>
<h3 id="3-jpa와-컴포지션">3. <strong>JPA와 컴포지션</strong></h3>
<p>JPA에서 <strong>값 타입(예: @Embeddable)</strong>은 엔티티에 포함되어 컴포지션 관계를 형성합니다.  </p>
<ul>
<li><p><strong>값 타입의 생명주기</strong>: 값 타입은 엔티티와 동일한 생명주기를 가집니다.</p>
<ul>
<li>엔티티가 삭제되면 값 타입도 함께 삭제됩니다.</li>
</ul>
</li>
<li><p><strong>임베디드 타입 예제</strong>:  </p>
<pre><code class="language-java">@Embeddable
public class Period {
    private LocalDate startDate;
    private LocalDate endDate;

    public boolean isWork() {
        return LocalDate.now().isAfter(startDate) &amp;&amp; LocalDate.now().isBefore(endDate);
    }
}

@Entity
public class Employee {
    @Id
    private Long id;

    @Embedded
    private Period workPeriod;
}</code></pre>
</li>
</ul>
<p>위 코드에서 <code>Employee</code>와 <code>Period</code>는 컴포지션 관계입니다. <code>Employee</code>가 삭제되면 <code>Period</code>도 함께 삭제됩니다.</p>
<h3 id="4-왜-중요한가">4. <strong>왜 중요한가?</strong></h3>
<p>컴포지션은 객체지향 설계에서 객체의 강한 결합 관계를 나타내며, 데이터의 생명주기를 관리하고 데이터 무결성을 유지하는 데 유용합니다. JPA에서는 이를 통해 <strong>엔티티와 값 타입의 관계를 자연스럽게 표현</strong>할 수 있습니다.</p>
<hr>
<h2 id="2-값-타입-비교는-왜-동등성-비교를-해야-하는데-equals">2. 값 타입 비교는 왜 동등성 비교를 해야 하는데? (equals())</h2>
<p>값 타입을 비교할 때, 값이 같은지 여부에 따라 비교하는 이유는 <strong>불변 객체(immutable object)</strong>와 관련이 있습니다. 불변 객체는 그 상태가 생성된 이후 변경되지 않는 객체로, 주로 값 타입을 불변 객체로 설계합니다. </p>
<h3 id="1-불변-객체와-값-타입의-관계">1. <strong>불변 객체와 값 타입의 관계</strong></h3>
<p>값 타입은 주로 불변 객체로 설계됩니다. 불변 객체의 특성상 한 번 생성되면 객체의 내부 상태를 변경할 수 없기 때문에, 값 타입의 비교 시 객체의 <strong>상태(state)</strong>를 기반으로 동등성을 판단합니다. 값 타입의 내부 속성 값이 같으면 두 객체는 사실상 <strong>동일한 의미를 가진 객체</strong>로 간주되며, 이렇게 동일한 값을 가진 객체들이 &quot;같다&quot;고 평가됩니다.</p>
<h3 id="2-값-타입-비교">2. <strong>값 타입 비교</strong></h3>
<p>JPA에서는 값 타입이 <strong>동등성 비교</strong>를 할 때 객체의 <strong>식별자</strong>가 아니라 <strong>값</strong>을 비교합니다. 값 타입의 경우, 두 객체가 같은 값을 가지면 <strong>같은 객체</strong>로 취급되며, 이는 불변 객체의 특성상 자연스러운 설계입니다. 예를 들어, <code>Period</code>라는 값 타입이 <code>startDate</code>와 <code>endDate</code>를 가지고 있다면, 두 <code>Period</code> 객체가 동일한 <code>startDate</code>와 <code>endDate</code>를 가졌다면 그들은 동일한 값 타입으로 간주됩니다.</p>
<h3 id="3-jpa의-값-타입-처리">3. <strong>JPA의 값 타입 처리</strong></h3>
<p>JPA에서는 값 타입을 객체로 취급하되, 비교할 때는 객체의 참조가 아니라 그 <strong>값</strong>을 비교합니다. 예를 들어, <code>@Embeddable</code>로 정의된 클래스에서 <code>equals()</code>와 <code>hashCode()</code> 메서드를 오버라이드하여 객체의 값 비교를 수행할 수 있습니다. 이렇게 하면, 객체의 인스턴스가 다르더라도 그 값이 같으면 동일한 객체로 간주합니다.</p>
<h3 id="4-불변성과-동등성-비교의-중요성">4. <strong>불변성과 동등성 비교의 중요성</strong></h3>
<p>불변 객체의 특성은 여러 상황에서 유용합니다. 예를 들어, 데이터베이스에서 객체를 비교할 때, 값이 동일한 두 객체를 동일한 객체로 취급함으로써 <strong>효율적</strong>이고 <strong>일관성</strong> 있는 데이터를 관리할 수 있습니다. 이 과정에서 객체의 인스턴스가 다르더라도 값만 같다면 동일한 값 타입으로 취급되기 때문에, 애플리케이션 로직에서 객체의 비교가 더 단순해집니다.</p>
<h3 id="결론">결론</h3>
<p>따라서 값 타입은 불변 객체의 특성을 가지며, 그 값이 같으면 동일한 객체로 취급되는 이유는 값 타입이 &quot;상태를 비교&quot;하는 방식으로 설계되어 있기 때문입니다. 이 방식은 데이터의 일관성을 유지하고 비교를 더 효율적으로 만들어 줍니다.</p>
<p>참조 : [자바 ORM 표준 JPA 프로그래밍]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[8장 프록시와 연관관계 관리]]></title>
            <link>https://velog.io/@now_here/8%EC%9E%A5-%ED%94%84%EB%A1%9D%EC%8B%9C%EC%99%80-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@now_here/8%EC%9E%A5-%ED%94%84%EB%A1%9D%EC%8B%9C%EC%99%80-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Sun, 15 Dec 2024 11:39:14 GMT</pubDate>
            <description><![CDATA[<p>객체는 객체그래프 탐색을 통해 연관된 객체를 탐색한다. 그러나 데이터베이스에서는 나뉘어서 저장되어 있기 때문에 연관된 객체를 마음껏 탐색하기에 어려움이 있다. 이에 JPA에서는 <strong>프록시</strong>를 통해 이 문제를 해결하고 있다.
<strong>프록시</strong>를 사용하면 연관된 객체를 처음부터 데이터베이스에서 조회하는 것이 아니라, 실제 사용하는 시점에 데이터베이스에서 조회할 수 있다. 물론, 환경상 바로 조회하는 것이 유리할 경우도 있기 때문에 JPA는 즉시 로딩과 지연 로딩 모두를 지원한다.</p>
<h1 id="프록시">프록시</h1>
<h2 id="지연로딩">지연로딩</h2>
<p>객체를 조회할 때, 연관된 객체가 필요없는 경우에도 연관된 객체까지 데이터베이스에서 조회하는 것은 효율적이지 않다. 이에 JPA에서는 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이를 <strong>지연로딩</strong>이라고 한다.
<strong>지연 로딩</strong>을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수있는 가짜 객체가 필요한데 이것을 <strong>프록시 객체</strong>라고 한다.</p>
<h2 id="프록시-기초">프록시 기초</h2>
<p>JPA에서 식별자 하나로 엔티티를 조회할 때 EntityManager.find()를 사용한다. 이 메소드는 영속성 컨텍스트에 엔티티가 없으면 데이터베이스를 조회한다.</p>
<pre><code class="language-java">Member member = em.find(Member.class, &quot;member1&quot;);</code></pre>
<p>이렇게 엔티티를 직접 조회하면 조회한 엔티티를 실제 사용하든, 사용하지 않든 데이터베이스를 무조건 조회한다. 엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶으면 EntityManaer.getRefence() 메소드를 사용하면 된다.</p>
<pre><code class="language-java">Member member = em.getReference(Member.class, &quot;member1&quot;);</code></pre>
<p>이 메소드를 사용하면 JPA에서는 데이터베이스를 조회하지 않고 실제 엔티티 객체도 생성하지 않는다. 대신에 데이터베이스 접근을 위임한 <strong>프록시 객체</strong>를 반환한다.</p>
<h3 id="프록시-특징">프록시 특징</h3>
<p><img src="https://velog.velcdn.com/images/now_here/post/41af87c8-d9c4-4fab-9053-cae587bef9bb/image.png" alt=""></p>
<p>프록시 클래스는 실제 클래스를 상속 받아서 만들어지므로 실제 클래스와 겉모양이 같다.
또한 프록시 객체는 실체 객체에 대한 참조(target)을 보관한다. 그리고 프록시 객체의 메서드를 호출하면 프록시 객체는 실제 객체의 메서드를 호출한다.</p>
<h3 id="프록시-객체의-초기화">프록시 객체의 초기화</h3>
<p>프록시 객체는 member.getName()과 같이 실제 사용될 때 데이터베이스를 조회해 실제 엔티티 객체를 생성한다. 이것을 <strong>프록시 객체의 초기화</strong>라고 한다.</p>
<h3 id="샘플-코드">샘플 코드</h3>
<pre><code class="language-java">
//MemberProxy 반환
Member member = em.getReference(Member.class, &quot;id1&quot;);
member.getName();    //1. getName();
...
///

public class MemberProxy extends Member{

    Member target = null;   // 실제 엔티티 참조

    public String getName() {

        if (target == null) {
            //2. 초기화 요청(초기화)
            //-&gt; 프록시 객체는 실제 엔티티가 생성되어 있지 않으면
            //  영속성 컨텍스트에 실제 엔티티 생성을 요청한다.

            //3. DB 조회
            //  -&gt; 영속성 컨텍스트는 DB를 조회해서 실제 엔티티 객체를 생성한다.

            //4. 실제 엔티티 생성 및 참조 보관
            // -&gt; 프록시 객체는 생성된 실제 엔티티 객체의 참조를
            //  Member target 멤버변수에 보관한다.

            this.target = ...;
        }
        //5. 프록시 객체는 실제 엔티티 객체의 getName()을 호출해서 결과를 반환한다.
        return target.getName();
    }
}</code></pre>
<ul>
<li>프록시 객체는 처음 사용할 때 한 번만 초기화된다.</li>
<li>프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 다만 프록시 객체가 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근할 수 있다.</li>
<li>프록시 겍체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해서 사용해야 한다.</li>
<li>영속성 컨텍스트에 찾는 엔티티가 이미 있으면 데이터베이스를 조회할 필요가 없으므로 em.getRefence()를 호출해도 프록시 객체가 아닌 실제 엔티티를 반환한다.</li>
<li>초기화는 영속성 컨텍스트의 도움을 받아야 가능하다. 따라서 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제가 발생한다. 하이버네이트에서는 org.hibernate.LazyInitializationException예외를 발생시킨다.</li>
</ul>
<h3 id="프록시와-식별자">프록시와 식별자</h3>
<p>프록시 객체로 엔티티로 조회할 때 식별자값을 파라미터로 전달하는데 프록시 객체는 이 식별자값을 보관한다. 그렇기 때문에 <strong>식별자 값을 조회하는 코드에서는 프록시 객체가 초기화되지 않는다.</strong></p>
<pre><code class="language-java">Team team = em.getReference(Team.class, &quot;team1&quot;);    //식별자 보관
team.getId();    //프록시 객체 초기화되지 않음.</code></pre>
<p>엔티티 접근 방식을 프로퍼티로 설정한 경우에만 초기화하지 않고 필드로 설정하면 프록시 객체를 초기화한다고 한다.</p>
<ul>
<li>프로퍼티 접근 : @Access(AccessType.PROPERTY)  -&gt; 식별자 조회시 프록시 객체 초기화 x</li>
<li>필드 접근 : @Access(AccessType.FILED) -&gt; 식별자 조회시에도 프록시 객체 초기화 
하지만 하이버네이트 버전이 올라가면서 프록시 상태일 때, 필드/프로퍼티 접근 모두 getId()를 호출할 때 초기화 하지 않도록 변경됐다고 한다.</li>
</ul>
<h3 id="참고-프록시와-식별자">참고 (프록시와 식별자)</h3>
<p>현대 하이버네이트 동작 (버전 5.x 이후)</p>
<p>하이버네이트 5.x 이상부터는 식별자 조회 시 프록시를 초기화하지 않는 방식으로 최적화되었습니다.
즉, 필드 접근이든 프로퍼티 접근이든 getId() 호출만으로는 프록시 초기화가 발생하지 않습니다.
이는 프록시 상태를 유지하며 필요한 경우에만 데이터베이스와 통신하도록 설계된 개선 사항입니다.
하이버네이트 최적화가 적용된 버전
하이버네이트 5.2 이상에서 이러한 최적화가 더욱 안정적으로 동작하며, 최신 버전(예: Hibernate 6.x)에서도 동일한 원칙이 유지됩니다.</p>
<h3 id="프록시-확인">프록시 확인</h3>
<p>JPA가 제공하는 PersistenceUnitUtil.isLoaded(Object entity) 메서드를 사용하면 프록시 인스턴스의 초기화 여부를 확인할 수 있다. 아직 초기화되지 않은 프록시 인스턴스는 false를, 이미 초기화되었거나 프록시 인스턴스가 아니면 true를 반환한다. (프록시 초기화 상태를 확인하는 것은 로딩된 상태인지를 확인하기 위해 사용한다. 더 자세한 내용은 아래 참고)
직접 클래스를 확인하여 프록시임을 확인할 수도 있다. getClass().getName()을 통해 프록시 객체를 확인하면 클래스 명 뒤에 <strong>..javassist..</strong>라고 되어있다.
JPA는 프록시 강제 초기화가 없지만 하이버네이트에서는 initialize() 메서드로 강제초기화할 수 있다.</p>
<pre><code class="language-java">org.hibernate.Hibernate.initialize(order.getMember());    //프록시 강제 초기화</code></pre>
<p>JPA에서 강제 초기화하려면 member.getName()처럼 프록시의 메서드를 직접 호출하면 된다.</p>
<h3 id="참고프록시-확인">참고(프록시 확인)</h3>
<p><code>PersistenceUnitUtil.isLoaded(Object entity)</code> 메서드의 동작은 프록시 초기화 상태뿐만 아니라 <strong>전달된 객체가 프록시인지 아닌지</strong>도 확인할 수 있도록 설계되었습니다. 따라서 <strong>프록시 인스턴스가 아닌 경우에도 <code>true</code></strong>를 반환하는 것이 정상적인 동작입니다. </p>
<h3 id="이유">이유</h3>
<ol>
<li><p><strong>프록시가 아닌 객체는 이미 &quot;로딩 완료된&quot; 상태</strong>로 간주합니다.  </p>
<ul>
<li>프록시 객체는 엔티티를 완전히 초기화하기 전, 필요한 데이터를 지연 로딩하기 위해 사용됩니다.</li>
<li>프록시가 아닌 객체는 JPA가 데이터를 이미 완전히 로드한 상태이므로 초기화 여부를 따질 필요가 없고, 따라서 <code>true</code>를 반환합니다.</li>
</ul>
</li>
<li><p><strong>isLoaded() 메서드의 설계 철학</strong>  </p>
<ul>
<li><code>isLoaded()</code>는 객체가 현재 로딩된 상태인지 확인하는 메서드입니다.</li>
<li><strong>프록시 초기화 상태를 확인</strong>하는 것이 주요 목적이지만, 프록시가 아닌 경우 &quot;로딩 상태&quot;를 보장하기 위해 무조건 <code>true</code>를 반환합니다.</li>
</ul>
</li>
</ol>
<h3 id="동작-예시">동작 예시</h3>
<pre><code class="language-java">// em은 EntityManager
Team team = em.getReference(Team.class, &quot;team1&quot;);
System.out.println(PersistenceUnitUtil.isLoaded(team)); // false (프록시 초기화 전)

em.find(Team.class, &quot;team1&quot;); 
System.out.println(PersistenceUnitUtil.isLoaded(team)); // true (프록시 초기화 후)

Team team2 = em.find(Team.class, &quot;team2&quot;);
System.out.println(PersistenceUnitUtil.isLoaded(team2)); // true (프록시가 아님, 이미 로드된 상태)</code></pre>
<h3 id="핵심">핵심</h3>
<ul>
<li><strong>프록시 객체가 초기화되지 않은 상태</strong>: <code>isLoaded()</code>는 <code>false</code>를 반환.</li>
<li><strong>프록시 객체가 초기화된 상태 또는 프록시가 아닌 객체</strong>: <code>isLoaded()</code>는 <code>true</code>를 반환.</li>
</ul>
<p>즉, 프록시가 아닌 객체는 <strong>&quot;항상 로딩된 상태&quot;</strong>로 간주되어 <code>true</code>가 반환됩니다. </p>
<h3 id="추가-확인이-필요할-때">추가 확인이 필요할 때</h3>
<p>프록시 여부만 확인하고 싶다면 <code>entity instanceof HibernateProxy</code>를 사용해 직접 확인할 수도 있습니다.</p>
<hr>
<h1 id="즉시-로딩과-지연로딩">즉시 로딩과 지연로딩</h1>
<p>프록시 객체는 주로 연관된 엔티티를 지연 로딩할 때 사용한다. member1이 team1에 속해 있다고 해보자.</p>
<pre><code class="language-java">Member member = em.find(Member.class, &quot;member1&quot;);
Team team = member.getTeam();    //객체 그래프 탐색
System.out.prinln(team.getName());    // 팀 엔티티 사용</code></pre>
<p>회원 엔티티를 조회할 때 팀 엔티티를 조회하는 방법은 다음과 같이 두 가지 방법이 있다.</p>
<ul>
<li>즉시 로딩
엔티티를 조회할 때 연관된 엔티티도 함께 조회된다. em.find(Member.class, &quot;member1&quot;)를 호출할 때 회원 엔티티와 연관된 팀 엔티티도 함께 조회된다.
설정 방법 : @ManyToOne(fetch = FetchType.EAGER)</li>
<li>지연 로딩
연관된 엔티티를 실제 사용할 때 조회한다. member.getTeam().getName()처럼 조회한 팀 엔티티를 실제 사용하는 시점에 JPA가 SQL을 호출해서 팀 엔티티를 조회한다.
설정 방법 : @ManyToOne(fetch = FetchType.LAZY)</li>
</ul>
<h2 id="즉시-로딩">즉시 로딩</h2>
<p>앞서 말한 것처럼 즉시 로딩을 사용하면 em.find(Member.class, &quot;member1&quot;)를 호출할 때 연관된 팀 엔티티도 조회한다. 이 때 회원 테이블과 팀 테이블, 두 개의 테이블을 조회해야 하므로 쿼리를 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>
<p>여기서 LEFT OUTER JOIN을 사용했는데 회원테이블에 TEAM_ID 외래 키에 NULL인 경우에 해당 조인을 사용한다. 내부 조인을 사용하면 팀에 소속되지 않은 회원/팀 모두 조회할 수 없기 때문이다.
외부 조인보다 내부 조인이 성능과 최적화에서 더 유리하다. 그래서 외래 키에 NOT NULL 제약 조건을 설정을 하면 항상 값이 있는 것을 보장하기 때문에 내부 조인만 사용해도 된다.
@JoinColumn에 nullable 속성을 false로 설정해서 내부 조인을 사용하도록 변경할 수 있다. (nullable 속성은 따로 지정하지 않으면 true가 기본값이다.) 또는 @ManyToOne에 optional 속성을 false로 설정해도 된다.</p>
<h2 id="지연-로딩">지연 로딩</h2>
<p>지연 로딩을 사용하려면 @ManyToOne에 fetch 속성에 FetchType.LAZY로 설정하면 된다.</p>
<pre><code class="language-java">@Entity
public class Member {

    //...
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;TEAM_ID&quot;)
    private Team team;
    //...
}

//지연로딩 실행코드
Member member = em.find(Member.class, &quot;member1&quot;);
Team team = member.getTeam();     // 객체 그래프 탐색. 
team.getName();    //팀 객체 실제 사용
</code></pre>
<p><strong>이렇게 지연로딩을 지정한 다음 연관 객체를 탐색하면 team 멤버변수에 member에 저장된 TEAM_ID 외래키 (TEAM 식별자)만을 가진 프록시 객체를 반환한다.</strong>
-&gt; 여기서 team 멤버변수는 식별자 id = &quot;team1&quot;만을 가지고 있는 프록시 객체이다.</p>
<p>team.getName()처럼 실제 사용되기 전까지 데이터 로딩을 미룬다. 이처럼 실제 사용될 때 데이터베이스를 조회하며 프록시 객체의 초기화를 진행한다. 즉, 식별자만 가지고 있던 프록시 객체가 실제 엔티티의 나머지 값들을 받은 대리 객체가 된다는 것이다.
실행되는 SQL을 보자</p>
<pre><code class="language-sql">// em.find(Member.class, &quot;member1&quot;); 호출
SELECT * FROM MEMBER
WHERE MEMBER_ID = &#39;member1&#39;

// team.getName();    //팀 객체 실제 사용
SELECT * FROM TEAM
WHERE TEAM_ID = &#39;team1&#39;</code></pre>
<p>조회 대상이 영속성 컨텍스트에 이미 있으면 프록시 객체를 사용할 이유가 없다. 따라서 프록시 객체가 아닌 실제 객체를 사용한다. 예를 들어 team1 엔티티가 영속성 컨텍스트에 이미 로딩되어 있으면 프록시가 아닌 실제 team1 엔티티를 사용한다.</p>
<h3 id="참고-지연로딩">(참고) 지연로딩</h3>
<blockquote>
<p>지연로딩 사용하려면 em.find()가 아니라 em.getReference()를 호출해야 한다고 하지 않았나?</p>
</blockquote>
<p>@ManyToOne(fetch = FetchType.LAZY) 설정을 통해 <strong>지연 로딩(Lazy Loading)</strong>을 사용할 수 있습니다. 따라서 em.getReference()를 직접 호출하지 않아도 됩니다. 이는 JPA가 FetchType.LAZY를 해석하여 프록시 객체를 자동으로 생성해 주기 때문입니다.</p>
<h3 id="정리">정리</h3>
<ul>
<li>지연 로딩(LAZY) : 연관된 엔티티를 프록시로 조회한다. 프록시를 실제 사용할 때 초기화하면서 데이터베이스를 조회한다.</li>
<li>즉시 로딩(EAGER) : 연관된 엔티티를 즉시 조회한다. 하이버네이트는 가능하면 SQL 조인을 사용해서 한 번에 조회한다. </li>
</ul>
<h2 id="프록시와-컬렉션-래퍼">프록시와 컬렉션 래퍼</h2>
<pre><code class="language-java">Member member = em.find(Member.class, &quot;member1&#39;);
List&lt;Order&gt; orders = member.getOrders();
System.out.prinln(&quot;orders = &quot; + orders.getClass().getName());
//결과 : orders = org.hibernate.collection.internal.PersistentBag</code></pre>
<p>하이버네이트는 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하는데 이를 <strong>컬렉션 래퍼</strong>라고 한다.
엔티티를 지연 로딩하면 프록시 객체를 사용해서 지연 로딩을 수행하지만 주문 내역 같은 컬렉션은 컬렉션 래퍼가 지연 로딩을 처리해준다.
<strong>참고로 member.getOrders()를 호출해도 컬렉션은 초기화되지 않는다.</strong> 컬렉션은 member.getOrders().get(0) 처럼 컬렉션에서 실제 데이터를 조회할 때 데이터베이스를 조회해서 초기화한다.</p>
<h3 id="jpa-기본-fetch-전략">JPA 기본 fetch 전략</h3>
<ul>
<li>@ManyToOne, @OneToOne : 즉시 로딩(FetchType.EAGER)</li>
<li>@OneToMany, @ManyToMany : 지연 로딩(FetchType.LAZY)</li>
</ul>
<p>JPA의 기본 fetch 전략은 연관된 엔티티가 하나면 즉시 로딩을, 컬렉션이면 지연 로딩을 사용한다. 컬렉션을 로딩하는 것은 많은 비용이 들기 때문에 지연 로딩을 하는데 연관된 객체가 하나면 즉시 로딩해도 큰 문제가 발생하지 않기 때문에 그렇다.
추천하는 방법은 모든 연관관계에서 지연 로딩을 사용하고 애플리케이션 개발이 어느 정도 완료단계에 왔을 때 실제 사용하는 상황을 보고 꼭 필요한 곳에만 즉시 로딩을 사용하도록 최적화하는 것이다.</p>
<h2 id="컬렉션에-fetchtypeeager-사용-시-주의점">컬렉션에 FetchType.EAGER 사용 시 주의점</h2>
<ul>
<li><p>컬렉션을 하나 이상 즉시 로딩하는 것을 권장하지 않는다.</p>
</li>
<li><p>컬렉션 즉시 로딩은 항상 외부조인(outer join)을 사용한다.
내부조인 . 연관된 엔티티의 외래키가 not null이 아니라면 조회할 때 null이면 해당 정보 자체가 조회되지 않는 문제가 발생하기 때문에 outer join을 사용해서 조회한다.
예를 들어 팀 테이블에서 회원 테이블로 일대다 관계를 조인할 때 회원이 한명도 없는 팀을 내부조인하면 팀까지 조회되지 않는 문제가 발생한다. 데이터베이스 제약조건으로 이런 상황을 막을 수없어 일대다 관계를 즉시 로딩할 때 항상 외부 조인을 사용한다.</p>
</li>
<li><p>@ManyToOne, @OneToOne</p>
<ul>
<li>(optional = false) : 내부조인<ul>
<li>(optional = true) : 외부조인</li>
</ul>
</li>
</ul>
</li>
<li><p>@OneToMany, @ManyToMany</p>
<ul>
<li>(optional = false) : 외부조인<ul>
<li>(optional = true) : 외부조인</li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h1 id="영속성-전이-cascade">영속성 전이: CASCADE</h1>
<p>특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이(transitive persistence)기능을 사용하면 된다. JPA는 CASCADE 옵션으로 영속성 전이를 제공한다. 쉽게 말해 영속성 전이를 사용하면 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장할 수 있다.
<strong>JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.</strong>
cascade = CascadeType.PERSIST 옵션을 설정하면 간편하게 부모와 자식 엔티티를 한 번에 영속화할 수 있다.</p>
<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;Child&gt;();
    ...
}

// CASCADE 저장 코드
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(child2);

    // 부모 저장, 연관된 자식들 저장
    em.persist(parent);
}</code></pre>
<p>영속성 전이는 연관관계를 매핑하는 것과는 아무 관련이 없다. 단지 엔티티를 영속화할 때 연관된 엔티티도 같이 영속화하는 편리함을 제공할 뿐이다. 그러므로 위 코드에서 양방향 영관관계를 추가한 다음 영속 상태로 만든 것을 확인할 수 있다.</p>
<h2 id="영속성-전이--삭제">영속성 전이 : 삭제</h2>
<p>방금 저장한 부모 엔티티와 관련된 자식 엔티티도 삭제하려면 하나하나 엔티티를 삭제해줘야 한다. 이 때 CascadeType.REMOVE를 사용하면 엔티티를 삭제할 때도 영속성 전이를 사용할 수 있다. 이를 설정하면 부모 엔티티만 삭제하면 연관된 자식 엔티티도 함께 삭제 된다.</p>
<pre><code class="language-java">Parent findParent = em.find(Parent.class, 1L);
em.remove(findParent);</code></pre>
<p>위 코드를 실해하면 DELETE SQL이 3번 실행하고 부모는 물론 연관된 자식도 모두 삭제된다. 삭제 순서는 외래 키 제약조건을 고려해서 자식을 먼저 삭제하고 부모를 삭제한다.
이 때, CascadeType.REMOVE를 설정하지 않고 코드를 실행하면 부모 엔티티만 삭제된다. 하지만 데이터베이스의 부모 로우를 삭제하는 순간 자식 테이블에 걸려 있는 외래 키 제약조건으로 인해, 데이터베이스에서 외래키무결성 예외가 발생한다.</p>
<hr>
<h1 id="고아-객체orphan">고아 객체(ORPHAN)</h1>
<p>JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데 이것을 <strong>고아 객체(ORPHAN)</strong> 제거라 한다.</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;Child&gt;();
    //
}</code></pre>
<p>orphanRemoval 속성을 true로 설정하여 고아객체 제거를 사용해 보자.</p>
<pre><code class="language-java">Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0);    //자식 엔티티를 컬렉션에서 제거</code></pre>
<p>위 코드를 실행하면 다음과 같은 SQL이 실행된다.</p>
<pre><code class="language-sql">DELETE FROM CHILD WHERE ID=?</code></pre>
<p>컬렉션에서 첫 번째 자식 엔티티를 제거하면 데이터베이스에도 데이터가 삭제된다. 고아 객체 제거 기능은 <strong>영속성 컨텍스트를 플러시할 때 적용</strong>되므로 플러시 시점에 DELETE SQL이 실행된다.
고아 객체 제거는 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다. 그러므로 이 기능은 참조하는 곳이 하나일 때만 사용해야 한다.
또 고아 객체 제거에는 기능이 하나 더 있는데 부모 객체를 제거하면 개념적으로 자식 객체는 고아가 된다. 따라서 부모를 제거하면 자식도 같이 제거된다. 이는 CascadeType.REMOVE를 설정하는 것과 같다.</p>
<hr>
<h1 id="영속성-전이--고아-객체-생명주기">(영속성 전이 + 고아 객체) 생명주기</h1>
<p>CascadeType.ALL + orphanRemoval = true를 동시에 설정하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.
즉, 자식을 저장하려면 부모에 등록만 하면된다.(CASCADE)</p>
<pre><code class="language-java">Parent parent = em.find(Parent.class, id);
parent.addChild(child1);    </code></pre>
<p>자식을 삭제하려면 부모에서 제거하면 된다. (orphanRemoval)</p>
<pre><code class="language-java">Parent parent = em.find(Parent.class, id);
parent.getChildren().remove(removeObject);    </code></pre>
<p>참조 : [자바 ORM 표준 JPA 프로그래밍]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[7장 고급 매핑]]></title>
            <link>https://velog.io/@now_here/7%EC%9E%A5-%EA%B3%A0%EA%B8%89-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@now_here/7%EC%9E%A5-%EA%B3%A0%EA%B8%89-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Sun, 08 Dec 2024 12:31:35 GMT</pubDate>
            <description><![CDATA[<h1 id="상속-관계-매핑">상속 관계 매핑</h1>
<p>관계형 데이터베이스에는 객체지향 언어에서 다루는 상속이라는 개념이 없다. 대신에 <strong>슈퍼타입 서브타입 관계(Super-Type Sub-Type Relationship)</strong> 기법이 객체의 상속 개념과 가장 유사하다. ORM에서 <strong>상속관계 매핑</strong>은 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것을 말한다.</p>
<p><img src="https://velog.velcdn.com/images/now_here/post/be40b5e4-83c3-4ac4-b25f-d24786a4ca52/image.png" alt=""></p>
<blockquote>
<p>슈퍼타입 서브타입 논리 모델</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/now_here/post/0ee8a78c-9a36-41b4-9dd0-2acc9c703017/image.png" alt=""></p>
<blockquote>
<p>객체 상속 모델</p>
</blockquote>
<h2 id="조인-전략-joined-strategy">조인 전략 Joined Strategy</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/948c2c46-cbf7-42bd-b6c8-6b85ea5b387e/image.png" alt=""></p>
<p>엔티티 각각을 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략이다.
객체는 타입을 구분할 수 있지만 테이블은 타입의 개념이 없기 때문에, 타입을 구분하는 컬럼을 추가해야한다. 여기서는 DTYPE 컬럼을 구분 컬럼으로 사용한다.</p>
<pre><code class="language-java">@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@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 {

    private String artist;
    //
}

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

    private String director;    // 감독
    private String actor;       // 배우
    //
}</code></pre>
<ul>
<li>@Inheritance(strategy = InheritanceType.JOINED)
: 상속매핑은 부모 클래스에 해당 어노테이션을 사용해야 한다. strategy 속성으로 매핑 전략을 지정해야 하는데 여기서는 JOINED를 사용했다.</li>
<li>@DiscriminatorColumn(name = &quot;DTYPE&quot;)
: 부모 클래스에 구분 컬럼을 지정한다. 이 컬럼으로 저장된 자식 테이블을 구분할 수 있다. 기본값이 DTYPE이므로 @DiscriminatorColumn으로 줄여서 사용해도 된다.</li>
<li>@DiscriminatorValue(&quot;A&quot;)
: 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다. 만약 영화 엔티티를 저장하면 구분 컬럼인 DTYPE에 M이 저장된다.</li>
</ul>
<p>위에 Album 과 Movie는 id를 따로 설정하지 않았다. 이런 경우 자식 테이블은 부모 테이블의 ID 컬럼명을 기본값으로 사용하는데 아래 처럼 ID를 재정의할 수도 있다.</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;    // ISBN
    //
}</code></pre>
<ul>
<li>@PrimaryKeyJoinColumn
: 자식 테이블의 기본 키 컬럼명을 변경하고 싶을 때 사용. BOOK 테이블의 ITEM_ID 기본 키 컬럼명을 BOOK_ID로 변경했다.</li>
</ul>
<h3 id="조인전략의-장단점-및-특징">조인전략의 장/단점 및 특징</h3>
<ul>
<li>장점</li>
</ul>
<ol>
<li>테이블이 정규화 된다.</li>
<li>외래 키 참조 무결성 제약조건을 활용할 수 있다.</li>
<li>저장공간을 효율적으로 사용한다.</li>
</ol>
<ul>
<li>단점</li>
</ul>
<ol>
<li>조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.</li>
<li>조회 쿼리가 복잡하다.</li>
<li>데이터를 등록할 때 INSERT SQL을 두 번 실행한다.</li>
</ol>
<ul>
<li>특징
JPA 표준 명세는 구분 컬럼을 사용하도록 하지만 하이버네이트를 포함한 몇몇 구현체는 구분 컬럼(@DiscriminatorColun) 없이도 동작한다.</li>
</ul>
<h2 id="단일-테이블-전략-single-table-strategy">단일 테이블 전략 Single-Table Strategy</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/4623921e-8d4f-4508-8594-c8b26496babe/image.png" alt="">
이름 그대로 하나의 테이블만을 사용하며 구분컬럼(DTYPE)으로 어떤 자식 데이터가 저장되었는지 구분한다. 조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다. </p>
<h3 id="주의점">주의점</h3>
<p>이 전략을 사용할 때 자식 엔티티가 매핑한 컬럼을 모두 null을 허용해야 한다. Book 엔티티를 저장하면 ITEM 테이블의 AUTHOR, ISBN만 사용하고 다른 엔티티와 매핑된 컬럼은 사용하지 않으므로 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 {

    private String artist;
    //
}

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

    private String director;    // 감독
    private String actor;       // 배우
    //
}

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

    private String author;  // 작가
    private String isbn;    // ISBN
    //
}</code></pre>
<p>@Inheritance(strategy = InheritanceType.SINGLE_TABLE) 을 사용하며 테이블 하나에 모든 것을 통합하므로 구분 컬럼을 필수로 사용해야 한다.</p>
<h3 id="단일-테이블-전략-장단점-및-특징">단일 테이블 전략 장/단점 및 특징</h3>
<ul>
<li>장점 </li>
</ul>
<ol>
<li>조인이 필요 없으므로 일반적으로 조회 성능이 빠르다.</li>
<li>조회 쿼리가 단순하다.</li>
</ol>
<ul>
<li>단점</li>
</ul>
<ol>
<li>자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.</li>
<li>단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있어 상황에 따라 조회 성능이 오히려 느려질 수 있다.</li>
</ol>
<ul>
<li>특징</li>
</ul>
<ol>
<li>구분 컬럼을 꼭 사용해야 한다. (@DiscriminatorColumn)</li>
<li>@DiscriminatorValue를 지정하지 않으면 기본으로 엔티티 이름을 사용한다. (ex. Movie, Album, Book)</li>
</ol>
<h2 id="구현-클래스마다-테이블-전략-table-per-concrete-class-strategy">구현 클래스마다 테이블 전략 Table-per-Concrete-Class Strategy</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/e9d5a56f-63e3-4159-bd86-149fd2857100/image.png" alt="">
자식 엔티티마다 테이블을 만들고, 자식 테이블 각각에 필요한 컬럼이 모두 있다. 일반적으로 추천하지 않는 전략이다.
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
을 사용한다.</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>
<h3 id="구현-클래스마다-테이블-전략-장단점-및-특징">구현 클래스마다 테이블 전략 장/단점 및 특징</h3>
<ul>
<li>장점</li>
</ul>
<ol>
<li>서브 타입을 구분해서 처리할 때 효과적이다.</li>
<li>not null 제약조건을 사용할 수 있다.</li>
</ol>
<ul>
<li>단점</li>
</ul>
<ol>
<li>여러 자식 테이블을 함께 조회할 때 성능이 느리다. (SQL에 UNION을 사용해야 함)</li>
<li>자식 테이블을 통합해서 쿼리하기 어렵다.</li>
</ol>
<ul>
<li>특징
구분 컬럼을 사용하지 않는다.</li>
</ul>
<hr>
<h1 id="mappedsuperclass">@MappedSuperclass</h1>
<p>부모 클래스는 테이블과 매핑하지 않고 상속받는자식 클래스만 테이블과 매핑하고 싶을 때 @MappedSuperclass를 사용한다.
<img src="https://velog.velcdn.com/images/now_here/post/74f5c04d-449e-4735-8e76-46f74d8ebf03/image.png" alt=""></p>
<blockquote>
<p>테이블</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/now_here/post/00ec4ba8-8578-4576-90f5-613b2ea273c3/image.png" alt=""></p>
<blockquote>
<p>객체</p>
</blockquote>
<p>공통되는 id, name을 부모 클래스로 모으고 객체 상속 관계로 만들어보자.</p>
<pre><code class="language-java">@MappedSuperclass
public abstract class BaseEntity {

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

@Entity
public class Member extends BaseEntity {

    //ID 상속
    //NANE 상속
    private String email;
    //
}

@Entity
public class Seller extends BaseEntity {
    //ID 상속
    //NANE 상속
    private String shopName;
    //
}</code></pre>
<p>부모로부터 물려받은 매핑 정보를 재정의하려면 아래와 같이 @AttributeOverride와 @AttributeOverrides를 사용하면 된다.</p>
<pre><code class="language-java">@Entity
@AttributeOverride(name = &quot;id&quot;, column = @Column(name = &quot;MEMBER_ID&quot;))
public class Member extends BaseEntity {
    ...
}

@Entity
@AttributeOverrides({
    @AttributeOverride(name = &quot;id&quot;, column = @Column(name = &quot;SELLER_ID&quot;)),
    @AttributeOverride(name = &quot;name&quot;, column = @Column(name = &quot;SELLER_NAME&quot;))
})
public class Seller extends BaseEntity {
    ...
}</code></pre>
<h3 id="mappedsuperclass-특징">@MappedSuperclass 특징</h3>
<ul>
<li>테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용한다.</li>
<li>@MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 JPQL이나 em.find()를 사용할 수 없다.</li>
<li>이 클래스는 직접 생성해서 사용할 일이 거의 없으므로 추상 클래스로 만드는 것을 권장한다.</li>
</ul>
<p>*@Entity는 @Entity 클래스나 @MappedSuperclass만을 상속받을 수 있다.</p>
<hr>
<h1 id="73-복합-키와-식별-관계-매핑">7.3 복합 키와 식별 관계 매핑</h1>
<h2 id="식별관계-vs-비식별관계">식별관계 vs 비식별관계</h2>
<h3 id="식별관계-identifying-relationship">식별관계 Identifying Relationship</h3>
<p>자식 클래스에서 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키의 형태로 사용하는 관계이다. 이 때, 자식 클래스는 부모로부터 받은 기본 키와 자식 테이블의 고유식별자를 가지고 복합 키를 형성한다.</p>
<hr>
<h3 id="식별-관계의-구성"><strong>식별 관계의 구성</strong></h3>
<ol>
<li><strong>부모 테이블의 기본 키</strong>가 자식 테이블의 기본 키로 포함.</li>
<li>자식 테이블의 기본 키는:<ul>
<li><strong>부모 테이블의 기본 키</strong> + <strong>자식 테이블의 고유 식별자</strong>로 구성된 <strong>복합 키</strong></li>
</ul>
</li>
<li>부모 테이블의 기본 키는 자식 테이블에서 <strong>외래 키 역할</strong>도 한다.</li>
</ol>
<hr>
<h3 id="예제"><strong>예제</strong></h3>
<h4 id="1-parent-테이블"><strong>1. Parent 테이블</strong></h4>
<pre><code class="language-sql">CREATE TABLE Parent (
    parent_id BIGINT PRIMARY KEY, -- 부모 테이블의 기본 키
    name VARCHAR(255)
);</code></pre>
<h4 id="2-child-테이블"><strong>2. Child 테이블</strong></h4>
<pre><code class="language-sql">CREATE TABLE Child (
    parent_id BIGINT, -- 부모 테이블의 기본 키를 외래 키로 사용
    child_id BIGINT,  -- 자식 테이블의 고유 식별자
    PRIMARY KEY (parent_id, child_id), -- 복합 키
    CONSTRAINT fk_parent FOREIGN KEY (parent_id) REFERENCES Parent(parent_id) -- 외래 키
);</code></pre>
<hr>
<h3 id="자식-테이블의-특징"><strong>자식 테이블의 특징</strong></h3>
<ol>
<li><p><strong>복합 키 구성</strong>:</p>
<ul>
<li><code>parent_id</code> + <code>child_id</code>는 자식 테이블의 기본 키가 됩니다.</li>
<li>이 복합 키를 통해 자식 테이블에서 각 행이 고유하게 식별됩니다.</li>
</ul>
</li>
<li><p><strong>외래 키 역할</strong>:</p>
<ul>
<li><code>parent_id</code>는 부모 테이블의 기본 키를 참조하므로 외래 키 역할을 합니다.</li>
<li>부모 테이블의 행이 삭제되면 자식 테이블의 관련 행도 삭제되도록 <strong>ON DELETE CASCADE</strong>를 설정할 수 있습니다.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="비식별-관계-non-identifying-relationship">비식별 관계 Non-Identifying Relationship</h3>
<p>자식 테이블에서 부모 테이블의 기본 키를 외래 키로만 사용하는 관계를 말한다. 이 경우, 자식 테이블의 기본 키는 자식 테이블의 고유식별자만을 이용한다.</p>
<h3 id="예제-1">예제</h3>
<h4 id="parent-테이블">Parent 테이블</h4>
<pre><code class="language-sql">CREATE TABLE Parent (
    parent_id BIGINT PRIMARY KEY, -- 부모 테이블의 기본 키
    name VARCHAR(255)
);</code></pre>
<h4 id="child-테이블">Child 테이블</h4>
<pre><code class="language-sql">CREATE TABLE Child (
    child_id BIGINT PRIMARY KEY, -- 자식 테이블의 고유 식별자
    parent_id BIGINT,            -- 부모 테이블의 기본 키를 외래 키로 사용
    description VARCHAR(255),
    CONSTRAINT fk_parent FOREIGN KEY (parent_id) REFERENCES Parent(parent_id) -- 외래 키 제약 조건
);</code></pre>
<p><img src="https://velog.velcdn.com/images/now_here/post/588f095f-2a47-417c-b0d4-d5ea4391ae05/image.png" alt=""></p>
<h4 id="필수적-비식별관계mandatory">필수적 비식별관계(Mandatory)</h4>
<p>외래 키에 NULL을 허용하지 않아 필수적으로 연관관계를 맺어야 한다.</p>
<h4 id="선택적-비식별관계optional">선택적 비식별관계(Optional)</h4>
<p>외래 키에 NULL을 헝요하여 연관관계를 맺을지 말지 선택할 수 있다.</p>
<p>데이터베이스 테이블을 설계할 때 식별관계나 비식별 관계 중 하나를 선택해야 한다. 최근에는 비식별 관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용하는 추세이다. JPA는 식별관계/비식별관계 모두 지원한다.</p>
<hr>
<h2 id="복합-키--비식별-관계-매핑">복합 키 : 비식별 관계 매핑</h2>
<p>JPA는 영속성 컨텍스트에 엔티티를 저장할 때 식별자를 키로 이용한다. 그리고 식별자를 구분하기 위해 eqauls와 hashCode를 사용해서 동등성 비교를 한다. 식별자가 하나라면 자바의 기본 타입을 사용하므로 문제가 없지만 식별자가 두 개 이상이면 별도의 식별자 클래스를 만들고 그곳에 equals와 hashCode를 구현해야 한다.
JPA는 복합 키를 지원하기 위해 관계형 데이터베이스에 가까운 방법으로 <strong>@IdClass</strong>와 좀 더 객체지향에 가까운 방법으로 <strong>@EmbeddedId</strong>를 제공한다.</p>
<h3 id="idclass">@IdClass</h3>
<p><img src="https://velog.velcdn.com/images/now_here/post/a7719c82-73c2-41ba-bec3-4093bce25fd2/image.png" alt=""></p>
<blockquote>
<p>복합 키 테이블</p>
</blockquote>
<pre><code class="language-java">//부모 클래스
@Entity
@IdClass(ParentId.class)
public class Parent {

    @Id
    @Column(name = &quot;PARENT_ID1&quot;)
    private String id1; // ParentId.id1과 연결

    @Id
    @Column(name = &quot;PARENT_ID2&quot;)
    private String id2; // ParentId.id2와 연결

    private String name;
    ...
}
</code></pre>
<pre><code class="language-java">// 식별자 클래스
public class ParentId implements Serializable {

    private String id1; //ParentId.id1과 연결
    private String id2; //ParentId.id2와 연결

    public ParentId() {}

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

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

    @Override
    public int hashCode() {...}
}</code></pre>
<h4 id="idclass-식별자-클래스-조건">@IdClass 식별자 클래스 조건</h4>
<ul>
<li>식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다.</li>
<li>Serializable 인터페이스를 구현해야 한다.</li>
<li>equals, hashCode를 구현해야 한다. (보통 모든 필드를 사용하여 구현)</li>
<li>기본 생성자가 있어야 한다.</li>
<li>식별자 클래스는 public이어야 한다.</li>
</ul>
<h4 id="idclass-식별자-클래스-저장-코드">@IdClass 식별자 클래스 저장 코드</h4>
<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>따로 식별자 클래스 parentId를 생성하지 않았는데 em.persist()를 호출하면 영속성컨텍스트에 엔티티를 등록하기 직전에 내부에서 Parent.id1, Parent.id2 값을 사용해서 식별자 클래스인 ParentId를 생성하고 영속성 컨텍스트의 키로 사용한다.</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>자식 클래스는 위와 같으며 referencedColumnName의 속성의 값이 @JoinColumn의 name값과 같다면 생략해도 된다.</p>
<hr>
<h3 id="embeddedid">@EmbeddedId</h3>
<pre><code class="language-java">@Entity
public class Parent {

    @EmbeddedId
    private ParentId id;

    private String name;
    ...
}</code></pre>
<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;

    public ParentId() {}

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

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

    @Override
    public int hashCode() {...}
}</code></pre>
<p>위 처럼 식별자 클래스 자체를 속성 타입으로 지정하여 좀 더 객체지향적인 방법이다.</p>
<h4 id="embeddid를-적용한-식별자-클래스의-조건">@EmbeddId를 적용한 식별자 클래스의 조건</h4>
<ul>
<li>@Embeddable 을 사용해야 한다.</li>
<li>Serializable 인터페이스를 구현해야 한다.</li>
<li>equals, hashCode를 구현해야 한다. (보통 모든 필드를 사용하여 구현)</li>
<li>기본 생성자가 있어야 한다.</li>
<li>식별자 클래스는 public이어야 한다.</li>
</ul>
<h4 id="embeddid를-적용한-식별자-클래스-저장-코드">@EmbeddId를 적용한 식별자 클래스 저장 코드</h4>
<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>
<hr>
<h2 id="복합-키--식별-관계-매핑">복합 키 : 식별 관계 매핑</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/aaea446f-5586-4c3b-89d7-bc9f350b384c/image.png" alt=""></p>
<h3 id="idclass와-식별-관계-매핑">@IdClass와 식별 관계 매핑</h3>
<pre><code class="language-java">@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;
    ...
}

public class GrandChildId implements Serializable {

    private ChildId child;  // GrandChild.child 매핑
    private String id;      // GrandChild.id 매핑

    //
}</code></pre>
<p>위의 식별관계를 일부만 표현하였다. 식별관계는 위처럼 자식 테이블이 부모 테이블의 기본 키를 <strong>기본 키 + 외래 키</strong>로 사용하기 때문에 @Id로 기본 키를 매핑하면서 @ManyToOne와 @JoinColumn으로 외래키를 같이 매핑한다.</p>
<h3 id="embeddedid와-식별-관계">@EmbeddedId와 식별 관계</h3>
<pre><code class="language-java">@Entity
public class GrandChild {

    @EmbeddedId
    private GrandChildId 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;
    ...
}

@Embeddable
public class GrandChildId implements Serializable {

    private ChildId childId;  // @MapsId(&quot;childId&quot;) 매핑

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

    //
}</code></pre>
<p>@EmbeddedId를 사용할 때는 연관관계의 속성에 <strong>@MapsId</strong>를 사용하면 된다. 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻이다. @MapsId의 속성 값은 @EmbeddedId를 사용한 식별자 클래스의 기본 키 필드를 지정하면 된다.</p>
<hr>
<h2 id="일대일-식별-관계">일대일 식별 관계</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/274fa453-2c4a-4507-a687-c4fa009875ca/image.png" alt="">
일대일 식별 관계는 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키값만 사용한다.</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 boardId;

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

    private String content;
    //
}</code></pre>
<p>BoardDetail처럼 식별자가 단순히 컬럼 하나면 @MapsId를 사용하고 속성 값은 비워두면 된다. 이 때, @MapsId는 @Id를 사용해서 식별자로 지정한 BoardDetail.boardId와 매핑된다.</p>
<hr>
<h2 id="식별-비식별-관계의-장단점">식별, 비식별 관계의 장단점</h2>
<p>식별관계보다 비식별관계를 선호하는데 이유는 아래와 같다.</p>
<ul>
<li>식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식테이블의 기본 키 컬럼이 점점 늘어난다. 이는 조인할 때 SQL을 복작합게 하고 기본 키 인덱스가 불필요하게 커질 수 있다.</li>
<li>식별 관계는 2개 이상의 컬럼을 합해서 복합 키를 만들어야 하는 경우가 많다.</li>
<li>식별 관계는 기본 키로 비즈니스적 의미가 있는 자연 키 컬럼을 주로 사용하고 비식별 관계는 대리 키를 주로 사용한다. 비즈니스적인 자연 키를 사용하는 식별관계의 경우 자연 키가 변경되었을 때 자식 테이블까지 전파되면 변경이 힘들다.</li>
<li>식별관계는 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용하므로 테이블 구조가 유연하지 못 하다.</li>
</ul>
<p>식별 관계의 장점도 있다. 기본 키 인덱스를 활용하기 좋고, 상위 테이블들의 기본 키 컬럼을 자식, 손자 테이블들이 가지고 있으므로 특정 상황에서 조인 없이 하위 테이블만으로 검색을 완료할 수 있다.</p>
<p>되도록이면 <strong>비식별 관계를 사용하고 기본 키는 Long 타입의 대리 키를 권장한다.</strong> 선택적인 비식별관계보다 필수 비식별관계를 권장한다. 선택적인 비식별관계는 NULL을 허용하므로 조인할 때 외부 조인(outer join)을 사용해야 하고, 필수 비식별 관계는 NULL을 허용하지 않기 때문에 항상 관계가 있다는 것을 보장함으로 조인할 때 내부 조인(inner join)만 사용해도 되기 때문이다.</p>
<hr>
<h1 id="조인-테이블">조인 테이블</h1>
<p>데이터베이스 테이블의 연관관계를 설계하는 방법은 크게 두 가지이다.</p>
<ul>
<li>조인 컬럼 사용 (외래 키)</li>
<li>조인 테이블 사용 (테이블 사용)</li>
</ul>
<p>조인 컬름을 사용하는 방식은 단순히 외래 키 컬럼만 추가해서 연관관계를 맺지만 조인 테이블을 사용하는 방법은 연관 관계를 관리하는 조인 테이블을 추가하고, 이 테이블에서 두 테이블의 외래 키를 가지고 연관관계를 관리한다. 따라서 두 테이블은 연관관계를 관리하기 위한 외래 키 컬럼이 없다.</p>
<p>조인 테이블의 단점은 아무래도 테이블이 늘어나서 관리해야 할 테이블이 늘어난다는 것이다. 또한 두 테이블을 조인하려면 조인 테이블까지 추가로 조인해야 한다는 단점이 있다. 그렇기 때문에 기본적으로 조인 컬럼을 사용하고 필요하다고 판단될 때만 조인 테이블을 사용하자.</p>
<h2 id="일대일-조인-테이블">일대일 조인 테이블</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/e7a9a3e2-9516-406d-a23d-0015c897ee4c/image.png" alt="">
일대일 관계를 만드려면 조인 테이블의 외래 키 컬럼 각각 총 2개의 유니크 제약조건을 걸어야 한다.
위에 PARENT_CHILD 테이블을 보면 PARENT_ID를 PK로 가지고 (PK의 특성상 UK 조건이 걸림) CHILD_ID에 UK 제약조건이 걸려있는 것을 확인할 수 있다.</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;

    // 양방향 매핑
    @OneToOne(mappedBy = &quot;child&quot;)
    private Parent parent;

    //
}</code></pre>
<p>조인 테이블을 사용하기 위해 <strong>@JoinTable</strong>을 사용하였고 <strong>joinColumns</strong> 를 이용하여 현재 엔티티를 참조하는 외래 키를 표현하고 <strong>inverseJoinColumns</strong>를 이용하여 반대방향 엔티티를 참조하는 외래 키를 지정하였다.</p>
<h2 id="일대다-조인-테이블">일대다 조인 테이블</h2>
<p>일대다 관계를 만드려면 조인 테이블의 컬럼 중 다와 관련된 컬럼인 CHILD_ID에 유니크 제약조건을 걸어야 한다.
(PK이므로 UK 제약조건이 걸려있음)
<img src="https://velog.velcdn.com/images/now_here/post/5b8390f9-42da-41ca-929e-f039111ed4a2/image.png" alt=""></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;&gt;();
    ...
}

@Entity
public class Child {

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

    //
}</code></pre>
<h2 id="다대일-조인-테이블">다대일 조인 테이블</h2>
<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;),
            inverseJoinColumns = @JoinColumn(name = &quot;PARENT_ID&quot;)
    )
    private Parent parent;

    //
}
</code></pre>
<h2 id="다대다-조인-테이블">다대다 조인 테이블</h2>
<p>다대다 관계를 만들려면 조인 테이블의 두 컬럼을 합해서 <strong>하나의 복합 유니크 제약조건을 걸어야 한다.(PARENT_ID, CHILD_ID는 복합 기본키)</strong></p>
<hr>
<h1 id="엔티티-하나에-여러-테이블-매핑">엔티티 하나에 여러 테이블 매핑</h1>
<p>하나의 엔티티에 여러 테이블을 매핑하는 방법으로 <strong>@SecondaryTable</strong>을 사용할 수 있다.
두 테이블을 하나의 엔티티로 매핑하는 것보다 테이블당 엔티티를 각각 만들어서 매핑하는 것을 권장하므로 이런 것이 있구나 정도로 하고 넘어가자.</p>
<hr>
<h1 id="부록">부록</h1>
<h2 id="1-uk가-각각-제약-조건이라면">1. uk가 각각 제약 조건이라면?</h2>
<blockquote>
<p>근데 uk 가 여러 개 인데 조합 말고 단 하나의 값만 들어오게 하고 싶다면?</p>
</blockquote>
<p>만약 <strong>Unique Key(UK)</strong>를 여러 개 지정했을 때, 각 컬럼마다 <strong>독립적으로 고유성을 보장</strong>하고 싶다면, 다음과 같은 방식으로 구현할 수 있습니다.</p>
<hr>
<h3 id="방법-1-각각의-컬럼에-columnunique--true-사용"><strong>방법 1: 각각의 컬럼에 <code>@Column(unique = true)</code> 사용</strong></h3>
<ul>
<li>각 컬럼을 독립적으로 고유하게 설정하려면, JPA에서 <code>@Column</code> 어노테이션의 <code>unique = true</code> 속성을 사용합니다.</li>
</ul>
<p><code>@Column(unique = true)</code>를 사용하면, <strong>각 컬럼이 개별적으로 고유해야 하며</strong>, 중복된 값을 허용하지 않습니다.<br>즉, <strong><code>uk1</code>과 <code>uk2</code> 각각이 독립적으로 유일한 값을 가져야</strong> 합니다.</p>
<h4 id="올바른-예제"><strong>올바른 예제</strong></h4>
<pre><code class="language-java">@Entity
public class ExampleEntity {

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

    @Column(unique = true) // uk1 컬럼에 대해 고유 제약 조건
    private String uk1;

    @Column(unique = true) // uk2 컬럼에 대해 고유 제약 조건
    private String uk2;

    // Getter, Setter
}</code></pre>
<h4 id="데이터-삽입-예제"><strong>데이터 삽입 예제</strong></h4>
<table>
<thead>
<tr>
<th>uk1</th>
<th>uk2</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>A</td>
<td>B</td>
<td>삽입 성공</td>
</tr>
<tr>
<td>A</td>
<td>C</td>
<td>삽입 실패 (uk1 중복)</td>
</tr>
<tr>
<td>D</td>
<td>B</td>
<td>삽입 실패 (uk2 중복)</td>
</tr>
<tr>
<td>D</td>
<td>E</td>
<td>삽입 성공</td>
</tr>
</tbody></table>
<h3 id="결론"><strong>결론</strong></h3>
<p><code>@Column(unique = true)</code>는 각 컬럼에 독립적으로 <strong>고유 제약 조건을 설정</strong>합니다.  </p>
<ul>
<li><strong>중복된 값은 허용되지 않으며</strong>, 각 컬럼의 값이 고유해야 합니다.</li>
<li>예를 들어, <code>uk2 = &quot;B&quot;</code>가 이미 존재하면 다시 <code>uk2 = &quot;B&quot;</code>를 삽입하려고 할 때 <strong>제약 조건 위반 오류</strong>가 발생합니다.</li>
</ul>
<hr>
<h3 id="방법-2-테이블-제약-조건으로-설정"><strong>방법 2: 테이블 제약 조건으로 설정</strong></h3>
<p>JPA에서는 <code>@Table</code>의 <code>@UniqueConstraint</code>를 통해 <strong>컬럼별 고유성</strong>을 정의할 수 있습니다.</p>
<h4 id="예제-2"><strong>예제</strong></h4>
<pre><code class="language-java">@Entity
@Table(
    uniqueConstraints = {
        @UniqueConstraint(columnNames = &quot;uk1&quot;),
        @UniqueConstraint(columnNames = &quot;uk2&quot;)
    }
)
public class ExampleEntity {

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

    private String uk1; // uk1에 대한 고유 제약 조건 설정
    private String uk2; // uk2에 대한 고유 제약 조건 설정

    // Getter, Setter
}</code></pre>
<ul>
<li><strong>결과</strong>:<ul>
<li><code>uk1</code>과 <code>uk2</code>에 대해 각각 <strong>독립적인 UNIQUE 제약 조건</strong>이 설정됩니다.</li>
<li>동일한 컬럼값을 삽입하려고 하면 <strong>제약 조건 위반 오류</strong>가 발생합니다.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="방법-3-데이터베이스에서-직접-제약-설정"><strong>방법 3: 데이터베이스에서 직접 제약 설정</strong></h3>
<p>데이터베이스 레벨에서 UNIQUE 제약 조건을 설정할 수도 있습니다.</p>
<h4 id="sql-예제"><strong>SQL 예제</strong></h4>
<pre><code class="language-sql">CREATE TABLE ExampleEntity (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    uk1 VARCHAR(255) UNIQUE, -- uk1 고유 제약
    uk2 VARCHAR(255) UNIQUE  -- uk2 고유 제약
);</code></pre>
<hr>
<h3 id="결론-1"><strong>결론</strong></h3>
<ul>
<li><p>각 컬럼에 대해 <strong>단일 컬럼의 고유성</strong>을 원한다면:</p>
<ol>
<li>JPA에서 <code>@Column(unique = true)</code>를 사용하거나,</li>
<li><code>@Table</code>의 <code>@UniqueConstraint</code>로 각각 설정하거나,</li>
<li>데이터베이스에서 직접 UNIQUE 제약 조건을 추가하세요.</li>
</ol>
</li>
<li><p><strong>조합이 아닌 독립적으로 고유성을 보장</strong>할 때는, 각 컬럼별로 고유 제약 조건을 지정하면 됩니다.</p>
</li>
</ul>
<hr>
<h3 id="구체적-예시">구체적 예시</h3>
<p>다음은 각 컬럼에 독립적인 고유 제약 조건이 필요한 구체적인 예시입니다.  </p>
<h3 id="1-이메일과-사용자명username-등록"><strong>1. 이메일과 사용자명(Username) 등록</strong></h3>
<p>사용자 정보를 관리하는 시스템에서 <strong>이메일</strong>과 <strong>사용자명</strong>은 각각 고유해야 합니다.  </p>
<ul>
<li><strong>이메일</strong>은 모든 사용자 간에 유일해야 하며,</li>
<li><strong>사용자명</strong> 또한 중복될 수 없습니다.</li>
</ul>
<h4 id="jpa-엔티티"><strong>JPA 엔티티</strong></h4>
<pre><code class="language-java">@Entity
public class User {

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

    @Column(unique = true) // 이메일 고유 제약 조건
    private String email;

    @Column(unique = true) // 사용자명 고유 제약 조건
    private String username;

    // Getter, Setter
}</code></pre>
<h4 id="삽입-예제"><strong>삽입 예제</strong></h4>
<table>
<thead>
<tr>
<th>email</th>
<th>username</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td><a href="mailto:user1@example.com">user1@example.com</a></td>
<td>user1</td>
<td>삽입 성공</td>
</tr>
<tr>
<td><a href="mailto:user1@example.com">user1@example.com</a></td>
<td>user2</td>
<td>삽입 실패 (이메일 중복)</td>
</tr>
<tr>
<td><a href="mailto:user2@example.com">user2@example.com</a></td>
<td>user1</td>
<td>삽입 실패 (사용자명 중복)</td>
</tr>
<tr>
<td><a href="mailto:user2@example.com">user2@example.com</a></td>
<td>user2</td>
<td>삽입 성공</td>
</tr>
</tbody></table>
<hr>
<h3 id="2-차량-관리-시스템에서-차량-번호와-vin-vehicle-identification-number"><strong>2. 차량 관리 시스템에서 차량 번호와 VIN (Vehicle Identification Number)</strong></h3>
<ul>
<li>차량 번호(Plate Number)는 동일 국가 내에서 고유해야 합니다.</li>
<li>차량의 고유 식별 번호(VIN)는 전 세계적으로 고유해야 합니다.</li>
</ul>
<h4 id="jpa-엔티티-1"><strong>JPA 엔티티</strong></h4>
<pre><code class="language-java">@Entity
public class Vehicle {

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

    @Column(unique = true) // 차량 번호 고유 제약 조건
    private String plateNumber;

    @Column(unique = true) // VIN 고유 제약 조건
    private String vin;

    // Getter, Setter
}</code></pre>
<h4 id="삽입-예제-1"><strong>삽입 예제</strong></h4>
<table>
<thead>
<tr>
<th>plateNumber</th>
<th>vin</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>ABC-1234</td>
<td>1HGCM82633A123456</td>
<td>삽입 성공</td>
</tr>
<tr>
<td>ABC-1234</td>
<td>2HGCM82633A654321</td>
<td>삽입 실패 (차량 번호 중복)</td>
</tr>
<tr>
<td>XYZ-5678</td>
<td>1HGCM82633A123456</td>
<td>삽입 실패 (VIN 중복)</td>
</tr>
<tr>
<td>XYZ-5678</td>
<td>3HGCM82633A987654</td>
<td>삽입 성공</td>
</tr>
</tbody></table>
<hr>
<h3 id="3-상품-관리-시스템에서-상품-코드와-시리얼-번호"><strong>3. 상품 관리 시스템에서 상품 코드와 시리얼 번호</strong></h3>
<ul>
<li><strong>상품 코드(Product Code)</strong>: 제품 모델을 구분하는 고유 코드.</li>
<li><strong>시리얼 번호(Serial Number)</strong>: 개별 제품의 유일성을 보장.</li>
</ul>
<h4 id="jpa-엔티티-2"><strong>JPA 엔티티</strong></h4>
<pre><code class="language-java">@Entity
public class Product {

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

    @Column(unique = true) // 상품 코드 고유 제약 조건
    private String productCode;

    @Column(unique = true) // 시리얼 번호 고유 제약 조건
    private String serialNumber;

    // Getter, Setter
}</code></pre>
<h4 id="삽입-예제-2"><strong>삽입 예제</strong></h4>
<table>
<thead>
<tr>
<th>productCode</th>
<th>serialNumber</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>PROD001</td>
<td>SN123456</td>
<td>삽입 성공</td>
</tr>
<tr>
<td>PROD001</td>
<td>SN654321</td>
<td>삽입 실패 (상품 코드 중복)</td>
</tr>
<tr>
<td>PROD002</td>
<td>SN123456</td>
<td>삽입 실패 (시리얼 번호 중복)</td>
</tr>
<tr>
<td>PROD002</td>
<td>SN987654</td>
<td>삽입 성공</td>
</tr>
</tbody></table>
<hr>
<h3 id="결론-2"><strong>결론</strong></h3>
<p>위의 예제는 <strong>각 컬럼에 대해 독립적으로 고유성을 보장해야 하는 상황</strong>을 보여줍니다.  </p>
<ul>
<li><code>@Column(unique = true)</code>를 사용하면 <strong>각각의 컬럼에 대한 유일성 제약 조건</strong>이 적용됩니다.</li>
<li>예를 들어, 이메일과 사용자명, 차량 번호와 VIN, 상품 코드와 시리얼 번호 등 <strong>각 컬럼이 독립적으로 고유해야 하는 경우</strong>에 적합합니다.</li>
</ul>
<hr>
<h2 id="2-식별자-클래스에는-왜-serializable-인터페이스를-구현해야-할까">2. 식별자 클래스에는 왜 Serializable 인터페이스를 구현해야 할까?</h2>
<p>JPA에서 식별자 클래스가 <strong><code>Serializable</code> 인터페이스를 구현해야 하는 이유</strong>는 <strong>복합 키(Composite Key)</strong>를 안전하게 직렬화하고, 여러 컨텍스트에서 일관성 있게 사용할 수 있도록 보장하기 위해서입니다.</p>
<hr>
<h3 id="1-직렬화serialization와-네트워크-전송"><strong>1. 직렬화(Serialization)와 네트워크 전송</strong></h3>
<ul>
<li>JPA는 식별자 객체를 <strong>1차 캐시</strong>, <strong>쿼리 결과 캐시</strong> 등에서 사용하며, 경우에 따라 이를 네트워크를 통해 전송하거나 파일에 저장해야 할 수도 있습니다.</li>
<li><strong><code>Serializable</code></strong> 인터페이스를 구현하면, 식별자 객체를 직렬화하여 바이트 스트림으로 변환할 수 있습니다.</li>
<li>이를 통해 데이터베이스와의 통신이나 데이터 전송 과정에서 객체를 안전하게 처리할 수 있습니다.</li>
</ul>
<hr>
<h3 id="2-캐싱-및-동등성-비교"><strong>2. 캐싱 및 동등성 비교</strong></h3>
<ul>
<li>JPA는 <strong>1차 캐시</strong>에서 엔티티를 관리하기 위해 식별자를 키(key)로 사용합니다.</li>
<li>식별자 클래스가 <strong><code>Serializable</code></strong>을 구현하면, JPA가 내부적으로 직렬화된 객체를 캐시에서 관리하기 쉬워지고, 다른 컨텍스트에서 일관되게 동작할 수 있습니다.</li>
</ul>
<hr>
<h3 id="3-식별자-비교-및-동등성-보장"><strong>3. 식별자 비교 및 동등성 보장</strong></h3>
<ul>
<li>식별자 클래스는 <strong><code>equals()</code></strong>와 <strong><code>hashCode()</code></strong> 메서드를 올바르게 구현해야 합니다.</li>
<li><strong><code>Serializable</code></strong>을 구현함으로써 JPA는 식별자를 직렬화하여 비교하거나, 특정 캐시나 컬렉션(Map, Set 등)에서 올바르게 사용할 수 있도록 보장합니다.</li>
</ul>
<hr>
<h3 id="4-jpa-스펙-요구-사항"><strong>4. JPA 스펙 요구 사항</strong></h3>
<ul>
<li>JPA 명세에서 복합 키를 위한 식별자 클래스는 반드시 <strong><code>Serializable</code></strong> 인터페이스를 구현하도록 요구합니다.</li>
<li>이는 JPA 구현체(Hibernate 등)가 식별자를 안정적으로 처리하기 위해 <strong>기본 요구 사항</strong>으로 설정한 규칙입니다.</li>
</ul>
<hr>
<h3 id="예제-식별자-클래스"><strong>예제: 식별자 클래스</strong></h3>
<pre><code class="language-java">import java.io.Serializable;
import java.util.Objects;

public class OrderId implements Serializable {

    private String orderNumber; // 식별자 필드
    private String productCode; // 식별자 필드

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

    public OrderId(String orderNumber, String productCode) {
        this.orderNumber = orderNumber;
        this.productCode = productCode;
    }

    // equals()와 hashCode() 구현
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        OrderId orderId = (OrderId) o;
        return Objects.equals(orderNumber, orderId.orderNumber) &amp;&amp;
               Objects.equals(productCode, orderId.productCode);
    }

    @Override
    public int hashCode() {
        return Objects.hash(orderNumber, productCode);
    }
}</code></pre>
<hr>
<h3 id="식별자-클래스-적용"><strong>식별자 클래스 적용</strong></h3>
<pre><code class="language-java">@Entity
@IdClass(OrderId.class)
public class Order {

    @Id
    private String orderNumber; // 복합 키 필드

    @Id
    private String productCode; // 복합 키 필드

    private int quantity;

    // Getter, Setter
}</code></pre>
<hr>
<h3 id="결론-3"><strong>결론</strong></h3>
<p><code>Serializable</code> 구현은 식별자 클래스가 JPA의 다양한 요구 사항(캐싱, 전송, 비교 등)을 충족하고, 안정적이고 일관되게 동작하도록 보장하는 핵심적인 역할을 합니다.<br>따라서 JPA 복합 키를 사용할 때 반드시 <strong><code>Serializable</code></strong>을 구현해야 합니다.</p>
<p>참조 : <strong>[자바 ORM 표준 JPA 프로그래밍]</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[6장 다양한 연관관계 매핑]]></title>
            <link>https://velog.io/@now_here/6%EC%9E%A5-%EB%8B%A4%EC%96%91%ED%95%9C-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@now_here/6%EC%9E%A5-%EB%8B%A4%EC%96%91%ED%95%9C-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Sun, 01 Dec 2024 13:16:26 GMT</pubDate>
            <description><![CDATA[<h2 id="다대일">다대일</h2>
<p>데이터베이스에서는 항상 다대일관계이면 다쪽에 외래키를 가지고 있어, 다쪽이 연관관계의 주인이다.</p>
<h3 id="다대일-단방향-n1">다대일 단방향 [N:1]</h3>
<h4 id="회원-엔티티">회원 엔티티</h4>
<pre><code class="language-java">@Entity
public class Member {

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

    @ManyToOne
    @JoinColumn(name = &quot;TEAM_ID&quot;)
    private Team team;

    //Getter, Setter</code></pre>
<h4 id="팀-엔티티">팀 엔티티</h4>
<pre><code class="language-java">@Entity
public class Team {

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

    private String name;

    //Getter, Setter</code></pre>
<p>회원은 Member.team으로 팀 엔티티를 참조할 수 있지만 팀은 회원을 참조하는 필드가 없다. 이런 관계를 회원과 팀 기준으로 <strong>다대일 단방향 연관관계</strong>라고 한다.</p>
<h3 id="다대일-양방향-n1-1n">다대일 양방향 [N:1, 1:N]</h3>
<h4 id="회원-엔티티-1">회원 엔티티</h4>
<pre><code class="language-java">@Entity
public class Member {

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

    private String username;
    @ManyToOne
    @JoinColumn(name = &quot;TEAM_ID&quot;)
    private Team team;

    //연관관계 설정

    public void setTeam(Team team) {
        this.team = team;
        //무한루프에 빠지지 않도록 체크
        if (!team.getMembers().contains(this)) {
            team.getMembers().add(this);
        }
    }</code></pre>
<h4 id="팀-엔티티-1">팀 엔티티</h4>
<pre><code class="language-java">@Entity
public class Team {

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

    private String name;

    @OneToMany(mappedBy = &quot;team&quot;)
    private List&lt;Member&gt; members = new ArrayList&lt;Member&gt;();

    public void addMember(Member member) {
        this.members.add(member);
        if (member.getTeam() != this) {    //무한루프에 빠지지 않도록 체크
            member.setTeam(this);
        }
    }

    ...
}</code></pre>
<p>양방향은 외래 키가 있는 쪽이 연관관계의 주인이다. 일대다와 다대일 관계에서는 항상 다쪽이 외래키를 가지고 있으므로 여기서는 Member.team이 연관관계의 주인이다. JPA는 외래 키를 관리할 때 연관관계의 주인만을 사용한다. 주인이 아닌 Team.members는 조회를 위한 JPQL이나 객체 그래프 탐색을 사용할 때 이용한다.</p>
<h4 id="양방향-연관관계는-항상-서로를-참조해야-한다">양방향 연관관계는 항상 서로를 참조해야 한다.</h4>
<p>어느 한 쪽만 참조하면 양방향 연관관계가 성립되지 않는다. 항상 서로 참조하기 위해서 편의 메서드를 사용하면 좋은데 이런 편의 메서드는 한쪽에만 작성하거나 양쪽에 작성할 수 있다. 양쪽에 작성할 경우 무한 루프에 빠지지 않도록 주의해야 하며, 여기서는 무한 루프에 빠지지 않도록 검사하는 로직을 추가하였다.</p>
<hr>
<h2 id="일대다">일대다</h2>
<p>일대다 관계는 다대일 관계의 반대이다. 엔티티를 하나 이상 참조할 수 있으므로 자바 컬렉션인 Collection, List, Set, Map 중에 하나를 사용해야 한다.</p>
<h3 id="일대다-단방향-1n">일대다 단방향 [1:N]</h3>
<p>예를 들어, 하나의 팀은 여러 명의 회원을 참조할 수 있는데 이를 1:N 관계라고 한다. 이 때, 팀은 회원들을 참조하지만 회원은 팀을 참조하지 않으면 단방향 관계이다.
(일대다 관계는 JPA 2.0부터 지원함)</p>
<p>일대다 단방향 관계는 특이한 구조를 가지는데 테이블에서는 항상 다쪽이 외래키를 관리하는데 객체에서는 일쪽에서만 참조를 하기 때문이다. 다시 설명하면 다 쪽인 Member 엔티티에는 외래 키를 매핑할 수 있는 참조 필드가 없고 반대쪽인 Team 엔티티에만 참조 필드인 members가 있어 팀 엔티티에서 반대편 테이블의 외래 키를 관리하는 특이한 모습이 나타난다.</p>
<pre><code class="language-java">//일대다 단방향 팀 엔티티
@Entity
public class Team {

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

    private String name;

    @OneToMany
    @JoinColumn(name = &quot;TEAM_ID&quot;)   //MEMBER 테이블의 TEAM_ID (FK)
    private List&lt;Member&gt; members = new ArrayList&lt;Member&gt;();
    //</code></pre>
<pre><code class="language-java">//일대다 단방향 회원 엔티티
@Entity
public class Member {

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

    private String username;
    //
}</code></pre>
<p>일대다 단방향 관계를 매핑할 때는 @JoinColumn을 명시해야 한다. 그렇지 않으면 JPA는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용해서 매핑한다. (자세한 사항은 7장에서)</p>
<h3 id="일대다-단방향-매핑의-단점">일대다 단방향 매핑의 단점</h3>
<p>매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 문제가 있다. 본인 테이블에 외래키가 있다면 엔티티의 저장과 연관관계 처리를 INSERT로 끝내면 될 것을 UPDATE를 추가로 진행해야 한다.</p>
<pre><code class="language-java">        Member member1 = new Member(&quot;member1&quot;);
        Member member2 = new Member(&quot;member2&quot;);

        Team team1 = new Team(&quot;team1&quot;);
        team1.getMembers().add(member1);
        team1.getMembers().add(member2);

        em.persist(member1);    //INSERT-member1
        em.persist(member2);    //INSERT-member2
        em.persist(team1);      //INSERT-team1,
                                //UPDATE-member1.fk, UPDATE-member2.fk

        transaction.commit();</code></pre>
<p>위를 실행하면 아래와 같은 SQL이 실행된다.</p>
<pre><code class="language-sql">insert into Member (MEMBER_ID, username) values (null, ?)
insert into Member (MEMBER_ID, username) values (null, ?)
insert into Team (TEAM_ID, name) values (null, ?)
update Member set TEAM_ID=? where MEMBER_ID=?
update Member set TEAM_ID=? where MEMBER_ID=?</code></pre>
<p>회원 엔티티는 팀 엔티티를 모르고 연관관계의 정보는 팀 엔티티의 members가 관리하기 때문에 회원 엔티티를 저장할 때, MEMBER 테이블의 TEAM_ID 외래 키에 아무 값도 저장되지 않는다. 팀 엔티티를 저장할 때에서야 Team.members의 참조 값을 확인해서 회원 테이블에 있는 TEAM_ID 외래 키를 업데이트한다.</p>
<p>성능상의 문제와 관리의 문제점으로 인해 일대다 단방향 매핑보다는 <strong>다대일 양방향 매핑을 권장</strong>한다.</p>
<h3 id="일대다-양방향-1n-n1">일대다 양방향 [1:N, N:1]</h3>
<p>연관관계의 주인이 1인 일대다 양방향은 존재하지 않는다. 연관관계의 주인이 다쪽인 다대일 양방향 매핑을 사용해야 한다. 관계형 데이터베이스의 특성상 일대다, 다대일 관계는 항상 다쪽에 외래키가 존재하기 때문에 연관관계의 주인이 일쪽에 있는 일대다 양방향 매핑은 존재하지 않는다.
대신에 일대다 단방향 매핑 반대편에 같은 외래 키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 하나 추가하여 일대다 양방향 매핑을 할 수 있다.(정확히 말하면 일대다 양방향 매핑처럼 보이게 하는 방법)</p>
<pre><code class="language-java">//일대다 양방향 팀 엔티티
@Entity
public class Team {

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

    private String name;

    @OneToMany
    @JoinColumn(name = &quot;TEAM_ID&quot;)   
    private List&lt;Member&gt; members = new ArrayList&lt;Member&gt;();
    //
}    </code></pre>
<pre><code class="language-java">//일대다 양방향 회원 엔티티
@Entity
public class Member {

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

    private String username;

    @ManyToOne
    @JoinColumn(name = &quot;TEAM_ID&quot;, insertable = false, updatable = false)
    private Team team;
    //
}    </code></pre>
<p>일대다 단방향 매핑 반대편에 다대일 단방향 매핑을 추가했다. (Member 엔티티에 @ManyToOne부분)
이때 일대다 단방향 매핑과 같은 TEAM_ID 외래 키 컬럼을 매핑했다. 이렇게 되면 둘 다 같은 키를 관리하므로 문제가 발생할 수 있어 다대일 쪽에 insertable = false, updatable = false를 설정하여 읽기만 가능하게 했다.
이 방법은 일대다 단방향 매핑이 가지는 단점을 그대로 가져가므로 될 수 있으면 다대일 양방향 매핑을 사용하자.</p>
<hr>
<h2 id="일대일-11">일대일 [1:1]</h2>
<p>일대일 관게는 주테이블이나 대상 테이블 중 누가 외래 키를 가질지 선택해야 한다.</p>
<h3 id="주-테이블-외래-키">주 테이블 외래 키</h3>
<p>주 객체가 대상 객체를 참조하는 것처럼 주 테이블에 외래키를 두고 대상 테이블을 참조한다. 외래 키를 객체 참조와 비슷하게 사용할 수 있어 객체지향 개발자들이 선호한다. 또한 JPA도 주 테이블에 외래 키가 있으면 좀 더 편리하게 매핑할 수 있고 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다.</p>
<h4 id="단방향">단방향</h4>
<pre><code class="language-java">
@Entity
public class Member {

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

    private String username;

    @OneToOne
    @JoinColumn(name = &quot;LOCKER_ID&quot;)
    private Locker locker;
    //
}

@Entity
public class Locker {

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

    private String name;
    //

}</code></pre>
<p>일대일 관계이므로 @OneToOne을 사용했고 데이터베이스에는 LOCKER_ID 외래키에 유니크 제약 조건을 걸었다. 이 관계는 다대일 단방향(@ManyToOne)과 거의 유사하다.</p>
<h4 id="양방향">양방향</h4>
<p>Locker 엔티티에 반대방향 참조를 추가하자.</p>
<pre><code class="language-java">@Entity
public class Locker {

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

    private String name;

    @OneToOne(mappedBy = &quot;locker&quot;)
    private Member member;

}</code></pre>
<p>주 테이블 Member가 외래키를 가지고 있으므로 연관관계의 주인은 Member.locker이다. 따라서 반대편인 Locker.member에 mappedBy 속성을 선언한다.</p>
<h3 id="대상-테이블에-외래-키">대상 테이블에 외래 키</h3>
<h4 id="단방향-1">단방향</h4>
<p><img src="https://velog.velcdn.com/images/now_here/post/d5b50278-6bda-4833-b799-33abf7e701e1/image.png" alt=""></p>
<p>일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않는다. 매핑할 수 있는 방법도 없다.
이 때는 <strong>단방향 관계를 Locker에서 Member에서 수정하거나</strong>, 양방향 관계로 만들고 Locker를 연관관계의 주인으로 설정해야 한다.</p>
<h4 id="양방향-1">양방향</h4>
<p><img src="https://velog.velcdn.com/images/now_here/post/b7aa2ed5-083f-45bc-9a92-1a2f78b538ca/image.png" alt=""></p>
<pre><code class="language-java">//일대일 대상 테이블에 외래 키, 양방향

@Entity
public class Member {

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

    private String username;

    @OneToOne(mappedBy = &quot;member&quot;)
    private Locker locker;
    //
}    

@Entity
public class Locker {

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

    private String name;

    @OneToOne
    @JoinColumn(name = &quot;MEMBER_ID&quot;)
    private Member member;
    //
}</code></pre>
<p>일대일 매핑에서 대상 테이블에 외래 키를 두고 싶다면 위와 같이 양방향 매핑을 한다. 주 엔티티인 Member 엔티티 대신에 대상 엔티티인 Locker를 연관관계의 주인으로 만들어서 LOCKER 테이블의 외래 키를 관리하도록 했다.</p>
<hr>
<h2 id="다대다-nn">다대다 [N:N]</h2>
<p>관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없어 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다.
예를 들어 회원은 상품을 여러 개 주문할 수 있고, 상품은 회원들에 의해 주문되는 이런 관계가 다대다 관계이다.
<img src="https://velog.velcdn.com/images/now_here/post/38113eb5-704f-47eb-8d00-9fde47cbf85a/image.png" alt=""></p>
<h3 id="다대다-단방향">다대다: 단방향</h3>
<p>객체에서는 @ManyToMany를 사용하여 다대다 관계를 편리하게 매핑할 수 있다.</p>
<pre><code class="language-java">@Entity
public class Member {

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

    private String username;

    @ManyToMany
    @JoinTable(name = &quot;MEBMER_PRODUCT&quot;,
        joinColumns = @JoinColumn(name = &quot;MEMBER_ID&quot;),
        inverseJoinColumns = @JoinColumn(name = &quot;PRODUCT_ID&quot;))
    private List&lt;Product&gt; products = new ArrayList&lt;Product&gt;();
    //
}    </code></pre>
<pre><code class="language-java">// 다대다 단방향 상품
@Entity
public class Product {

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

    private String name;
    ...
}</code></pre>
<p>@MantyToMany와 @JoinTable을 사용해서 연결 테이블을 바로 매핑했다. 따라서 회원과 상품을 연결하는 회원_상품(Member_Product) 엔티티 없이 매핑을 완료할 수 있다.</p>
<h3 id="다대다--양방향">다대다 : 양방향</h3>
<p>역방향도 @ManyToMany를 사용하영 양방향을 매핑을 한다. 이 때 mappedBy를 사용하여 연관관계의 주인도 설정한다.</p>
<pre><code class="language-java">@Entity
public class Product {

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

    @ManyToMany(mappedBy = &quot;products&quot;)  // 역방향 추가
    private List&lt;Member&gt; members;
    //
}    </code></pre>
<p>member.getProducts().add(product);
product.getMembers().add(member);
와 같은 방법으로 다대다 양방향 연관관계를 설정한다.</p>
<p>양방향 연관관계는 다음과 같이 편의 메소드를 추가해서 관리하는 것이 편리하다.</p>
<pre><code class="language-java">public void addProduct(Product product) {
    ...
    products.add(product);
    product.getMembers().add(this);
}</code></pre>
<p>이러면 다음과 같이 양방향 연관관계를 설정할 수 있다.
member.addProduct(product);</p>
<h3 id="다대다-매핑의-한계와-극복-연결-엔티티-사용">다대다: 매핑의 한계와 극복, 연결 엔티티 사용</h3>
<p>실무에서는 연결테이블에 외래키만 존재하는 것이 아니라 다른 컬럼들도 추가로 필요하다. @ManyToMany를 통해서 연결 엔티티를 안 만들었지만 이럴 경우에는 추가한 컬럼들을 매핑할 수 없기 때문에 연결 엔티티를 사용해야 한다.</p>
<pre><code class="language-java">
@Entity
public class Member {

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

    //역방향
    @OneToMany(mappedBy = &quot;member&quot;)
    private List&lt;Product&gt; products = new ArrayList&lt;Product&gt;();
    //
}</code></pre>
<pre><code class="language-java">//회원상품 엔티티
@Entity
@IdClass(MemberProductId.class)
public class MemberProduct {

    @Id
    @ManyToOne
    @JoinColumn(name = &quot;MEMBER_ID&quot;)
    private Member member;  //MemberProductId.member와 연결

    @Id
    @ManyToOne
    @JoinColumn(name = &quot;PRODUCT_ID&quot;)
    private Product product;  //MemberProductId.product와 연결

    private int orderAmount;

    ...
}</code></pre>
<pre><code class="language-java">//회원상품 식별자 클래스
public class MemberProductId implements Serializable {

    private String member;  //MemberProduct.member와 연결
    private String product; //MemberProduct.product와 연결

    @Override
    public int hashCode() {
        //
    }

    @Override
    public boolean equals(Object obj) {
        //
    }
}</code></pre>
<p>회원상품(MemberProduct)엔티티를 보면 @Id와 @JoinColumn을 동시에 써서 기본 키와 외래 키를 한 번에 매핑했다. 그리고 @IdClass를 사용해서 복합 기본 키를 매핑했다. 이 엔티티는 기본 키가 MEMBER_ID와 PRODUCT_ID로 이루어진 복합키이다. 이럴 경우 JPA에서는 복합키를 사용하기 위해서 식별자 클래스를 만들어야 한다. 그리고 엔티티에 @IdClass를 사용해서 식별자 클래스를 지정하면 된다.</p>
<h4 id="식별자-클래스-특징">식별자 클래스 특징</h4>
<ul>
<li>복합 키는 별도의 식별자 클래스로 만들어야 한다.</li>
<li>Serializable을 구현해야 한다.</li>
<li>equals와 hashCode 메소드를 구현해야 한다.</li>
<li>기본 생성자가 있어야 한다.</li>
<li>식별자 클래스는 public이어야 한다.</li>
<li>@IdClass를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법도 있다.</li>
</ul>
<h4 id="식별-관계">식별 관계</h4>
<p>부모 테이블의 기본 키를 받아서 자신의 <strong>기본 키 + 외래 키</strong>로 사용하는 것을 데이터베이스 용어로 <strong>식별관계</strong>(Identifying Relationship)이라고 한다.</p>
<p>이처럼 복합키를 사용하면 ORM매핑에서 해야할 일이 늘어난다. 식별자 클래스를 만들어야 하고 @IdClass나 @EmbeddedId도 사용해야 한다. 이를 극복하기 위해 복합 키를 사용하지 않고 간단히 다대다 관계를 구성하는 방법을 알아보자.</p>
<h3 id="다대다--새로운-기본-키-사용">다대다 : 새로운 기본 키 사용.</h3>
<p>추천하는 기본 키 생성 전략은 데이터베이스에서 자동으로 생성해주는 대리 키를 Long값으로 사용하는 것이다. 간편하고 거의 영구히 쓸 수 있으며 비지니스에 의존적이지도 않다.
<img src="https://velog.velcdn.com/images/now_here/post/bba1ff46-ced1-4bb9-9e4b-4691a05a018e/image.png" alt=""></p>
<pre><code class="language-java">@Entity
public class Order {

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

    @ManyToOne
    @JoinColumn(name = &quot;MEMBER_ID&quot;)
    private Member member;

    @ManyToOne
    @JoinColumn(name = &quot;PRODUCT_ID&quot;)
    private Product product;

    private int orderAmount;
    ...
}</code></pre>
<p>위와 같이 새로운 기본 키를 만들고 MEMBEER_ID, PRODUCT_ID는 외래 키로만 사용한다.
이 처럼 새로운 기본 키를 생성하면 식별자 클래스를 사용하지 않아서 코드가 한결 단순해진다.</p>
<h3 id="다대다-연관관계-정리">다대다 연관관계 정리</h3>
<p>다대다 연관관계를 일대다, 다대일 연관관계로 풀어내기 위해서는 연결 테이블을 만들 때 식별자를 어떻게 구성할지 선택해야 한다.</p>
<ul>
<li>식별 관계 : 받아온 식별자를 기본 키 + 외래 키로 사용한다.</li>
<li>비식별 관계 : 받아온 식별자는 외래 키로만 사용하고 새로운 식별자를 추가한다.</li>
</ul>
<p>객체 입장에서는 비식별 관계를 사용하는 것이 복합 키를 위한 식별자 클래스를 만들지 안혹 편리하게 ORM 매핑을 할 수 있어 식별관계보다는 비식별 관계를 추천한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[5장 연관관계 매핑 기초]]></title>
            <link>https://velog.io/@now_here/5%EC%9E%A5-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-%EA%B8%B0%EC%B4%88</link>
            <guid>https://velog.io/@now_here/5%EC%9E%A5-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-%EA%B8%B0%EC%B4%88</guid>
            <pubDate>Wed, 27 Nov 2024 12:33:11 GMT</pubDate>
            <description><![CDATA[<p>객체는 참조를 통해 관계를 맺고 테이블은 외래키를 통해 관계를 맺는다. 이런 객체의 연관관계와 테이블의 연관관계를 맺는 것이 이 장의 목표이다.</p>
<h1 id="단방향-연관관계">단방향 연관관계</h1>
<p>다대일(N:1) 단방향 관계를 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/now_here/post/004d777c-fc6b-4a75-846f-22b2764f5f61/image.png" alt=""></p>
<h3 id="객체-연관관계">객체 연관관계</h3>
<ul>
<li>회원 객체는 Member.team 참조를 통해 연관관계를 맺는다. 이 때, Memer -&gt; Team 조회는 Member.getTeam()으로 조회가 가능하지만 Team -&gt; Member는 불가능하다.</li>
</ul>
<h3 id="테이블-연관관계">테이블 연관관계</h3>
<ul>
<li>테이블은 항상 <strong>양방향 연관관계</strong>를 가진다.</li>
<li>회원 테이블은 TEAM_ID 외래키로 팀 테이블과 연관관계를 맺는다. </li>
<li>이 때, TEAM_ID 외래키를 가지고 회원 테이블에서 팀 테이블을 조인할 수 있고, 팀 테이블에서 회원 테이블을 조인할 수 있다.</li>
</ul>
<pre><code class="language-sql">// MEMBER 테이블에서 TEAM 테이블 조인
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

// TEAM 테이블에서 MEMBER 테이블 조인
SELECT *
FROM TEAM T
JOIN MEMBER T ON T.TEAM_ID = M.TEAM_ID</code></pre>
<p>정리하면 객체의 참조는 항상 단방향 연관관계를 맺고 테이블은 항상 단방향 연관관계를 가진다.
따라서 객체를 양방향 연관관계를 가지기 위해서는 단방향 연관관계를 2번 맺어야 한다.
다음과 같이 객체의 연관관계를 맺으려는 객체를 속성으로 각각 가져야 함을 의미한다.
A -&gt; B (a.b)
B -&gt; A (b.a)</p>
<pre><code class="language-java">
// 객체의 양방향 연관관계
class A {
    B b
 }

 class B {
     A a
 }</code></pre>
<h3 id="객체-그래프-탐색">객체 그래프 탐색</h3>
<pre><code class="language-java">Team team = member1.getTeam();</code></pre>
<p>위와 같이 참조를 통해 연관관계를 탐색하는 것을 <strong>객체 그래프 탐색</strong>이라고 한다.</p>
<p>위에 참조를 통한 연관관계 탐색을 테이블에서도 똑같이 구현해 보면 아래와 같다.</p>
<pre><code class="language-sql">SELECT T.*
FROM MEMBER M
    JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
WHERE M.MEMBER_ID = &#39;member1&#39;</code></pre>
<p>이렇게 외래키를 통해 연관관계 탐색하는 것을 테이블에서는 <strong>조인</strong>이라고 한다.</p>
<h3 id="객체-관계-매핑">객체 관계 매핑</h3>
<p>이제 객체와 테이블의 연관관계를 매핑해보자.</p>
<pre><code class="language-java">@Entity
public class Member {
    @Id
    @Column(name = &quot;MEMBER_ID&quot;)
    private String id;
    private String name;

    @ManyToOne
    @JoinColumn(name = &quot;TEAM_ID&quot;)
    private Team team;

    //연관관계 설정
    public void setTeam(Team team) {
        this.team = team;
    }

    //setter, getter
}


@Entity
public class Team {

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

    private String name;

    //setter, getter
}</code></pre>
<ul>
<li>객체 연관관계 : 회원 객체의 Member.team 필드 사용</li>
<li>테이블 연관관계 : 회원 테이블의 MEMBER.TEAM_ID 외래 키 컬럼을 사용</li>
</ul>
<p>Member.team과 MEMBER.TEAM_ID를 매핑하는 것이 연관관계 매핑이다.</p>
<h3 id="manytoone">@ManyToOne</h3>
<p>다대일 연관관계를 맺기 위해 사용하는 어노테이션이다. 이름 그대로 다대일(N:1) 관계라는 매핑 정보이다.
다대일을 매핑하기 위해서 필수적으로 어노테이션을 사용해야한다.</p>
<h3 id="joincolumn">@JoinColumn</h3>
<p><strong>외래키를 매핑할 때 사용하는 어노테이션</strong>
name 속성에는 매핑할 외래키의 이름을 지정한다. 이 어노테이션은 생략할 수 있다.
(어노테이션을 생략하면 선언한 필드 이름과 조인할 객체의 id가 외래키로 설정된다고 한다)</p>
<pre><code class="language-java">@Entity
public class Member {
    @Id
    @Column(name = &quot;MEMBER_ID&quot;)
    private String id;
    private String name;

    @ManyToOne
    private Team team;

    //연관관계 설정
    public void setTeam(Team team) {
        this.team = team;
    }

    //setter, getter
}


@Entity
public class Team {

    @Id
    private String id;

    private String name;

    //setter, getter
}</code></pre>
<p>이런 경우 Member에 선언한 team 이름과 외래 키로 연관관계를 맺을 Team에 id를 언더바로 조합하여 외래키로 설정한다고 한다. (외래 키 : team_id)
<strong>자동 생성 외래 키 이름: 참조 대상 필드_참조 대상 클래스 기본 키 필드</strong></p>
<hr>
<h2 id="연관관계-사용">연관관계 사용</h2>
<h3 id="저장">저장</h3>
<pre><code class="language-java">        // 팀 1 저장
        Team team1 = new Team(&quot;team1&quot;, &quot;팀1&quot;);
        em.persist(team1);

        // 회원1 저장
        Member member1 = new Member(&quot;member1&quot;, &quot;회원1&quot;);
        member1.setTeam(team1); //연관관계 설정 member1 -&gt; team1
        em.persist (member1);</code></pre>
<p><em>- JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.</em>
memer1에  set을 통해서 team1을 참조했고 em.persist로 저장하였다. 이렇게 하면 JPA에서는 참조한 팀의 식별자를 통해서 적절한 등록 쿼리를 생성한다.</p>
<pre><code class="language-sql">INSERT INTO TEMA (TEAM_ID, NAME) VALUES (&#39;team1&#39;, &#39;팀1&#39;)
INSERT INTO MEMBER (MEMBER_ID, NAME, TEAM_ID) VALUES (&#39;member1&#39;, &#39;회원1&#39;, &#39;team1&#39;)</code></pre>
<h3 id="조회">조회</h3>
<ul>
<li><p><strong>객체 그래프 탐색 (객체 연관관계를 사용한 조회)</strong>
member.getTeam()을 사용해서 member와 연관된 team 엔티티를 조회할 수 있다.</p>
<pre><code class="language-java">Member member = em.find(Member.class, &quot;member1&quot;);
Team team = member.getTeam();    // 객체 그래프 탐색</code></pre>
</li>
<li><p><strong>객체지향 쿼리 사용(JPQL)</strong></p>
<pre><code class="language-java">      String jpql = &quot;select m from Member m join m.team t where &quot; +
          &quot;t.name =:teamName&quot;;

      List&lt;Member&gt; resultList = em.createQuery(jpql, Member.class)
          .setParameter(&quot;teamName&quot;, &quot;팀1&quot;)
          .getResultList();</code></pre>
<p>위에 jpql을 보면 조인을 할 때 Member가 참조하고 있는 m.team(필드)을 이용해서 조인을 하고 있다.
:teamName 부분은 : 뒤에 붙어있는 이름으로 파라미터를 바인딩하는 문법이다. 이를 실행하면 다음과 같은 SQL이 실행된다.</p>
<pre><code class="language-sql">SELECT M.*
FROM MEMBER MEMBER
INNER JOIN TEAM TEAM ON MEMBER.TEAM_ID = TEAM.TEAM_ID
WHERE TEAM.NAME = &#39;팀1&#39;</code></pre>
</li>
</ul>
<h3 id="수정">수정</h3>
<p>em.update 와 같은 기능이 없기 때문에 앞선 장에서 설명했듯이 엔티의 값만 변경하면 트랜잭션을 커밋할 때 더티체킹을 통해서 변경사항이 자동으로 반영된다.
이것은 연관관계를 수정할 때도 같은데, 참조하는 대상만 변경하면 나머지는 JPA가 자동으로 처리한다.</p>
<pre><code class="language-java">Team team2 = new Team(&quot;team2&quot;, &quot;팀2&quot;);
em.persist(team2);

Member member = em.find(Member.class, &quot;member1&quot;);
member.setTeam(team2);    //참조하는 team1을 team2로 변경</code></pre>
<h3 id="연관관계-제거">연관관계 제거</h3>
<p>연관관계를 null로 설정하여 연관관계를 제거한다.</p>
<pre><code class="language-java">Member member1 = em.find(Member.class, &quot;member1&quot;);
member1.setTeam(null);    // 연관관계 제거</code></pre>
<p>다음과 같은 SQL이 실행된다.</p>
<pre><code class="language-sql">UPDATE MEMBER
SET
    TEAM_ID=NULL, ...
WHERE
    ID=&#39;member1&#39;</code></pre>
<h3 id="연관된-엔티티-삭제">연관된 엔티티 삭제</h3>
<p>연관된 엔티티를 삭제하려면 기존의 연관관계를 먼저 제거하고 삭제해야 한다. 그렇지 않으면 외래 키 제약조건으로 인해, 데이터베이스에서 오류가 발생한다.
만약, 팀1에 회원1과 회원2가 소속되어 있다면 팀1을 삭제하려면 연관관계를 먼저 제거해야 한다.</p>
<pre><code class="language-java">member1.setTeam(null);    // 회원1 연관관계 제거
member2.setTeam(null);    // 회원2 연관관계 제거
em.remove(team);        // 팀 삭제</code></pre>
<hr>
<h2 id="양방향-연관관계">양방향 연관관계</h2>
<p><img src="https://velog.velcdn.com/images/now_here/post/02fc20a3-40c6-41dd-b3ca-041f45dd5832/image.png" alt=""></p>
<p>이제 양방향 연관관계를 살펴보자.
Member와 Team은 다대일 관계이다. 반대로 팀과 멤버는 일대다 관계이다. 일대다 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야 한다. 그러기 위해 Team.members를 List 컬렉션으로 추가한다.</p>
<ul>
<li>회원 -&gt; 팀 (Member.team)</li>
<li>팀 -&gt; 회원(Team.members)</li>
</ul>
<p>테이블은 앞서 말했듯이 외래키로 양방향 연관관계를 맺기 때문에 별다른 조치가 필요하지 않다.</p>
<p>양방향 연관관계를 매핑하기 위해 팀 엔티티를 다음과 같이 변경한다.</p>
<pre><code class="language-java">@Entity
public class Team {

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

    private String name;

    // 추가
    @OneToMany(mappedBy = &quot;team&quot;)
    private List&lt;Member&gt; members = new ArrayList&lt;Member&gt;();

    //Getter, Setter...</code></pre>
<p>일대다 관계를 매핑하기 위해 List<Member>로 컬렉션을 추가했고, @OneToMany 매핑정보를 사용했다.
해당 어노테이션에서 사용한 mappedBy 속성은 양방향 매핑일 때 사용하는데, 반대쪽 매핑의 필드 이름을 값으로 주면 된다. 반대쪽 매핑이 Member.team이므로 team을 값으로 넣어주었다.
이제부터 팀에서 회원 컬렉션으로 객체 그래프를 탐색할 수 있다.</p>
<hr>
<h2 id="연관관계의-주인">연관관계의 주인</h2>
<p>mappedBy를 설명하기 위해서는 연관관계의 주인이 있다는 것을 알아야 한다. 앞서 말했듯이 테이블은 외래키를 통해 양방향 연관관계를 가지지만 객체는 그렇지 않다. 위에서 양방향 연관관계를 매핑했다고 했지만 사실은 단방향 연관관계를 2개 매핑해 양방향처럼 보이게 한 것이다.</p>
<ul>
<li>팀 -&gt; 회원 (단방향)</li>
<li>회원 -&gt; 팀 (단방향)</li>
</ul>
<p>이처럼 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래키는 하나여서 둘 사이의 차이가 발생한다.
이런 차이로 인해 JPA에서는 두 객체 연관관계 . 중하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 <strong>연관관계의 주인(Owner)</strong>이라 한다.</p>
<p>그래서 양방향 연관관계를 매핑할 때는 다음과 같은 규칙이 존재한다. </p>
<ul>
<li>두 연관관계 중 하나를 연관관계의 주인으로 정해야 한다.</li>
<li>연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록,수정,삭제)할 수 있다.</li>
<li>주인이 아닌 쪽은 읽기만 할 수 있다.</li>
<li>mappedBy를 이용해서 연관관계의 주인을 정한다.</li>
<li>주인은 mappedBy 속성을 사용하지 않는다.</li>
<li>주인이 아니라면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.</li>
</ul>
<p><strong>연관관계의 주인을 정한다는 것은 외래키의 관리자를 선택하는 것이다.</strong>
그래서 외래키를 가지고 있는 테이블이 연관관계의 주인이 된다. 여기서는 Member가 Team을 외래키로 가지고 있기 때문에 Member.team이 주인이 된다.
데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 &quot;다&quot;쪽이 외래 키를 가진다. 그래서 항상 @ManyToOne은 연관관계의 주인이 되므로 mappedBy 속성이 없다.</p>
<hr>
<h2 id="양방향-연관관계-저장">양방향 연관관계 저장</h2>
<p>양방향 연관관계에서 엔티티를 저장하는 것은 앞에 단방향 연관관계에서 저장하는 것과 완전히 코드가 동일하다.
양방향 연관관계는 연관관계의 주인이 외래 키를 관리한다. 따라서 주인이 아닌 방향은 값을 설정하지 않아도 데이터베이스에서 외래 키값이 정상 입력된다. 이 말은 다음과 같이 team1에 member1,member2를 추가할 필요가 없다는 말이다.</p>
<pre><code class="language-java">team1.getMembers().add(member1);    //무시(연관관계의 주인이 아님)
team1.getMembers().add(member2);    //무시(연관관계의 주인이 아님)</code></pre>
<p>연관관계의 주인인 Member.team이 다음과 같이 외래 키를 관리한다.</p>
<pre><code class="language-java">  member1.setTeam(team1);    //연관관계 설정(연관관계의 주인)
  member2.setTeam(team1);    //연관관계 설정(연관관계의 주인)</code></pre>
<hr>
<h2 id="양방향-연관관계의-주의점">양방향 연관관계의 주의점</h2>
<p>양방향 연관관계를 설정하고 가장 많이 하는 실수가 바로 위에서 언급한 연관관계의 주인이 아닌 것에만 값을 입력하는 경우라고 한다.</p>
<pre><code class="language-java">  team1.getMembers().add(member1);    //무시(연관관계의 주인이 아님)
  team1.getMembers().add(member2);    //무시(연관관계의 주인이 아님)

  //member1.setTeam(team1);    //연관관계 설정(연관관계의 주인)
  //member2.setTeam(team1);    //연관관계 설정(연관관계의 주인)</code></pre>
<p>이렇게 설정하면 외래 키 TEAM_ID에 team1이 아닌 null 값이 입력된다. 연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문이다. 외래 키는 연관관계의 주인(여기서는 Member.team)이 관리하는 것을 꼭 기억하자.</p>
<p>하지만 이는 데이터베이스에 저장될 때의 이야기이고 연관관계의 주인이 아닌 엔티티에 값을 안 넣는 것은 객체 입장에서 심각한 문제를 야기할 수 있다. team1.getMembers().size()를 구한다고 하면 값을 안 넣었을 경우 0이 나오기 때문이다. 그래서 객체까지 고려해서 주인이 아닌 곳에도 값을 입력해야 한다.</p>
<h3 id="연관관계-편의-메서드">연관관계 편의 메서드</h3>
<p>이처럼 양방향 연관관계는 양쪽 다 신경을 써야 하는데 각각 코드를 호출하면 실수로 한 곳에만 값을 넣어줄 수도 있다. 이를 방지하기 위해 다음과 같이 set을 수정하면 좋다.</p>
<pre><code class="language-java">public class Member {

  private Team team;

  public void setTeam(Team team) {
      this.team = team;
    team.getMembers().add(this);
  }
  ...</code></pre>
<p>이렇게 수정하면 다음과 같이 사용할 수 있다.</p>
<pre><code class="language-java">public void test() {

  Team team1 = new Team(&quot;team1&quot;, &quot;팀1&quot;);
  em.persist(team1);

  Member member1 = new Member(&quot;member1&quot;, &quot;회원1&quot;);
  member1.setTeam(team1);    // 양방향 설정
  em.persist(member1);

  Member member2 = new Member(&quot;member2&quot;, &quot;회원2&quot;);
  member2.setTeam(team1);    // 양방향 설정
  em.persist(member2);
}</code></pre>
<h3 id="연관관계-편의-메서드-주의사항">연관관계 편의 메서드 주의사항</h3>
<p>위에 코드는 사실 문제가 있다. member1의 Team을 수정했을 때 기존 팀에서 여전히 member1이 조회된다는 것이다.</p>
<pre><code class="language-java">  member1.setTeam(teamA)    //teamA
  member1.setTeam(teamB)    //teamA -&gt; teamB 수정
  Member findMember = teamA.getMember();    //member1이 여전히 조회된다.</code></pre>
<p>이 문제는 연관관계를 수정할 때, 기존 연관관계를 제거하지 않아서 일어나는 문제이다. 다음과 같이 수정하면 된다.</p>
<pre><code class="language-java">
public void setTeam(Team team) {
  if (this.team != null) {
      this.team.getMembers().remove(this);
  }
  this.team = team;
  team.getMembers().add(this);</code></pre>
<h3 id="정리">정리</h3>
<ul>
<li>단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.</li>
<li>단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.</li>
<li>양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.</li>
</ul>
<p>참조 : [자바 ORM 표준 JPA 프로그래밍]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[4장 엔티티 매핑]]></title>
            <link>https://velog.io/@now_here/4%EC%9E%A5-%EC%97%94%ED%8B%B0%ED%8B%B0-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@now_here/4%EC%9E%A5-%EC%97%94%ED%8B%B0%ED%8B%B0-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Sat, 23 Nov 2024 09:55:16 GMT</pubDate>
            <description><![CDATA[<p>엔티티와 테이블을 어떻게 매핑하는지 설계에 해당하는 정적인 부분을 알아보자!
JPA를 사용할 때 가장 중요한 부분은 엔티티와 테이블을 정확히 매핑하는 일이다.</p>
<hr>
<h2 id="entity">@Entity</h2>
<p>JPA를 사용해서 테이블과 <strong>매핑할 클래스</strong>
해당 클래스는 필수로 @Entity를 붙여야 한다. 해당 어노테이션이 붙은 클래스는 JPA가 관리하는 것으로 <strong>엔티티</strong>라고 부른다.</p>
<pre><code class="language-java">@Entity(name = &quot;엔티티 이름 지정&quot;)  // name을 지정하지 않으면 클래스 이름을 그대로 사용한다. ex) Member
public class Member {
//
...
}</code></pre>
<h3 id="entity-사용시-주의사항">@Entity 사용시 주의사항</h3>
<ul>
<li>기본 생성자는 필수다.
JPA가 엔티티 객체를 생성할 때 기본 생성자를 사용하므로 이 생성자는 반드시 있어야 한다.</li>
<li>final 클래스, enum, interface, inner 클래스에는 사용할 수 없다.</li>
<li>저장할 필드에 final을 사용하면 안 된다.</li>
</ul>
<h2 id="table">@Table</h2>
<p><strong>엔티티와 매핑할 테이블</strong>
@Table은 엔티티와 매핑할 테이블을 지정한다. 생략하면 매핑한 엔티티 이름을 테이블 이름으로 사용한다.</p>
<pre><code class="language-java">import javax.persistence.*;

@Entity
@Table(name=&quot;MEMBER&quot;)
public class Member {
...
}</code></pre>
<hr>
<h2 id="데이터베이스-스키마-자동-생성">데이터베이스 스키마 자동 생성</h2>
<p>엔티티만 만들고 테이블은 자동 생성되도록 데이터베이스 스키마 자동 생성 기능을 알아보자.
JPA는 매핑정보와 데이터베이스 방언을 사용해서 데이터베이스 스키마를 자동으로 생성하는 기능을 지원한다.
해당 기능을 사용하려면 먼저 persistence.xml에 다음 속성을 추가해야 한다.</p>
<pre><code class="language-java">// 애플리케이션 실행 시점에 DB 테이블 자동 생성
&lt;property name=&quot;hibernate.hbm2ddl.auto&quot; value=&quot;create&quot; /&gt;

// 콘솔에 실행되는 테이블 생성 DDL 출력
&lt;property name=&quot;hibernate.show_sql&quot; value=&quot;true&quot; /&gt;</code></pre>
<p>스키마 자동 생성 기능이 만든 DDL은 운영 환경에서 사용할 만큼 완벽하지는 않으므로 개발 환경에서 사용하거나 매핑을 어떻게 해야하는지 참고하는 정도로만 사용하는 것이 좋다.</p>
<p><strong>hibernate.hbm2ddl.auto 속성</strong></p>
<ul>
<li>create : 기존 테이블을 삭제하고 새로 생성한다. (DROP + CREATE)</li>
<li>create-drop : create 속성에 추가로 애플리케이션을 종료할 때 생성한 DDL을 제거한다. (DROP+CREATE+DROP)</li>
<li>update : DB 테이블과 엔티티 매핑정보를 비교하여 변경 사항만 수정한다.</li>
<li>validate : DB 테이블과 엔티티 매핑정보를 비교해서 차이가 있으면 경고를 남기고 애플리케이션을 실행하지 않는다. 이 설정은 DDL을 수정하지 않는다.</li>
</ul>
<p>(개발 초기에는 create 또는 update
초기화 상태로 자동화된 테스트를 진행하는 개발자 환경과 CI 서버는 create 또는 create-drop
테스트 서버는 update 또는 validate
스테이징과 운영 서버는 validate</p>
<h3 id="이름-매핑-전략-변경하기">이름 매핑 전략 변경하기</h3>
<p>자바 언어는 관례상 카멜표기법을 주로 사용하고, DB는 관례상 언더스코어(_)를 주로 사용한다. </p>
<pre><code class="language-java">@Column(name = &quot;role_type&quot;)
String roleType;</code></pre>
<p>위와 같이 직접적으로 이름을 매핑해도 되지만 hibernate.ejb.naming_strategy 속성을 사용하면 이름 매핑 전략을 변경할 수 있다. 하이버네이트는 org.hibernate.cfg.ImprovedNamingStrategy 클래스를 제공한다. 이 클래스는 테이블 명이나 컬럼 명이 생략되면 자바의 카멜 표기법을 테이블의 언더스코어 표기법으로 매핑한다.</p>
<pre><code class="language-java">&lt;property name=&quot;hibernate.ejb.naming_strategy&quot; value=&quot;org.hibernate.cfg.ImprovedNamingStrategy&quot; /&gt;</code></pre>
<hr>
<h2 id="ddl-생성-기능">DDL 생성 기능</h2>
<p>그 전에 앞서 설명 안 하고 넘겼던 몇 가지 어노테이션을 소개하겠다.
<strong>@Id</strong> : 기본 키 매핑
<strong>@Column</strong> : 필드와 컬럼 매핑
<strong>@Enumerated</strong> : 자바의 enum을 사용하기 위해 쓰는 어노테이션
<strong>@Temporal</strong> : 자바의 날짜 타입을 사용하기 위해 쓰는 어노테이션
<strong>@Lob</strong> : 데이터베이스의 VARCHAR 타입 대신에 CLOB, BLOB 타입을 매핑하기 위해 쓰는 어노테이션</p>
<pre><code class="language-java">@Entity
@Table(name=&quot;MEMBER2&quot;)
public class Member2 {

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

    @Column(name = &quot;NAME&quot; , nullable = false, length = 10)
    private String username;</code></pre>
<p>스키마 자동 생성하기를 통해 만들어지는 DDL에 nullable과 length와 같은 속성을 이용하여 제약조건을 추가할 수 있다. 위에 예제는 username 필드를 NAME 컬럼과 매핑하고 NOT NULL, 10자 제한을 건 모습이다.
실행되는 DDL은 아래와 같다.</p>
<pre><code class="language-sql">create table MEMBER2 
    ID varchar(255) not null,
    NAME varchar(10) not null,
    ...
    primary key (ID)
)</code></pre>
<h3 id="유니크-제약조건">유니크 제약조건</h3>
<pre><code class="language-java">@Entity
@Table(name=&quot;MEMBER2&quot;, uniqueConstraints = {@UniqueConstraint(
    name = &quot;NAME_AGE_UNIQUE&quot;,
    columnNames = {&quot;NAME&quot;, &quot;AGE&quot;})})
public class Member2 {

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

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

    private Integer age;
    ...
}</code></pre>
<p>위와 같이 @UniqueConstraint 을 이용해서 유니크 제약조건을 만들 수 있다. 애플리케이션을 실행하면 스키마 자동생성 기능으로 다음과 같은 DDL이 실행된다.</p>
<pre><code class="language-sql">ALTER TABLE MEMBER
    ADD CONSTRAINT NAME_AGE_UNIQUE UNIQUE (NAME, AGE)    </code></pre>
<p><strong>이런 기능들은 단지 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다.</strong>
이 부분을 좀 더 설명하자면 해당 어노테이션들이 JPA가 엔티티를 관리하고 동작하는 방식(더티체킹, 동일성 보장 등)에는 영향을 미치지 않는다는 의미이다.
따라서 직접 DDL을 만들어 스키마 자동생성 기능을 사용하지 않는다면 사용할 이유가 없지만 해당 기능을 명시하면 개발자가 엔티티만 보고도 손쉽게 다양한 제약 조건을 파악할 수 있다는 장점이 있다.</p>
<hr>
<h2 id="기본-키primary-key-매핑">기본 키(Primary Key) 매핑</h2>
<p>지금까지 @Id 만을 사용하여 애플리케이션에서 기본 키를 직접 할당했다. 여기서는 데이터베이스에서 생성해주는 값을 기본 키로 사용하는 방법을 배우자.</p>
<p>JPA에서 기본 키를 할당하는 전략은 크게 아래 두 가지이다.</p>
<ul>
<li>직접 할당 : 애플리케이션에서 기본 키를 직접 할당한다.</li>
<li>자동 생성 : 대리키 사용 방식</li>
<li><em>IDENTITY*</em> : 기본 키 생성을 데이터베이스에 위임한다. (ex. MySQL)</li>
<li><em>SEQUENCE*</em> : 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다. (ex. 오라클)</li>
<li><em>TABLE*</em> : 키 생성 테이블을 사용한다. 키 생성용 테이블을 만들어두고 마치 시퀀스처럼 사용하는 방법. 모든 데이터베이스에서 사용할 수 있다.</li>
</ul>
<p>JPA에서 위처럼 자동 생성 전략이 다양한 이유는 데이터베이스 벤더마다 지원하는 방식이 다르기 때문이다.
기본 키를 직접 할당하려면 @Id 만 사용하면 되고 자동 생성 전략을 사용하려면 @GeneratedValue를 사용하면 된다.</p>
<pre><code class="language-java">// 자동 키 생성 전략을 사용하기 위해서는 persistence.xml에 아래 속성을 추가해야 한다.
&lt;property name=&quot;hibernate.id.new_generator_mappings&quot; value=&quot;true&quot; /&gt;</code></pre>
<h3 id="기본-키-직접-할당-전략">기본 키 직접 할당 전략</h3>
<pre><code class="language-java">@Id
@Column(name = &quot;id&quot;)
private String id;</code></pre>
<p>@Id 적용 가능 자바 타입은 다음과 같다.</p>
<ul>
<li>자바 기본형</li>
<li>자바 래퍼Wraper형</li>
<li>String</li>
<li>java.util.Date</li>
<li>java.sql.Date</li>
<li>java.math.BigDecimal</li>
<li>java.math.BigInteger</li>
</ul>
<p>기본 키 직접 할당 전략은 em.persist()로 엔티티를 저장하기 전에 직접적으로 애플리케이션에서 기본 키를 직접 할당하는 방법이다.</p>
<pre><code class="language-java">Student st = new Student();
st.setId(&quot;id&quot;)    // 기본 키 직접 할당
em.persist(st);</code></pre>
<h2 id="기본-키로-객체를-사용할-수-있을까">기본 키로 객체를 사용할 수 있을까?</h2>
<blockquote>
<p>그런데 그러면 기본 키로 위와 같은 타입만 사용하면 객체를 기본 키로 사용할 수 없는거야?</p>
</blockquote>
<p>위의 내용을 보다가 객체를 기본 키로 사용할 수는 없나 하는 궁금증이 생겼다. 
조금 조사해보니 객체 자체를 기본 키로 사용할 수는 있지만 위 처럼 @Id를 사용하는 것으로는 안 된다고 한다. 객체 자체를 기본 키로 사용하는 경우 기본 키를 복합키로 설정해야 한다고 하며 @IdClass 나 @EmbeddedId 를 사용해야 한다고 한다. 아래는 해당내용을 좀 더 자세히 서술한 부분인데 <strong>7장 고급 매핑 - 7.3 복합 키와 식별 관계 매핑</strong>에서 다룰 것으로 보인다. 지금은 &#39;이런 것도 있구나&#39; 정도로 넘어가자.</p>
<hr>
<h3 id="1-기본-키로-객체를-사용할-수-없는-이유"><strong>1. 기본 키로 객체를 사용할 수 없는 이유</strong></h3>
<ul>
<li><code>@Id</code>는 기본 키를 지정하는 어노테이션이지만, <strong>단순 값 타입</strong>(String, Long, Integer 등)이나 <strong>임베디드 타입</strong>만 기본 키로 직접 사용할 수 있습니다.</li>
<li>객체 타입(<code>Student</code>)를 직접 기본 키로 설정하려면, 해당 객체가 <strong>복합 키</strong>로 간주되어야 하고, JPA에서 복합 키를 처리하기 위해 <strong><code>@EmbeddedId</code> 또는 <code>@IdClass</code></strong>를 사용해야 합니다.</li>
</ul>
<hr>
<h3 id="2-복합-키를-사용하는-방법"><strong>2. 복합 키를 사용하는 방법</strong></h3>
<h4 id="1-embeddedid-사용">(1) <code>@EmbeddedId</code> 사용</h4>
<p><code>@EmbeddedId</code>를 사용하면 <strong>객체 자체를 기본 키로 사용</strong>할 수 있습니다.<br>이 방식에서는 기본 키 객체를 별도로 정의하고, 엔티티에서 이를 포함하도록 합니다.</p>
<h5 id="예제">예제:</h5>
<pre><code class="language-java">@Embeddable
public class StudentId {
    private Long studentId;
    private String schoolId;

    // equals()와 hashCode() 반드시 구현
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        StudentId that = (StudentId) o;
        return Objects.equals(studentId, that.studentId) &amp;&amp;
               Objects.equals(schoolId, that.schoolId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(studentId, schoolId);
    }

    // getter, setter
}</code></pre>
<pre><code class="language-java">@Entity
public class Student {
    @EmbeddedId
    private StudentId id; // 객체를 기본 키로 사용

    private String name;

    // getter, setter
}</code></pre>
<h4 id="2-idclass-사용">(2) <code>@IdClass</code> 사용</h4>
<p><code>@IdClass</code>는 <strong>복합 키를 정의하는 또 다른 방식</strong>입니다.<br>이 방식에서는 기본 키 클래스를 별도로 정의하고, 엔티티에서 해당 키의 구성 요소를 명시합니다.</p>
<h5 id="예제-1">예제:</h5>
<pre><code class="language-java">// 복합 키 클래스
public class StudentId implements Serializable {
    private Long studentId;
    private String schoolId;

    // equals()와 hashCode() 반드시 구현
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        StudentId that = (StudentId) o;
        return Objects.equals(studentId, that.studentId) &amp;&amp;
               Objects.equals(schoolId, that.schoolId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(studentId, schoolId);
    }

    // getter, setter
}</code></pre>
<pre><code class="language-java">@Entity
@IdClass(StudentId.class)
public class Student {
    @Id
    private Long studentId;

    @Id
    private String schoolId;

    private String name;

    // getter, setter
}</code></pre>
<hr>
<h3 id="3-차이점-비교"><strong>3. 차이점 비교</strong></h3>
<table>
<thead>
<tr>
<th><strong>특징</strong></th>
<th><strong>@EmbeddedId</strong></th>
<th><strong>@IdClass</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>구현 방식</strong></td>
<td>기본 키를 하나의 임베디드 객체로 관리</td>
<td>기본 키를 개별 필드로 선언</td>
</tr>
<tr>
<td><strong>클래스 선언 위치</strong></td>
<td>엔티티에서 기본 키로 사용할 객체를 필드로 선언</td>
<td>엔티티 외부에 별도의 기본 키 클래스를 정의</td>
</tr>
<tr>
<td><strong>코드 가독성</strong></td>
<td>하나의 필드로 관리 가능해 간결</td>
<td>개별 필드를 명시해야 하므로 약간 복잡</td>
</tr>
<tr>
<td><strong>equals/hashCode</strong> 구현 여부</td>
<td>필수</td>
<td>필수</td>
</tr>
</tbody></table>
<hr>
<h3 id="4-질문으로-돌아와서"><strong>4. 질문으로 돌아와서</strong></h3>
<h4 id="코드">코드:</h4>
<pre><code class="language-java">@Id
@Column(name = &quot;id&quot;)
private Student student;</code></pre>
<p>위 코드는 JPA에서 <strong>에러가 발생합니다</strong>. 이유는:</p>
<ol>
<li><code>@Id</code>가 객체(<code>Student</code>) 타입에 직접 적용되었기 때문에, JPA는 이를 기본 키로 간주할 수 없습니다.</li>
<li>기본 키로 객체를 사용하려면 <code>@EmbeddedId</code> 또는 <code>@IdClass</code>를 사용해야 합니다.</li>
</ol>
<hr>
<h3 id="5-결론"><strong>5. 결론</strong></h3>
<ul>
<li>JPA에서 기본 키로 객체를 사용하려면 <strong>복합 키</strong>로 처리해야 하며, <code>@EmbeddedId</code> 또는 <code>@IdClass</code>를 사용해야 합니다.</li>
<li>단순히 <code>@Id</code>를 객체 타입에 붙이는 것은 JPA 실행 시점에서 예외를 발생시킵니다.</li>
<li>위의 방식 중 <strong>코드 가독성</strong>과 <strong>유지보수성</strong>을 고려해 적합한 방법을 선택하세요.<br>일반적으로, <strong><code>@EmbeddedId</code></strong> 방식이 더 깔끔하고 객체 지향적인 설계를 선호할 때 사용됩니다.</li>
</ul>
<hr>
<h3 id="identity-전략">IDENTITY 전략</h3>
<p><strong>기본 키 생성을 데이터베이스에 위임하는 전략.</strong> 
MySQL, PostgreSQL, SQL Server, DB2에서 주로 사용하는 방법이라고 한다. MySQL의 AUTO_INCREMENT 기능은 많이 봤을 것이라고 생각든다.</p>
<pre><code class="language-sql">CREATE TABLE BOARD (
    ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    DATA VARCHAR(255)
);</code></pre>
<p>IDENTITY 전략은 데이터베이스에 값을 저장하고 나서야 기본 키 값을 구할 수 있을 때 사용한다.
IDENTITY 전략을 사용하려면 @GeneratedValue의 strategy 속성 값을 GenerationType.IDENTITY로 지정하면 된다. 이 전략을 사용하면 JPA는 기본 키 값을 얻어오기 위해 데이터베이스를 추가로 조회한다.</p>
<pre><code class="language-java">@Entity
public class Board {

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

// IDENTITY 사용코드
  private static void logic(EntityManager em) {
        Board board = new Board();
        em.persist(board);
        System.out.println(&quot;board.getId() = &quot; + board.getId());  //board.getId() = 1
    }</code></pre>
<hr>
<h2 id="identity-전략-동작-원리-더-살펴보기">IDENTITY 전략 동작 원리 더 살펴보기</h2>
<blockquote>
<p>em.persist()를 호출하면 원래는 <strong>쓰기 지연 SQL 저장소</strong>에 insert 쿼리를 모아두고 트랜잭션을 커밋하면, 먼저 flush해서 DB와 통신하는 거였잖아? 여기서는 em.persist()를 호출하면 바로 DB와 통신하는 것 같은데?</p>
</blockquote>
<h3 id="1-identity-전략의-기본-동작"><strong>1. IDENTITY 전략의 기본 동작</strong></h3>
<p><code>IDENTITY</code> 전략은 <strong>데이터베이스가 기본 키 값을 생성하는 방식</strong>입니다. 보통 <code>AUTO_INCREMENT</code> 속성을 사용하는 데이터베이스에서 활용됩니다.<br>이 방식에서는 JPA가 기본 키 값을 <strong>데이터베이스에서 가져와야</strong> 엔티티 객체에 할당할 수 있습니다.</p>
<hr>
<h3 id="2-empersist-호출-시-동작"><strong>2. <code>em.persist()</code> 호출 시 동작</strong></h3>
<h4 id="일반적인-empersist-흐름">일반적인 <code>em.persist()</code> 흐름:</h4>
<ul>
<li><p><code>@GeneratedValue(strategy = GenerationType.IDENTITY)</code>가 아닌 경우:</p>
<ul>
<li>JPA는 엔티티를 영속성 컨텍스트에 저장하지만, 쓰기 지연 메커니즘에 따라 <strong>트랜잭션이 커밋될 때</strong> DB에 INSERT 쿼리를 보냅니다.</li>
<li>이때 ID 값은 JPA가 미리 생성(예: SEQUENCE 전략)하거나, DB의 <code>INSERT</code> 쿼리가 끝난 후 가져옵니다.</li>
</ul>
</li>
<li><p><strong>IDENTITY 전략인 경우</strong>:</p>
<ul>
<li><code>em.persist()</code> 호출 시, 즉시 <strong><code>INSERT</code> 쿼리를 실행하고 데이터베이스와 통신</strong>하여 ID 값을 조회합니다.</li>
<li><strong>쓰기 지연이 아닌 즉시 플러시</strong>가 이루어지는 이유는, 기본 키 값이 엔티티에 바로 할당되어야 하기 때문입니다.</li>
</ul>
</li>
</ul>
<h4 id="이유">이유:</h4>
<p>JPA는 엔티티의 식별자(기본 키)가 필요해야 다른 동작(연관 관계 설정, 영속성 컨텍스트 관리 등)을 수행할 수 있습니다.<br>IDENTITY 전략은 DB에서 기본 키 값을 생성하므로, <strong>영속성 컨텍스트에 엔티티를 저장하기 전에 DB와 통신</strong>이 필수적입니다.</p>
<hr>
<h3 id="3-언제-데이터베이스와-통신하나"><strong>3. 언제 데이터베이스와 통신하나?</strong></h3>
<h4 id="empersist-호출-시점"><strong><code>em.persist()</code> 호출 시점</strong>:</h4>
<ol>
<li><strong>IDENTITY 전략</strong>을 사용할 때는 <code>em.persist()</code>를 호출하는 순간, JPA는 바로 DB에 <code>INSERT</code> 쿼리를 실행합니다.<ul>
<li>이 과정에서 데이터베이스에 행이 삽입되고, 기본 키 값이 생성됩니다.</li>
<li>생성된 기본 키 값을 JPA가 가져와 엔티티의 식별자 필드에 할당합니다.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="4-정리-identity-전략의-특징"><strong>4. 정리: IDENTITY 전략의 특징</strong></h3>
<ul>
<li><p><strong>쓰기 지연 SQL 저장소</strong>:</p>
<ul>
<li>일반적으로 JPA는 쓰기 지연을 통해 SQL 쿼리를 한 번에 실행하지만, IDENTITY 전략은 <strong>즉시 플러시</strong>를 수행합니다.</li>
<li>즉, <code>em.persist()</code> 시점에 INSERT 쿼리가 실행되고, DB에서 생성된 ID 값을 가져옵니다.</li>
</ul>
</li>
<li><p><strong>데이터베이스와의 통신 시점</strong>:</p>
<ul>
<li><code>em.persist()</code>를 호출하면 JPA는 바로 DB와 통신해 INSERT를 수행하고 ID 값을 가져옵니다.</li>
<li>이후 <code>getId()</code>를 호출하면 이미 가져온 ID 값을 반환하므로 추가적인 DB 통신은 발생하지 않습니다.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="5-왜-identity-전략은-바로-플러시가-필요한가"><strong>5. 왜 IDENTITY 전략은 바로 플러시가 필요한가?</strong></h3>
<p>다른 전략(<code>SEQUENCE</code>, <code>TABLE</code>, <code>AUTO</code>)은 JPA가 직접 키를 관리하거나 미리 생성할 수 있지만,<br><strong>IDENTITY 전략은 DB에서 기본 키를 생성하므로, DB와 통신 없이는 키 값을 얻을 수 없습니다.</strong><br>이 때문에 <code>em.persist()</code> 단계에서 바로 플러시를 실행해 DB와 통신합니다.</p>
<hr>
<h3 id="6-결론"><strong>6. 결론</strong></h3>
<ol>
<li><strong>IDENTITY 전략을 사용하면 <code>em.persist()</code> 시점에 즉시 플러시가 이루어집니다.</strong><ul>
<li>INSERT 쿼리가 실행되고, DB에서 생성된 ID 값을 바로 조회합니다.</li>
<li>쓰기 지연 저장소에 쌓아두지 않습니다.</li>
</ul>
</li>
<li><strong>ID 값을 사용하는 시점에서 추가적인 DB 통신은 발생하지 않습니다.</strong><ul>
<li>이미 ID 값을 영속성 컨텍스트에서 가져왔기 때문입니다.</li>
</ul>
</li>
<li><code>persist()</code> 이후 아직 <code>flush()</code>되지 않은 다른 엔티티들은 여전히 쓰기 지연 저장소에 쌓여 있다가 트랜잭션 종료 시점에 처리됩니다.</li>
</ol>
<hr>
<h3 id="sequence-전략">SEQUENCE 전략</h3>
<p>데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트다. SEQUENCE 전략은 이 시퀀스를 사용해서 기본 키를 생성한다. 이 전략은 시퀀스를 지원하는 오라클, PostgreSQL, H2 데이터베이스에서 사용할 수 있다.</p>
<pre><code class="language-sql">//
CREATE TABLE BOARD (
    ID BIGINT NOT NULL PRIMARY KEY,
    DATA VARCHAR(255)
)

// 시퀀스 생성
CREATE SEQUENCE BOARD_SEQ START WITH 1 INCREMENT BY 1;</code></pre>
<p>아래는 시퀀스 매핑 코드이다.</p>
<pre><code class="language-java">@Entity
@SequenceGenerator(
    name = &quot;BOARD_SEQ_GENERATOR&quot;,
    sequenceName = &quot;BOARD_SEQ&quot;, //매핑할 데이터베이스 시퀀스 이름
    initialValue = 1, allocationSize = 1)
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE,
                    generator = &quot;BOARD_SEQ_GENERATOR&quot;)
    private Long id;
    //
}</code></pre>
<p>@SequenceGenerator를 사용하여 &quot;BOARD_SEQ_GENERATOR&quot;라는 시퀀스 생성기를 등록한 후 sequenceName으로 데이터베이스 시퀀스의 &quot;BOARD_SEQ&quot;과 매핑한다.
@GeneratedValue의 속성의 strategy를 GenerationType.SEQUENCE로 지정한 후 generator 속성으로 방금 등록한 &quot;BOARD_SEQ_GENERATOR&quot; 시퀀스 생성기를 선택한다. 이 후에 id의 식별자 값은 BOARD_SEQ_GENERATOR 시퀀스 생성기가 할당한다.</p>
<h3 id="sequence-전략-동작-원리">SEQUENCE 전략 동작 원리</h3>
<p>SEQUENCE 전략은 em.persit()를 호출하면 먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회한다. 조회한 식별자를 엔티티에 할당한 후 엔티티를 영속성 컨텍스트에 저장한다. 이 후 트랜잭션을 커밋할 때 플러시가 실행되어 엔티티를 데이터베이스에 저장한다.</p>
<hr>
<h2 id="table-전략">TABLE 전략</h2>
<p>TABLE 전략은 키 생성 전용 테이블을 만들어 이름과 값으로 사용할 컬럼을 만들어 마치 시퀀스처럼 사용하는 전략이다. 모든 데이터베이스 적용할 수 있다.</p>
<pre><code class="language-sql">
// TABLE 전략 키 생성 DDL
create table MY_SEQUENCES (
    sequence_name varchar(255) not null,
    next_val bigint,
    primary key (sequence_name)
 )</code></pre>
<p>sequence_name 컬럼을 시퀀스 이름으로 사용하고 next_val 컬럼을 시퀀스 값으로 사용한다. 컬럼 값은 변경할 수 있는데 위의 값이 기본 값이다.</p>
<pre><code class="language-java">// TABLE 전략 매핑 코드
@Entity
@TableGenerator(
    name = &quot;BOARD_SEQ_GENERATOR&quot;,
    table = &quot;MY_SEQUENCES&quot;,
    pkColumnValue = &quot;BOARD_SEQ&quot;, allocationSize = 1)
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE,
                    generator = &quot;BOARD_SEQ_GENERATOR&quot;)
    private Long id;
    ...
 }   </code></pre>
<p>@TableGenerator를 사용해서 BOARD_SEQ_GENERATOR라는 이름의 테이블 키 생성기를 등록했다. 이 후 table 속성에 방금 생성한 키 생성용 테이블(MY_SEQUENCES)을 매핑했다. 필드에는  @GeneratedValue(strategy = GenerationType.TABLE)을 지저하고 generator 속성에 등록한 테이블 키 생성기를 입력한다. 이 후 id 식별자 값은 BOARD_SEQ_GENERATOR 테이블 키 생성기가 할당한다.
MY_SEQUENCES 테이블에는 @TableGenerator에서 pkColumnValue 속성으로 지정한 BOARD_SEQ 라는 이름으로 레코드가 생성된다.</p>
<table>
<thead>
<tr>
<th>sequence_name</th>
<th>next_value</th>
</tr>
</thead>
<tbody><tr>
<td>BOARD_SEQ</td>
<td>2</td>
</tr>
</tbody></table>
<hr>
<h2 id="auto-전략">AUTO 전략</h2>
<p><strong>GenereationType.AUTO</strong>
선택한 데이터베이스 방언에 따라 자동으로 IDENTITY, SEQUENCE, TABLE을 사용하도록 설정하는 방법이다.
기본적으로 @GeneratedValue strategy 속성 값의 기본 값은 AUTO이므로 다음과 같이 사용해도 된다.</p>
<pre><code class="language-java">@Id @GeneratedValue  //@GeneratedValue(strategy = GenerationType.AUTO) 와 동일</code></pre>
<p>데이터베이스를 수정해도 코드를 수정할 필요가 없다는 점이 장점이다. SEQUENE나 TABLE 전략을 사용되기 위해서는 시퀀스나 키 생성용 테이블을 미리 만들어 둬야 하는데, 만약 스키마 자동 생성 기능을 사용한다면 하이버네이트가 기본 값을 사용해서 적절한 시퀀스나 키 생성용 테이블을 만들어 줄 것이다.</p>
<h3 id="권장하는-식별자-전략">권장하는 식별자 전략</h3>
<p>데이터베이스의 기본 키는 다음과 같은 규칙을 반드시 지켜야 한다.</p>
<ul>
<li>null 값을 허용해서는 안 된다.</li>
<li>유일한 값이어야 한다.</li>
<li>변해선 안 된다.</li>
</ul>
<p>테이블의 기본 키 규칙은 크게 <strong>자연 키(natural key)</strong>와 <strong>대리 키(surrogate key)</strong>가 있다.
자연 키는 비즈니스적으로 의미있는 키로 주민등록 번호, 이메일 등이 있다.
대리 키는 비즈니스와 관련이 없는 임의의 키를 뜻하며 오라클 시퀀스나 auto_increment 등으로 생성하는 키를 의미한다. 대체키라고도 불린다.</p>
<p><strong>비지니스는 언제든 바뀔 수 있으므로 대리 키를 권장한다.</strong></p>
<hr>
<h2 id="필드와-컬럼-매핑">필드와 컬럼 매핑</h2>
<p>앞에서 간단하게만 설명하고 넘어갔던 필드-컬럼 매핑 어노테이션에 대해 설명하겠다. 모든 부분을 세세히 설명하는게 아니라 중요하다고 생각 드는 부분만 정리할 예정이다.</p>
<h3 id="column">@Column</h3>
<ul>
<li>객체 필드를 테이블 컬럼에 매핑한다.</li>
</ul>
<p>@Column을 생략하면 자바 기본 타입일 때 nullable 속성에 예외가 있다.</p>
<pre><code class="language-java">int data1;
data1 integer not null    // 생성된 DDL

Integer data2;
data2 integer    // 생성된 DDL

@Column
int data3;
data3 integer    // 생성된 DDL</code></pre>
<p>위와 같이 @Column을 생략하면 자바의 기본 형은 null 허용이 안 되므로 not null이 붙는다. 기본형 타입의 @Column을 사용할거면 nullable속성을 false로 지정해야 함을 주의하자.</p>
<h3 id="enumerated">@Enumerated</h3>
<ul>
<li>자바의 enum 타입을 매핑할 때 사용한다.</li>
</ul>
<p><strong>EnumType.ORDINAL</strong> : enum의 순서를 데이터베이스에 저장
장점 : 데이터 베이스의 enum에 생성한 순서에 맞게 0,1 로 들어가 저장되는 데이터 크기가 작다.
단점 : 이미 저장된 enum 순서를 바꿀 수 없다.</p>
<p><strong>EnumType.STRING</strong> : enum의 이름을 데이터베이스에 저장
장점 : 저장된 enum의 순서를 바꾸거나 enum을 추가해도 안전하다.
단점 : 데이터베이스의 저장되는 데이터 크기가 ORDINAL에 비해 크다.</p>
<pre><code class="language-java">enum RoleType {
    ADMIN, MEMBER
}

//
@Enumerated(EnumType.STRING)
private RoleType roleType;</code></pre>
<p>ORDINAL은 ADMIN은 0으로 MEMBER는 1로 저장하는 반면, STRING은 ADMIN, MEMBER 그대로 저장한다.
(enum이 추가되거나 순서가 바뀔 수도 있으므로 STRING을 권장)</p>
<h3 id="temporal">@Temporal</h3>
<ul>
<li><p>날짜 타입을 매핑할 때 사용한다.</p>
<pre><code class="language-java">  @Temporal(TemporalType.DATE)
  private Date date;    // 날짜

  @Temporal(TemporalType.TIME)
  private Date time;    // 시간

  @Temporal(TemporalType.TIMESTAMP)
  private Date timestamp;    //날짜와 시간</code></pre>
<p>자바에는 Date가 있지만 데이터베이스에는 date, time, timestamp라는 세 가지 타입이 존재한다.
TemporalType을 설정하지 않으면 자바의 Date와 가장 유사한 timestamp가 지정된다. MySQL같은 경우에는 datetime을 이용하는데 데이터베이스 방언덕분에 코드를 수정하지 않아도 된다.</p>
</li>
</ul>
<h3 id="lob">@Lob</h3>
<ul>
<li>데이터베이스 BLOB, CLOB 타입과 매핑한다.</li>
</ul>
<h3 id="transient">@Transient</h3>
<ul>
<li>데이터베이스와 매핑하지 않을 필드에 사용한다.
데이터베이스에 저장/조회를 하지 않기 때문에 객체에 임시로 어떤 값을 담고 싶을 때 사용한다.</li>
</ul>
<h3 id="access">@Access</h3>
<ul>
<li>JPA가 엔티티에 접근하는 방식을 설정한다.</li>
</ul>
<p><strong>필드 접근</strong> : AccessType.FIELD로 지정하며 필드가 private이어도 접근할 수 있다. 필드에 @Id가 붙어있으면 @Access(AccessType.FIELD)로 지정한 것과 같아 생략할 수 있다.</p>
<p><strong>프로퍼티 접근</strong> : AccessType.PROPERTY로 지정하며 접근자(getter)를 사용한다. 다시 설명하면 getter 메서드를 이용해서 엔티티에 접근하는 것을 뜻한다.</p>
<p>아래는 해당 내용을 조금 더 찾아본 결과이다.</p>
<hr>
<h3 id="1-필드와-프로퍼티의-차이"><strong>1. 필드와 프로퍼티의 차이</strong></h3>
<h4 id="필드-기반-접근field-access"><strong>필드 기반 접근(Field Access)</strong></h4>
<ul>
<li><strong>설명</strong>: 엔티티 클래스의 필드(멤버 변수)에 직접 <code>@Id</code>와 같은 JPA 어노테이션을 붙이는 방식.</li>
<li><strong>특징</strong>:<ul>
<li>JPA는 <strong>필드에 직접 접근</strong>하여 데이터를 읽고 씁니다.</li>
<li>필드에 붙은 어노테이션을 기준으로 매핑을 처리합니다.</li>
<li>필드의 getter, setter는 무시됩니다.</li>
</ul>
</li>
</ul>
<h5 id="예제-2">예제:</h5>
<pre><code class="language-java">@Entity
public class Member {
    @Id // 필드에 직접 어노테이션 부착
    private Long id;

    private String name;

    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;
    }
}</code></pre>
<hr>
<h4 id="프로퍼티-기반-접근property-access"><strong>프로퍼티 기반 접근(Property Access)</strong></h4>
<ul>
<li><strong>설명</strong>: 엔티티 클래스의 <strong>getter 메서드</strong> 또는 <strong>setter 메서드</strong>에 JPA 어노테이션을 붙이는 방식.</li>
<li><strong>특징</strong>:<ul>
<li>JPA는 <strong>getter 메서드를 통해</strong> 데이터를 읽고, setter 메서드를 통해 데이터를 씁니다.</li>
<li>어노테이션은 필드가 아니라 <strong>메서드에 적용</strong>됩니다.</li>
<li>필드는 private이어도 상관없습니다.</li>
</ul>
</li>
</ul>
<h5 id="예제-3">예제:</h5>
<pre><code class="language-java">@Entity
public class Member {
    private Long id;

    private String name;

    @Id // getter 메서드에 어노테이션 부착
    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;
    }
}</code></pre>
<hr>
<h3 id="2-jpa에서-필드와-프로퍼티가-다른-이유"><strong>2. JPA에서 필드와 프로퍼티가 다른 이유</strong></h3>
<h4 id="jpa의-동작-방식">JPA의 동작 방식</h4>
<ul>
<li>JPA는 엔티티의 데이터에 접근할 때, <strong>어노테이션이 어디에 붙었는지</strong>를 기준으로 필드 기반인지 프로퍼티 기반인지 결정합니다.</li>
<li><strong>필드 기반 접근</strong>:<ul>
<li>데이터에 직접 접근 (<code>field.setAccessible(true)</code>를 사용하여 private 필드도 접근 가능).</li>
</ul>
</li>
<li><strong>프로퍼티 기반 접근</strong>:<ul>
<li>데이터에 getter/setter 메서드를 통해 접근.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="3-필드-vs-프로퍼티-비교"><strong>3. 필드 vs. 프로퍼티 비교</strong></h3>
<table>
<thead>
<tr>
<th><strong>구분</strong></th>
<th><strong>필드(Field) 접근</strong></th>
<th><strong>프로퍼티(Property) 접근</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>어노테이션 위치</strong></td>
<td>필드에 직접 붙임 (<code>@Id</code>, <code>@Column</code>)</td>
<td>getter/setter 메서드에 붙임</td>
</tr>
<tr>
<td><strong>데이터 접근 방식</strong></td>
<td>필드에 직접 접근</td>
<td>getter/setter를 통해 접근</td>
</tr>
<tr>
<td><strong>캡슐화</strong></td>
<td>필드에 직접 접근하므로 캡슐화 약함</td>
<td>getter/setter 사용으로 캡슐화 유지</td>
</tr>
<tr>
<td><strong>JPA의 기본 동작</strong></td>
<td>필드에 어노테이션이 있으면 필드 접근</td>
<td>getter에 어노테이션이 있으면 프로퍼티 접근</td>
</tr>
<tr>
<td><strong>장점</strong></td>
<td>간단하고 명료</td>
<td>캡슐화가 유지됨</td>
</tr>
<tr>
<td><strong>단점</strong></td>
<td>캡슐화가 깨질 수 있음</td>
<td>코드가 길어질 수 있음</td>
</tr>
</tbody></table>
<hr>
<h3 id="4-실제-사용-시-주의점"><strong>4. 실제 사용 시 주의점</strong></h3>
<ol>
<li><p><strong>필드와 프로퍼티를 혼용하지 말 것</strong>:</p>
<ul>
<li>JPA는 엔티티 클래스에서 <strong>필드 기반</strong> 또는 <strong>프로퍼티 기반</strong> 중 하나만 선택해서 사용합니다.</li>
<li>같은 클래스에서 필드와 프로퍼티 접근 방식을 혼용하면 예기치 않은 동작이 발생할 수 있습니다.</li>
</ul>
</li>
<li><p><strong>필드 기반 접근을 권장</strong>:</p>
<ul>
<li>JPA 구현체는 필드 기반 접근을 더 효율적으로 처리할 수 있습니다.</li>
<li>getter/setter를 사용하지 않으므로 코드가 간결해집니다.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="5-결론-1"><strong>5. 결론</strong></h3>
<ul>
<li><strong>필드(field)</strong>: 클래스의 멤버 변수.</li>
<li><strong>프로퍼티(property)</strong>: 클래스의 getter/setter 메서드.</li>
<li>JPA에서 <code>@Id</code>를 어디에 붙이는지에 따라 JPA의 데이터 접근 방식이 달라집니다:<ul>
<li>필드에 붙이면 필드 기반 접근.</li>
<li>getter에 붙이면 프로퍼티 기반 접근.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>