<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hj_.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 26 Mar 2024 04:42:27 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hj_.log</title>
            <url>https://velog.velcdn.com/images/hj_/profile/da5f6a89-7431-4e5b-a796-f100b573227e/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hj_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hj_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[CI/CD] Docker 와 Github Actions 를 활용한 CI/CD 환경 구축 - (4) Github Actions CD/CD 와 Docker 연결]]></title>
            <link>https://velog.io/@hj_/CICD-Docker-%EC%99%80-Github-Actions-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-CICD-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-4-Github-Actions-CDCD-%EC%99%80-Docker-%EC%97%B0%EA%B2%B0</link>
            <guid>https://velog.io/@hj_/CICD-Docker-%EC%99%80-Github-Actions-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-CICD-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-4-Github-Actions-CDCD-%EC%99%80-Docker-%EC%97%B0%EA%B2%B0</guid>
            <pubDate>Tue, 26 Mar 2024 04:42:27 GMT</pubDate>
            <description><![CDATA[<h1 id="github-actions-살펴보기">Github Actions 살펴보기</h1>
<blockquote>
<p><a href="https://www.youtube.com/watch?v=iLqGzEkusIw&t=262s">드림코딩의 Github Actions</a>를 참고해서 작성하였습니다.</p>
</blockquote>
<p>Github Actions 는 특정한 <strong>이벤트</strong>가 발생했을 때 내가 원하는 <strong>일</strong>을 <strong>자동으로 수행</strong>할 수 있도록 만들어주는 도구입니다.</p>
<h2 id="주요-개념">주요 개념</h2>
<h4 id="-events-">[ Events ]</h4>
<p><strong>어떤 일이 발생했을 때</strong> 수행할 것인지를 지정합니다. 이때 이벤트에는 깃허브에서 발생하는 대부분의 이벤트를 지정할 수 있습니다. 예를 들어 특정 브랜치에 커밋을 하거나 머지를 하는 등의 이벤트를 지정할 수 있습니다.</p>
<h4 id="-workflows-">[ Workflows ]</h4>
<p>특정한 이벤트가 발생했을 때 <strong>어떤 일을 수행할 지</strong>를 지정합니다. Workflows 안에는 Job 이 있는데 하나의 workflow 안에는 하나 이상의 job 을 가질 수 있습니다.</p>
<p><code>.github/workflows</code> 디렉토리에 yaml 파일을 만들고 여기에 작성합니다. 파일명은 아무거나 지정해도 됩니다. </p>
<h4 id="-jobs-">[ Jobs ]</h4>
<p>Workflows 의 Job 은 동시에 병렬적으로 수행됩니다. 반대로 순차적으로 진행하도록 지정하는 것도 가능합니다.</p>
<p>하나의 Job 안에는 어떤 순서로 Job 이 실행되어야 하는지 Step 을 지정할 수 있습니다. 쉘 스크립트를 사용해서 어떤 step 을 해야 하는지 명시해줄 수 있습니다. 또 actions 를 사용할 수 있습니다.</p>
<h4 id="-actions-">[ Actions ]</h4>
<p>step 에는 직접만든 Action 을 사용할 수 있지만 Github Actions 에는 우리가 재사용할 수 있는, 공개적으로 오픈된 액션들이 존재하며, <code>actions/</code> 으로 시작합니다.</p>
<h4 id="-runners-">[ Runners ]</h4>
<p>지정한 Job 들을 실행하는 것이 바로 Runner( VM ) 입니다. 각각의 Job 들은 독립적인 각각의 Runner 라는 컨터이너에서 실행됩니다. </p>
<br>


<h2 id="yml-파일-형태">yml 파일 형태</h2>
<pre><code class="language-yml">name: [workflow 이름]
on: [이벤트 지정]
jobs: 
    [job 이름]:
        runs-on: [어떤 Runner(VM) 를 사용할 것인지]
        steps: [어떤 순서대로 job 을 실행해야 하는지 step 을 지정]  
            ## step 1
            - uses:
            ## step 2
            - name: [step 이름 지정]
              uses:
              with:
            ## step 3
            - name: [step 이름 지정]  
              run: 
            ...</code></pre>
<p><br><br></p>
<h1 id="github-actions-사용하기">Github Actions 사용하기</h1>
<h2 id="1-github-secrets-환경변수--설정">1. Github Secrets( 환경변수 ) 설정</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/113e83c8-3bbd-4811-84de-8f42d01b7881/image.PNG" alt=""></p>
<p>Settings ➜ Secrets and Variables ➜ Actions 에 들어가서 Github Actions 에 쓰일 환경변수를 지정합니다.</p>
<br>



<h2 id="2-workflow-생성">2. Workflow 생성</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/d34383f2-3b34-414b-a791-4478302df969/image.PNG" alt=""></p>
<p>Actions ➜ Java with Gradle 을 선택하면 어느 정도 작성되어 있는 yml 파일을 생성할 수 있습니다. 상단의 set up a workflow yourself 를 선택하면 처음부터 본인이 작성할 수 있습니다.</p>
<br>



<h2 id="3-workflow-작성">3. Workflow 작성</h2>
<pre><code class="language-yml"># Workflow 이름
name: CI/CD with Gradle

# Event 지정
on:
  push:
    branches: [ &quot;main&quot; ]

# Workflow 내 Job 을 정의
jobs:
  # Job 의 이름
  CI-CD-build:
    # Runner 환경 정의
    runs-on: ubuntu-latest

    # Step 정의
    steps:
    # 정의된 Actions 의 체크아웃 사용
    - uses: actions/checkout@v4

    # 1. jdk 세팅
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: &#39;17&#39;
        distribution: &#39;temurin&#39;

    # 2. Gradle Caching
    - name: Gradle Caching
      uses: actions/cache@v3
      with:
        path: |
          ~/.gradle/caches
          ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-${{ hashFiles(&#39;**/*.gradle*&#39;, &#39;**/gradle-wrapper.properties&#39;) }}
        restore-keys: |
          ${{ runner.os }}-gradle-

    # 3. craete firebase key
    - name: create firebase key
      run: |
        cd ./src/main/resources
        ls -a .
        touch ./firebaseKey.json
        echo &quot;${{ secrets.FIREBASE_KEY }}&quot; &gt; ./firebaseKey.json
      shell: bash

    # 4. Gradle build
    - name: Build with Gradle Wrapper
      run: |
          chmod +x ./gradlew
          ./gradlew clean build -x test

    # 5. docker build &amp; push
    - name: docker build and push
      run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker build -f Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/vitalroutes-spring ./
          docker push ${{ secrets.DOCKER_USERNAME }}/vitalroutes-spring

    # 6. Docker Compose Start
    - name: SSH into Ubuntu Server
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.SERVER_HOST }}
        username: ${{ secrets.SERVER_USERNAME }}
        password: ${{ secrets.SERVER_PASSWORD }}
        script: |
          docker-compose stop
          docker rm -f $(docker ps -qa)
          docker pull ${{ secrets.DOCKER_USERNAME }}/vitalroutes-spring
          docker-compose up -d</code></pre>
<br>


<h3 id="workflow-이름">workflow 이름</h3>
<pre><code class="language-yml"># Workflow 이름
name: CI/CD with Gradle</code></pre>
<p><img src="https://velog.velcdn.com/images/hj_/post/1a4a469a-041d-49d8-a867-fc70ec56df19/image.PNG" alt=""></p>
<p>workflow 의 이름을 지정하면 Actions 에 들어갔을 때 해당 이름으로 workflow 가 생성된 것을 확인할 수 있습니다.</p>
<br>



<h3 id="jobs-이름">Jobs 이름</h3>
<pre><code class="language-yml"># Event 지정
on:
  push:
    branches: [ &quot;main&quot; ]

# Workflow 내 Job 을 정의
jobs:
  # Job 의 이름
  CI-CD-build:</code></pre>
<p><img src="https://velog.velcdn.com/images/hj_/post/47e169f0-de61-449e-97df-ca6ae2b3694d/image.PNG" alt=""></p>
<p>수행된 workflow 를 들어가면 Jobs 의 이름이 지정된 것을 볼 수 있고, 우측에 보면 on push 라고 되어 있습니다. </p>
<p>이벤트를 <strong>main 브랜치에 push</strong> 한 경우에 실행되도록 했기 때문에 정상적으로 실행된 것을 확인할 수 있습니다.</p>
<br>



<h3 id="runner-환경-정의">Runner 환경 정의</h3>
<pre><code class="language-yml"># Workflow 내 Job 을 정의
jobs:
  # Job 의 이름
  CI-CD-build:
    # Runner 환경 정의
    runs-on: ubuntu-latest</code></pre>
<p><code>runs-on</code> 으로 runner 가 수행될 환경을 지정합니다. 저는 우분투 서버를 사용하기 때문에 ubuntu 로 지정하였습니다.</p>
<br>


<h3 id="step-정의">step 정의</h3>
<pre><code class="language-yml">steps:
    # 정의된 Actions 의 체크아웃 사용
    - uses: actions/checkout@v4

    # 1. jdk 세팅
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: &#39;17&#39;
        distribution: &#39;temurin&#39;</code></pre>
<p><code>steps</code> 를 이용해 job 안에서 수행할 작업들을 지정합니다. 먼저 미리 정의된 action 을 활용하기 위해 <code>actions</code> 를 이용해 체크아웃을 합니다.</p>
<p>그 후 runner 는 각각의 독립적인 환경이기 때문에 JDK 를 setup 합니다. </p>
<br>

<h3 id="gradle-캐싱">Gradle 캐싱</h3>
<pre><code class="language-yml"># 2. Gradle Caching
    - name: Gradle Caching
      uses: actions/cache@v3
      with:
        path: |
          ~/.gradle/caches
          ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-${{ hashFiles(&#39;**/*.gradle*&#39;, &#39;**/gradle-wrapper.properties&#39;) }}
        restore-keys: |
          ${{ runner.os }}-gradle-</code></pre>
<p>gradle 캐싱을 수행합니다. 해당 step을 작성하면 빌드가 조금 빠르다고 합니다.</p>
<br>


<h3 id="firebasekey-생성">firebaseKey 생성</h3>
<pre><code class="language-yml"># 3. craete firebase key
    - name: create firebase key
      run: |
        cd ./src/main/resources
        ls -a .
        touch ./firebaseKey.json
        echo &quot;${{ secrets.FIREBASE_KEY }}&quot; &gt; ./firebaseKey.json
      shell: bash</code></pre>
<p>기존에는 로컬에서 테스트를 했기 때문에 <code>main/resources</code> 에 firebaseKey 파일이 있었는데 github 리포지토리에는 존재하지 않기 때문에 해당 파일을 생성해줍니다.</p>
<p>이때 Github Secret 에 지정한 환경변수인 FIREBASE_KEY 를 참조하도록 하였습니다. FIREBASE_KEY 는 파일의 내용을 붙여넣었으며, <strong><code>&quot;</code> 를 인식하기 위해서는 <code>\&quot;</code> 를 사용해야 합니다.</strong></p>
<pre><code class="language-json">// 기존 파일 내용
{
  &quot;type&quot;: &quot;service_account&quot;,
  &quot;project_id&quot;: &quot;...&quot;,
}

// secret 에 작성한 내용
{
  \&quot;type\&quot;: \&quot;service_account\&quot;,
  \&quot;project_id\&quot;: \&quot;...\&quot;,
}</code></pre>
<br>


<blockquote>
<p>COPY src/main/resources/firebaseKey.json firebaseKey.json</p>
</blockquote>
<p>이 과정을 거치면 /main/resources 에 파일이 생성되며, 기존에 dockerfile 에 작성한 위의 명령어가 수행되어 도커 이미지 생성 시에 정상적으로 파일이 복사됩니다.</p>
<br>


<h3 id="gradle-build">gradle build</h3>
<pre><code class="language-yml"># 4. Gradle build
    - name: Build with Gradle Wrapper
      run: |
          chmod +x ./gradlew
          ./gradlew clean build -x test</code></pre>
<p>gradle 빌드를 수행합니다. 이때 chmod 를 통해 실행 권한을 부여해야 합니다.</p>
<br>


<h3 id="도커-이미지-build--push">도커 이미지 build &amp; push</h3>
<pre><code class="language-yml"># 5. docker build &amp; push
    - name: docker build and push
      run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker build -f Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/vitalroutes-spring ./
          docker push ${{ secrets.DOCKER_USERNAME }}/vitalroutes-spring</code></pre>
<p>docker image 를 build 하고 push 하는 작업입니다. 해당 명령어에 필요한 USERNAME 과 PASSWORD 는 github secret 에 정의되어야 합니다.</p>
<br>


<h3 id="docker-compose-실행">docker-compose 실행</h3>
<pre><code class="language-yml"> # 6. Docker Compose Start
    - name: SSH into Ubuntu Server
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.SERVER_HOST }}
        username: ${{ secrets.SERVER_USERNAME }}
        password: ${{ secrets.SERVER_PASSWORD }}
        script: |
          docker-compose stop
          docker rm -f $(docker ps -qa)
          docker pull ${{ secrets.DOCKER_USERNAME }}/vitalroutes-spring
          docker-compose up -d</code></pre>
<p>그 후 서버에 접속하여 새로운 이미지를 pull 받고, docker-compose 를 실행하는 과정입니다. 이때 다른 사람의 정의한 action 을 사용하도록 <code>uses</code> 에 작성하였습니다.</p>
<p>먼저 서버의 정보를 <code>with</code> 에 작성하고, 서버에서 실행할 명령어들을 <code>script</code> 에 작성합니다.</p>
<blockquote>
<p>docker-compose logs -f -t</p>
</blockquote>
<p>그 후 서버에 접속해서 위 명령어를 입력하면 로그를 확인할 수 있습니다.</p>
<br>



<h2 id="참고-docker-권한-오류">참고&gt; docker 권한 오류</h2>
<blockquote>
<p>err: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get &quot;http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/json?all=1&quot;: dial unix /var/run/docker.sock: </p>
</blockquote>
<p>위처럼 권한 오류가 뜨는 경우에는 아래 명령어를 통해 docker.sock 의 권한을 수정하면 됩니다.</p>
<blockquote>
<p>sudo chmod 666 /var/run/docker.sock</p>
</blockquote>
<p><br><br></p>
<h1 id="최종-구조">최종 구조</h1>
<p><img src="https://velog.velcdn.com/images/hj_/post/f805b979-41f7-43c5-b30c-66b44eebffdd/image.PNG" alt=""></p>
<p>지금까지 총 4단계에 걸쳐 서버 구축, Docker-Compose, NGINX 및 HTTPS 설정, Github Actions 를 진행하였고, 자동화된 CI/CD 구축을 완료하였습니다. 최종적인 구조는 위와 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CI/CD] Docker 와 Github Actions 를 활용한 CI/CD 환경 구축 - (3) HTTPS 설정과 NGINX]]></title>
            <link>https://velog.io/@hj_/CICD-Docker-%EC%99%80-Github-Actions-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-CICD-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-3-HTTPS-%EC%84%A4%EC%A0%95%EA%B3%BC-NGINX</link>
            <guid>https://velog.io/@hj_/CICD-Docker-%EC%99%80-Github-Actions-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-CICD-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-3-HTTPS-%EC%84%A4%EC%A0%95%EA%B3%BC-NGINX</guid>
            <pubDate>Wed, 20 Mar 2024 15:57:49 GMT</pubDate>
            <description><![CDATA[<h1 id="암호화-살펴보기">암호화 살펴보기</h1>
<h2 id="https">HTTPS</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/16f1946d-96d9-4744-b738-f1f1521b44ee/image.png" alt=""></p>
<p>브라우저에서 서버로 데이터를 전송할 때 HTTP 를 사용하면 입력한 그대로 누구든 알아볼 수 있게 전달됩니다. 만약 로그인 정보가 위처럼 전달되면 보안 상 좋지 않습니다. </p>
<p>그래서 HTTPS 를 사용하는데 HTTPS 는 위처럼 <strong>서버로 전달하는 정보들을 암호화</strong>해서 보내기 때문에 다른 사람이 보더라도 알아볼 수 없습니다. 이때 <strong>정보를 암호화할 때는 대칭키 암호화 방식</strong>을 사용하는데 이는 아래에서 살펴보도록 하겠습니다.</p>
<br>



<h2 id="대칭키-암호화">대칭키 암호화</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/b1cf3a0a-49d3-4f4a-a8dc-7df9771a56b6/image.png" alt=""></p>
<p>대칭키 암호화는 <strong>암호키와 복호화키가 동일하며, 대칭키는 통신 주체들 사이마다 필요</strong>합니다. 서버와 브라우저가 동일한 대칭키를 가지고 있어야 브라우저에서 대칭키로 암호화 한 정보를 서버에서 복호화할 수 있습니다.</p>
<p>만약 대칭키가 노출된다면 대칭키를 가진 누군가가 암호화 된 정보를 볼 수 있게 되기 때문에 <strong>통신 당사자 간 대칭키를 안전하게 공유</strong>하는데 많은 노력이 필요한데 이를 해결할 수 있는 것이 바로 DH( Diffie-Hellman ) 알고리즘이며, <strong>안전하게 대칭키를 생성하기 위해 공개키 암호화 방식을 사용</strong>합니다.</p>
<p>아래에서 공개키 암호화 방식과 인증서를 살펴본 뒤, 공개키 암호화 방식을 이용해서 대칭키를 생성하는 방법에 대해 알아보도록 하겠습니다.</p>
<br>




<h2 id="공개키-암호화">공개키 암호화</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/09f77b26-d0db-42c2-b164-bb40251899c6/image.png" alt=""></p>
<p>공개키 암호화 방식에는 암호화, 복호화에 두 가지 키가 사용됩니다. 하나는 공개키이고 다른 하나는 개인키인데 만약 <strong>공개키로 암호화하면 개인키로 복호화</strong>가 가능하고, <strong>개인키로 암호화하면 공개키로 복호화</strong>할 수 있습니다.</p>
<p>예를 들어, 브라우저가 서버의 공개키로 암호화해서 전송하면 서버는 자신의 개인키로 이를 복호화해서 정보를 확인할 수 있게 됩니다.</p>
<p>추가적으로 <strong>개인키로 암호화하고, 자신의 공개키로 복호화</strong>할 수 있게 하는 것은 보통 <strong>송신자가 누구인지에 대한 정보가 필요할 때 사용</strong>하는데 대표적인 예시로 <strong>디지털 서명</strong>이 있습니다.</p>
<br>


<h2 id="인증서">인증서</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/55b05483-8b6f-4116-9836-11d2202ba78c/image.png" alt=""></p>
<p>신뢰할 수 있는 기관에서 서버의 공개키를 검증해준다면 이를 기준으로 안전하게 서버를 이용할 수 있게 됩니다. 이를 위해서 사이트는 인증서가 필요합니다. 인증서란 인증기관( CA ) 에서 사이트에 발급하는 문서입니다.</p>
<p>인증서를 발급받기 위해 사이트에서 인증기관에 사이트 정보와 사이트의 공개키를 전달합니다. 인증 기관에서는 전달 받은 데이터를 검증하고, <strong>검증이 완료되었다면 인증기관의 개인키로 서명을 하고 인증서를 생성</strong>합니다. 생성된 인증서는 사이트에 전달됩니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/0de38604-4898-4e4d-a91a-7adc576f4508/image.png" alt=""></p>
<p>브라우저가 사이트에 접속하게 되면 사이트는 핸드쉐이크 과정을 거치면서 <strong>자신이 신뢰할 수 있는 사이트임을 증명하기 위해 브라우저에 사이트의 인증서를 전달</strong>합니다. 브라우저들은 이 CA 목록들이 내장되어 있고, <strong>사이트로부터 전달받은 인증서를 CA 의 공개키로 복호화하여</strong> 진짜인지 가짜인지 판별할 수 있습니다.</p>
<br>



<h2 id="공개키-방식으로-대칭키-생성하기">공개키 방식으로 대칭키 생성하기</h2>
<p>브라우저가 사이트에 접속할 때 핸드 쉐이크 과정을 거치면서 인증서를 전달한다고 했습니다. 인증서를 전달 받은 후에는 공개키 방식을 이용해 통신에 사용할 대칭키를 생성하게 되는데 이를 간단하게만 알아보도록 하겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/526fdb84-439c-44e8-9bcf-a344ba7edd92/image.png" alt=""></p>
<ol>
<li><p>먼저 브라우저에서 임의로 난수를 생성해서 서버로 전달합니다.</p>
</li>
<li><p>서버도 임의로 난수를 생성한 뒤, 인증서와 함께 난수를 전달합니다.</p>
</li>
<li><p><strong>서버에서 전달받은 인증서를 CA 의 공개키로 복호화합니다. 복호화를 하면 서버의 공개키가 나오게 됩니다.</strong></p>
</li>
<li><p><strong>브라우저는 자신이 생성한 난수와 서버가 전달한 난수를 조합해서 대칭키를 생성하고, 이를 서버의 공개키로 암호화하여 전달합니다.</strong></p>
</li>
<li><p>서버는 자신의 개인키로 암호화된 대칭키를 복호화합니다.</p>
</li>
<li><p>핸드쉐이크가 종료되고, 생성된 대칭키로 HTTPS 통신이 시작됩니다.</p>
</li>
</ol>
<p><br><br></p>
<h1 id="https-설정하기">HTTPS 설정하기</h1>
<h2 id="1-인증서-발급">1. 인증서 발급</h2>
<p>Let&#39;s Encrypt 라는 비영리 기관을 통해 무료로 인증서를 발급받을 수 있습니다. 3가지 방식으로 할 수 있다고 하는데 저는 standalone 방식을 사용하였습니다.</p>
<pre><code>sudo certbot certonly --standalone -d {도메인}</code></pre><p>위 명령어를 입력하면 이메일 입력하라 동의하라고 뜨는데 이미 이전에 다른 방식을 시도하면서 입력해서 그런가 뜨지 않았습니다. 명령어 수행 결과로 <code>/etc/letsencrypt/live/{도메인명}</code> 위치에 pem 파일이 생성됩니다.</p>
<br>



<h2 id="2-nginx-설정">2. NGINX 설정</h2>
<p><code>docker-compose.yml</code> 이 위치한 곳에서 data/nginx 디렉터리를 만들고 내부에 app.conf 파일을 작성합니다.</p>
<pre><code>server {
        listen 80;
        server_name {도메인명};
        location / {
                return 301 https://$host$request_uri;
        }

}

server {
        listen 443 ssl;
        server_name {도메인명};

        ssl_certificate /etc/letsencrypt/live/{도메인명};/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/{도메인명};/privkey.pem;

        location / {
                proxy_pass  http://application:8080;
                proxy_set_header   Host                    $http_host;
                proxy_set_header   X-Real-IP               $remote_addr;
                proxy_set_header   X-Forwarded-For         $proxy_add_x_forwarded_for;
                proxy_set_header   X-Forwarded-Proto       $scheme;
        }
}</code></pre><p>여기서 application 은 <code>docker-compose.yml</code> 에 작성한 스프링 애플리케이션의 이름입니다.</p>
<p><br><br></p>
<h2 id="3-docker-composeyml-작성">3. docker-compose.yml 작성</h2>
<pre><code>services:
  nginx:
    container_name: nginx
    image: nginx
    volumes:
      - ./data/nginx:/etc/nginx/conf.d
      - /etc/letsencrypt:/etc/letsencrypt
    ports:
      - 80:80
      - 443:443
    depends_on:
      - application</code></pre><p>작성한 내용 app.conf 를 nginx 의 <code>/etc/nginx/conf.d</code> 에 위치할 수 있도록 volumes 에 작성합니다. 또 인증 후 생성된 키를 연결하기 위해 <code>/etc/letsencrypt</code> 도 추가합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CI/CD] Docker 와 Github Actions 를 활용한 CI/CD 환경 구축 - (2) Docker 를 활용한 배포]]></title>
            <link>https://velog.io/@hj_/CICD-Docker-%EC%99%80-Github-Actions-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-CICD-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-2-Docker-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@hj_/CICD-Docker-%EC%99%80-Github-Actions-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-CICD-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-2-Docker-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Wed, 20 Mar 2024 08:48:11 GMT</pubDate>
            <description><![CDATA[<h1 id="도커-살펴보기">도커 살펴보기</h1>
<blockquote>
<p>해당 내용은 <a href="https://www.youtube.com/watch?v=LXJhA3VWXFA">드림코딩 도커</a> 영상을 보고 작성하였습니다.</p>
</blockquote>
<p>스프링은 애플리케이션 코드와 함께 여러 dependencies 로 이루어져 있고, 이를 실행하기 위해서는 JDK 가 필요합니다.</p>
<p><strong>도커는 컨테이너 안에 애플리케이션 실행에 필요한 모든 것을 담고 있습니다. 이를 활용하여 어떤 환경에서도 애플리케이션을 실행할 수 있도록 도와줍니다.</strong></p>
<br>



<h2 id="vm-vs-container">VM vs Container</h2>
<h3 id="virtual-machine">Virtual Machine</h3>
<p><img src="https://velog.velcdn.com/images/hj_/post/cc77b574-e7d7-4084-989c-6fc44b97c43e/image.PNG" alt=""></p>
<p>infrastructure 위에 VM ware 같은 Hypervisor 소프트웨어를 사용하여 각각의 가상 머신을 만들 수 있습니다. 가상머신은 PC 의 운영체제 위해 가상머신 각각의 운영체제를 포함하고 있기 때문에 굉장히 무겁고 infrastructure 의 리소스를 많이 잡아먹게 됩니다.</p>
<h3 id="container">Container</h3>
<p><img src="https://velog.velcdn.com/images/hj_/post/07620de4-c69c-4357-9687-d61fb67a171a/image.PNG" alt=""></p>
<p>이런 가상머신에서 조금 경량화된 컨셉이 바로 컨테이너입니다. 컨테이너는 각각의 OS 설치 없이 호스트 OS 에 Container Engine 이라는 소프트웨어를 설치하면 개별적인 컨테이너를 만들어서 각 애플리케이션을 고립된 환경에서 구동할 수 있게 해줍니다. Container Engine 중에서 가장 많이 사용되는 것이 바로 Docker 입니다.</p>
<br>


<h2 id="docker">Docker</h2>
<p>도커를 이용할 때는 <strong>컨테이너를 만들고, 배포하고, 컨테이너를 실행</strong>하는 과정을 거치게 되는데 <strong>컨테이너를 만들기 위해서는 아래 3가지의 과정이 필요</strong>합니다.</p>
<h3 id="1-dockerfile-을-작성합니다">1. Dockerfile 을 작성합니다.</h3>
<p>Dockerfile 에는 컨테이너를 어떻게 만들어야 하는지를 작성합니다. </p>
<p>도커 파일에는 애플리케이션을 구동하기 위해 필요한 jar 와 같은 파일, 필요한 외부 라이브러리, 환경 변수를 작성할 수 있으며, 어떻게 실행시켜야 하는지 스크립트도 작성할 수 있습니다.</p>
<h3 id="2-image-를-생성합니다">2. Image 를 생성합니다.</h3>
<p>Image 는 작성한 도커 파일을 이용해서 만들 수 있으며 Image 는 불변의 상태입니다.</p>
<h3 id="3-컨테이너를-구동합니다">3. 컨테이너를 구동합니다.</h3>
<p>Image 를 고립된 환경에서 실행할 수 있도록 해주는 것이 컨테이너입니다. 컨테이너 안에서 애플리케이션이 동작한다고 생각하면 됩니다. 또 하나의 Image 로 여러 개의 컨테이너를 구동할 수 있습니다.</p>
<p><strong>로컬에서 생성한 Image 를 Docker Hub 에 push 하면, 서버에서 pull 을 받아, Image 를 통해 컨테이너를 생성하고 구동할 수 있습니다.</strong></p>
<br>

<h2 id="docker-compose">Docker Compose</h2>
<p>컨테이너를 실행할 때 각각의 image 를 push 하고, pull 받아서 실행하는데 만약 여러 개의 image 를 한 번에 실행해야 한다면 해당 과정을 여러 번 반복해야 합니다.</p>
<p>이러한 작업을 편하게 할 수 있도록 도와주는 것이 바로 Docker Compose 입니다. <code>docker-compose.yml</code> 설정 파일을 통해 여러 개의 도커 컨테이너를 정의하고 실행할 수 있으며, 컨테이너 간 종속성을 설정할 수 있습니다.</p>
<p><br><br></p>
<h1 id="도커로-스프링부트-배포하기">도커로 스프링부트 배포하기</h1>
<h2 id="1-도커-로그인">1. 도커 로그인</h2>
<pre><code>docker login -u [사용자명]</code></pre><br>


<h2 id="2-스프링-빌드">2. 스프링 빌드</h2>
<pre><code>Gradle ➜ Tasks ➜ build ➜ bootJar 실행</code></pre><p>이 과정을 수행하면 <code>build/libs</code> 에 jar 파일이 생성됩니다.</p>
<br>


<h2 id="3-dockerfile-생성">3. Dockerfile 생성</h2>
<pre><code>FROM openjdk:17

ARG JAR_FILE=build/libs/*.jar

COPY ${JAR_FILE} app.jar
COPY src/main/resources/firebaseKey.json firebaseKey.json

ENTRYPOINT [&quot;java&quot;,&quot;-jar&quot;,&quot;/app.jar&quot;]</code></pre><p>스프링부트 프로젝트 루트 디렉터리에 <code>Dockerfile</code> 을 생성합니다.</p>
<p>이때 저는 firebaseKey 가 담긴 json 파일까지 함께 실행되어야 하기 때문에 COPY 명령어로 파일도 추가해주었습니다.</p>
<blockquote>
<pre><code>COPY [파일위치] [도커 내 파일위치]</code></pre></blockquote>
<p>COPY 는 위와 같은 형태로 작성하는데, 위처럼 작성한 파일의 경로는 <code>/firebaseKey.json</code> 이 됩니다.</p>
<br>




<h2 id="4-도커-이미지-생성">4. 도커 이미지 생성</h2>
<pre><code>docker build -t [사용자명]/[이미지명] [도커파일위치]

ex&gt; docker build -t test/testDemo ./</code></pre><p>터미널을 열고 Dockerfile 이 위치한 곳까지 이동합니다. 그 후 [도커파일위치] 에 <code>./</code> 을 입력합니다.</p>
<br>


<h2 id="5-생성된-이미지-확인">5. 생성된 이미지 확인</h2>
<pre><code>docker images</code></pre><br>


<h2 id="6-도커-허브에-이미지-업로드">6. 도커 허브에 이미지 업로드</h2>
<pre><code>docker push [사용자명]/[이미지명]

ex&gt; docker push test/testDemo</code></pre><br>

<h2 id="7-도커-이미지-pull">7. 도커 이미지 pull</h2>
<pre><code>sudo docker pull [사용자명]/[이미지명]</code></pre><br>


<h2 id="8-환경변수-세팅-방법">8. 환경변수 세팅 방법</h2>
<pre><code>환경변수명=VALUE</code></pre><p><code>.env</code> 에 따옴표 없이 위와 같은 형태로 환경변수 파일을 생성합니다.</p>
<br>


<h2 id="9-도커-실행">9. 도커 실행</h2>
<pre><code># 실행
docker run -p [로컬 port]:[도커 port] [사용자명]/[이미지명]

# 환경변수 파일 포함 실행
docker run -p 8080:8080 --env-file ./파일명.env [사용자명]/[이미지명]</code></pre><p><code>-d</code> 옵션을 주면 백그라운드로 실행되게 됩니다.</p>
<br>


<h2 id="10-실행-중인-컨테이너-조회">10. 실행 중인 컨테이너 조회</h2>
<pre><code>sudo docker ps</code></pre><p><code>-a</code> 옵션을 주면 중지된 컨테이너까지 조회됩니다.</p>
<br>



<h2 id="11-컨테이너-중지">11. 컨테이너 중지</h2>
<pre><code>sudo docker stop [컨테이너ID]</code></pre><br>


<h2 id="12-삭제">12. 삭제</h2>
<p>컨테이너 확인</p>
<pre><code>sudo docker ps -a </code></pre><p>컨테이너 삭제</p>
<pre><code>sudo docker rm [컨테이너ID]</code></pre><p>이미지 삭제</p>
<pre><code>sudo docker image rm [사용자명]/[이미지명]</code></pre><p>이미지를 삭제하기 전, 컨테이너 삭제가 필요합니다.</p>
<p><br><br></p>
<h1 id="docker-compose-with-mariadb">Docker Compose (with mariaDB)</h1>
<p>Docker Compose 로 스프링과 mariaDB 를 함께 실행해보겠습니다. 수행해본 결과 따로 mariaDB 이미지를 pull 받지 않아도, docker-compose 를 실행하면 자동으로 이미지를 가져와 실행해줍니다.</p>
<p><strong>저는 <code>docker-compose.yml</code> 파일을 바로 서버에서 작성하였습니다.</strong></p>
<br>


<h2 id="1-docker-composeyml-작성">1. docker-compose.yml 작성</h2>
<pre><code>version: &quot;3&quot;
services:
  database:
    container_name: mariadb
    image: mariadb
    volumes:
      - ~/docker/mariadb/etc/mysql/conf.d:/etc/mysql/conf.d:ro
      - ~/docker/mariadb/var/lib/mysql:/var/lib/mysql
      - ~/docker/mariadb/var/log/maria:/var/log/maria
    environment:
      - MYSQL_DATABASE=데이터베이스명
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_ROOT_HOST=root
    command: [&#39;--character-set-server=utf8mb4&#39;,&#39;--collation-server=utf8mb4_unicode_ci&#39;]
    ports:
      - 3306:3306

  application:
    container_name: spring-app
    image: &quot;test/testDemo&quot;
    ports:
      - 8080:8080
    environment:
      - BUCKET=${BUCKET}
      - CONFIG_FILE_PATH=${CONFIG_FILE_PATH}
    depends_on:
      - database</code></pre><p><code>depends_on</code> 명령어는 서비스 간의 종속성 순서대로 실행되게 합니다. 위에서는 mariadb 가 실행된 후에 spring-app 이 실행됩니다.</p>
<p><strong>docker-compose.yml 과 같은 위치에 <code>.env</code> 로 환경변수 파일을 만들면 별도로 파일을 지정하지 않아도 자동으로 불러옵니다.</strong> 저는 env 파일에 파일명을 주니 읽어오지 못했고, 파일명 없이 .env 만 작성하니 정상적으로 읽어왔습니다.</p>
<p>그냥 실행하면 데이터베이스를 찾을 수 없다는 오류가 뜨면서 실행되지 않습니다. 찾아보니 스크립트를 작성하면 된다는데 저는 스크립트 파일을 읽지 못하는 것 같아 <code>application.yml</code> 에서 설정해주었습니다.</p>
<p>데이터베이스를 자동으로 생성하게 할 때는 application.yml 에서 데이터베이스 URL 뒤에 <code>?createDatabaseIfNotExist=true</code> 옵션을 추가하면 됩니다.</p>
<br>



<h2 id="2-실행">2. 실행</h2>
<pre><code>docker-compose up</code></pre><p>이미지를 pull 받지 않아도 <code>docker-compose.yml</code> 을 작성해놓고 docker-compose up 을 실행하면 자동으로 이미지를 pull 받아서 수행합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CI/CD] Docker 와 Github Actions 를 활용한 CI/CD 환경 구축 - (1) 홈서버 구축]]></title>
            <link>https://velog.io/@hj_/CICD-Docker-%EC%99%80-Github-Actions-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-CICD-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-1-%ED%99%88%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95</link>
            <guid>https://velog.io/@hj_/CICD-Docker-%EC%99%80-Github-Actions-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-CICD-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-1-%ED%99%88%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95</guid>
            <pubDate>Wed, 20 Mar 2024 06:02:32 GMT</pubDate>
            <description><![CDATA[<p>지난 팀 프로젝트 때 서버를 담당하지 않아서 아쉬움이 있었고, 서버 구성도 완전치 않아 코드를 커밋하고 push한 후에 다시 서버에 들어가 pull 받고, 다시 빌드하고, 실행하는 과정을 거쳐야 했습니다.
이러한 점들을 보완하기 위해 직접 노트북에 서버를 구성해보고, CI/CD 환경까지 만들면서 작업하는 내용을 정리해보려고 합니다.</p>
<br>

<h1 id="네트워크-개념-살펴보기">네트워크 개념 살펴보기</h1>
<h2 id="공인-ip-사설-ip">공인 IP, 사설 IP</h2>
<p>먼저 서버를 구축하기에 앞서 알아야 하는 내용은 공인 IP 와 사설 IP 입니다.</p>
<p>공인 IP 는 인터넷에서 사용되는 주소로, IP 주소 할당 기관에 의해 할당된 주소이며, 인터넷 상에서 유효한 주소입니다.</p>
<p>사설 IP 는 인터넷 미연결 TCP/IP 네트워크를 위한 IP 주소입니다. 인터넷 IP 주소 관리 대상에 포함되지 않으며, 인터넷에서 사용이 불가능한 주소입니다.</p>
<p>우리가 자주 사용하는 와이파이를 예시로 들어보겠습니다. AP 는 Access Point 의 약자로 쉽게 말해서 우리가 생각하는 공유기입니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/628b6980-ac09-480a-9f81-18da2931a843/image.jpg" alt=""></p>
<p>WRT300N 을 보면 외부와 연결된 것을 볼 수 있는데 바로 여기에 공인 IP 가 할당되는 것입니다. </p>
<p>AP 에 연결된 장치의 IP 주소를 보면 192.168.0.2 인 것을 볼 수 있는데 AP 내부에서 각 장치 별로 부여되는 것이 사설 IP 입니다. <strong>이때 사설 IP 는 동적으로 부여되게 됩니다.</strong></p>
<p>그리고 AP 에는 Default 게이트웨이가 있는데 AP 에 연결된 모든 장치들은 인터넷에 접속하기 위해 먼저 게이트웨이 주소로 요청을 보내게 됩니다. </p>
<br>


<h2 id="nat-와-napt">NAT 와 NAPT</h2>
<p>사설 IP 는 인터넷에 연결할 수 없기 때문에 사설 IP 주소와 인터넷 공인 IP 주소 간의 변환 프로토콜 NAT 가 사용됩니다. </p>
<p><img src="https://velog.velcdn.com/images/hj_/post/f429b700-a01a-41a2-83af-eeae58e8ded1/image.jpg" alt=""></p>
<p>내부에 연결된 장치가 인터넷에 접속하려고 하면 라우터의 디폴트 게이트웨이로 요청이 가게 되는데 이때 미리 정의되거나, 동적으로 정의되는 NAT 변환 테이블에 의해 하나의 사설 IP 가 하나의 공인 IP 로 변환됩니다.</p>
<p>이렇게 하면 인터넷에 접속할 때만 공인 IP 주소를 사용할 수 있기 때문에 IP 주소를 절약할 수 있습니다. 하지만 NAT 는 하나의 사설 IP 에 하나의 공인 IP 를 부여하게 되므로, 많은 장치가 있다면 이를 전부 다른 IP 주소로 변환할 수는 없습니다. 그래서 등장한 것이 NAPT 입니다. </p>
<p><img src="https://velog.velcdn.com/images/hj_/post/abe13350-55d5-489d-9fc4-ab411727ffdf/image.jpg" alt=""></p>
<p>NAPT 는 하나의 공인 IP 주소에 여러 개의 사설 IP 주소를 변환할 수 있습니다. <strong>하나의 공인 IP 에 포트번호를 사용하여 사설 IP 주소와 공인 IP 주소 변환</strong>을 하면서 포트 번호 변환을 동시에 수행하는 것입니다.</p>
<p>홈서버를 구축할 때 알아야 하는 내용이 바로 이 NAPT 의 개념과, 사설 IP 는 동적으로 할당된다는 점입니다.</p>
<p><br><br></p>
<h1 id="홈-서버-구축하기">홈 서버 구축하기</h1>
<h2 id="1-네트워크-환경">1. 네트워크 환경</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/d84684dc-89f3-4d51-9c59-2623524d6c14/image.PNG" alt=""></p>
<p>현재 저의 네트워크 환경은 sk 공유기 안에서 iptime 공유기를 사용하고 있었고, 이 iptime 공유기에 서버용 노트북이 연결되어 있습니다. </p>
<p><img src="https://velog.velcdn.com/images/hj_/post/c2c3c811-b991-470b-8d91-b5592126c997/image.PNG" alt=""></p>
<p>iptime 의 관리자 화면은 <code>192.168.0.1</code> 이고, 연결된 동적 ip 를 보면 <code>192.168.25.52</code> 인 것을 볼 수 있는데 이 의미는 <strong>sk 공유기 내부에서 iptime 공유기가 해당 사설 ip 를 할당 받았다</strong>는 의미이고, 관리자 화면으로 접속하기 위해 <code>192.168.25.1</code> 로 접속하였습니다.</p>
<p><br><br></p>
<h2 id="2-서버-ip-고정">2. 서버 ip 고정</h2>
<p>앞엣 사설 ip 내부에서는 연결된 기기에 할당되는 ip 는 동적이라고 하였습니다. 예를 들어, 서버의 사설 ip 가 192.168.0.2 로 되어 있는 상태로 모든 설정을 마쳤는데 ip 가 다른 주소로 바뀌어 버리면 설정값을 또 바꿔주어야 합니다. 
이를 방지하기 위해 <strong>서버의 사설 ip 를 고정시켜야 합니다.</strong></p>
<p><img src="https://velog.velcdn.com/images/hj_/post/b8589bf7-4880-4757-a1ca-fc4add3e9682/image.PNG" alt=""></p>
<p>현재 서버용 노트북이 192.168.0.6 에 연결되어 있습니다. DHCP 서버 설정에 들어간 후 사용중인 ip 주소에서 해당하는 ip 를 클릭한 뒤 우측의 등록 버튼을 누르면 해당 사설 ip 가 고정됩니다.</p>
<p><br><br></p>
<h2 id="3-포트-포워딩">3. 포트 포워딩</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/0681d87a-aced-4d48-8743-d118c527af45/image.PNG" alt=""></p>
<p>포트 포워딩에 들어가서 공유기의 몇 번 포트로 들어왔을 때 어떤 ip 의 몇 번 포트로 연결할 것인지를 지정합니다. </p>
<p>앞에 sk 공유기가 있기 때문에 조금 다르지만 앞에서 살펴보았던 NAPT 를 생각하면 됩니다. <strong>iptime 외부에서 iptime 의 ip 주소의 n 번 포트로 들어오게 되면 iptime 내부 장치의 m 번 포트로 보내주는 역할을 수행합니다.</strong></p>
<p><br><br></p>
<h2 id="4-dns-설정">4. DNS 설정</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/6a405eaf-5624-4338-99f2-cdc8bc3fb85b/image.PNG" alt=""></p>
<p>가비아에서 도메인을 구매한 후, DNS 설정을 들어갑니다. 여기서 DNS 정보를 추가하는데 타입은 A, 호스트는 @ 를 지정합니다. 호스트를 <code>@</code> 로 지정하면 <code>example.com</code> 처럼 바로 접속할 수 있게 됩니다. <code>www</code> 를 지정하면 <code>www.example.com</code> 으로 접속해야 합니다. 그 후 오른쪽에 외부 IP 주소를 작성하면 됩니다.</p>
<p><br><br></p>
<h2 id="5-ddns-설정">5. DDNS 설정</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/9e925d9c-edf8-44f8-8120-5fd2458f1f4d/image.PNG" alt=""></p>
<p>가비아에서 네임서버를 확인할 수 있습니다. 저는 <code>ns.gabia.co.kr</code> 로 지정되어 있고 이는 DDNS 를 설정할 때 사용됩니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/173ad0a1-07fa-4b14-8ad6-35807068034e/image.PNG" alt=""></p>
<p>그 후 sk 공유기 관리자 화면에서 DDNS 를 설정하는데 DDNS 서버에 위의 네임 서버를 작성하고, DDNS 도메인에 구입한 도메인을 입력하면 됩니다.</p>
<p><br><br></p>
<h2 id="6-포트-포워딩">6. 포트 포워딩</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/961ef393-b7ea-4c26-a55f-3bce0953babc/image.PNG" alt=""></p>
<p>이제 외부에서 접속했을 때 iptime 에 연결되어 있는 서버까지 연결되도록 해야 합니다. 이전처럼 포트 포워딩을 사용해서 이를 설정할 수 있는데 <strong>포워딩 ip 주소는 현재 iptime 에 부여된( sk 공유기 하위의 ) 사설 ip 주소를 입력</strong>합니다.</p>
<p><strong>웹의 기본 포트가 80이기 때문에 외부 포트를 80으로 지정</strong>하고, iptime 에서 80번으로 들어오는 요청을 다시 서버의 80으로 보냈기 때문에 sk 공유기에서 iptime 으로 보내는 포트 역시 80 번으로 지정합니다.</p>
<p>즉, sk 공유기 80 ➜ iptime 80 ➜ 서버 80 흐름대로 진행되도록 설정합니다. 추가로 뒤에서 <strong>https 도 설정할 것이기 때문에 443 포트도 동일하게 443 으로 연결되도록 설정</strong>합니다.</p>
<blockquote>
<p><strong>주의!!! 443 포트를 추가하면 문제가 발생할 수도 있습니다. 저는 문제가 발생하였고, 가장 아래에 적어두었습니다.</strong></p>
</blockquote>
<p><br><br></p>
<h2 id="7-최종-구축-환경">7. 최종 구축 환경</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/7b6e08dc-5d93-40ec-b446-59cee65920f3/image.PNG" alt=""></p>
<p>최종 구축 환경은 위와 같습니다. 이제 가비아에서 구매한 도메인으로 접속하면 포트번호 없이 서버에 접근할 수 있게 됩니다.</p>
<p><br><br></p>
<h1 id="이후-발생한-문제">이후 발생한 문제</h1>
<p>NGINX 를 설정하면서 실행에 오류가 뜨길래 혹시나 하는 마음으로 sk 공유기에 443 포트포워딩을 설정하니 문제가 해결되었습니다. 하지만 여기서 <strong>집에서 사용하는 공유기가 인터넷 연결이 되지 않는다는 문제</strong>가 생겨버렸습니다. </p>
<p>그래서 sk 공유기에 설정한 모든 내용을 지우고, <strong>브릿지 모드로 변경하여 iptime 공유기에 공인 ip 가 할당되도록 변경</strong>하였습니다. 하지만 <strong>브릿지 모드로 변경하게 되면 sk 공유기의 관리자 페이지의 접속은 불가능해집니다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[QueryDSL] 4. Spring Data JPA 와 QueryDSL]]></title>
            <link>https://velog.io/@hj_/QueryDSL-4.-Spring-Data-JPA-%EC%99%80-QueryDSL</link>
            <guid>https://velog.io/@hj_/QueryDSL-4.-Spring-Data-JPA-%EC%99%80-QueryDSL</guid>
            <pubDate>Wed, 13 Mar 2024 06:10:21 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한 님의 <a href="https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84/dashboard">실전! Querydsl</a> 강의를 보고 작성한 내용입니다.</p>
</blockquote>
<br>



<h1 id="1-사용자-정의-리포지토리">1. 사용자 정의 리포지토리</h1>
<p>Spring Data JPA 를 사용하면 findByUsername 과 같은 쿼리를 자동으로 생성해주지만, 검색 조건에 따른 동적 쿼리를 작성할 수 없습니다. 그래서 사용자 정의 인터페이스를 사용하여 QueryDSL 을 활용한 동적 쿼리를 작성합니다.</p>
<h4 id="-1-인터페이스-생성-">[ 1. 인터페이스 생성 ]</h4>
<pre><code class="language-java">public interface MemberRepositoryCustom {
    List&lt;MemberTeamDto&gt; search(MemberSearchCond condition);
}</code></pre>
<br>

<h4 id="-2-인터페이스-상속-">[ 2. 인터페이스 상속 ]</h4>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt;, 
                                          MemberRepositoryCustom {
    ...
}</code></pre>
<p>JpaRepository 를 상속 받는 인터페이스가 새롭게 정의한 인터페이스를 상속 받도록 합니다. 이렇게 하면 생성한 동적 쿼리를 <code>MemberRepository.search()</code> 로 사용할 수 있습니다.</p>
<br>


<h4 id="-3-구현체-생성-">[ 3. 구현체 생성 ]</h4>
<pre><code class="language-java">public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    public MemberRepositoryCustomImpl(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);
    }

    @Override
    public List&lt;MemberTeamDto&gt; search(MemberSearchCond condition) {
        ...
    }
}</code></pre>
<p>사용자 정의 인터페이스의 구현체를 만들 때 MemberRepositoryImpl 혹은 MemberRepositoryCustomImpl 둘 중 아무거나 선택해서 이름을 지정하면 됩니다. 
( Spring Data JPA 강의 참고 )</p>
<p>QueryDSL 을 활용하기 때문에 생성자를 통해 EntityManager 를 주입받아 JpaQueryFactory 를 생성하고, 이를 이용해 동적 쿼리를 작성하면 됩니다. 동적 쿼리는 이전 시간에 작성한 것과 동일하게 작성하면 됩니다.</p>
<p><br><br><br></p>
<h1 id="2-querydsl-과-페이징-연동">2. QueryDSL 과 페이징 연동</h1>
<p><code>fetchResult()</code> 를 사용하면 데이터를 가져오는 쿼리와 총 데이터 수를 가져오는 쿼리가 실행되는데 해당 기능은 deprecated 되었습니다. </p>
<p>그래서 QueryDSL 5.0 부터 <strong>페이징 처리를 할 때는 데이터를 조회하는 쿼리, 데이터 수를 가져오는 쿼리를 따로 작성해서 구현해야 합니다.</strong></p>
<p>또 content 를 가져올 때는 조인이 필요하지만 count 를 가져올 때는 조인 여부에 관계 없이 데이터의 수가 동일한 경우가 있습니다. 이러한 이유로 count 쿼리를 따로 작성하는 것이 성능 상 이점을 가져갈 수 있습니다.</p>
<pre><code class="language-java">public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
    ...
    @Override
    public Page&lt;MemberTeamDto&gt; searchPageComplex(MemberSearchCond condition, 
                                                 Pageable pageable) {
        // content 쿼리
        List&lt;MemberTeamDto&gt; content = queryFactory
                .select(new QMemberTeamDto(
                        member.id,
                        member.username,
                        member.age,
                        team.id,
                        team.name
                ))
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        // count 쿼리
        Long total = queryFactory
                .select(member.count())
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                .fetchOne();

        return new PageImpl&lt;&gt;(content, pageable, total);
    }
}</code></pre>
<ol>
<li><p>content 를 가져올 때는 <code>fetch()</code> 를 사용합니다.</p>
</li>
<li><p>count 를 가져올 때는 select 에 count() 를 사용하며, <code>fetchOne()</code> 으로 실행합니다.</p>
</li>
</ol>
<p><br><br><br></p>
<h1 id="3-countquery-최적화">3. CountQuery 최적화</h1>
<p>페이지의 시작이면서 content 사이즈가 page 사이즈보다 작은 경우, 혹은 마지막 페이지인 경우에는 count 쿼리를 생략할 수 있습니다.</p>
<p><strong>Spring Data 가 제공하는 라이브러리를 사용하면 count 쿼리를 생략할 수 있는 경우, 자동으로 count 쿼리를 생략</strong>합니다.</p>
<pre><code class="language-java">public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
    ...
    @Override
    public Page&lt;MemberTeamDto&gt; searchPageComplex(MemberSearchCond condition, 
                                                 Pageable pageable) {
        // content 쿼리 생략
        JPAQuery&lt;Long&gt; countQuery = queryFactory
                .select(member.count())
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()));

        return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
    }
}</code></pre>
<p>count 쿼리를 작성한 후에 <code>fetchXXX</code> 를 사용하지 않으면 실제 쿼리가 수행되지 않고 <code>fetchXXX</code> 를 호출해야 쿼리가 실행됩니다. </p>
<p><strong>count 쿼리를 반환받고, 위처럼 반환하면 <code>getPage()</code> 에서 content 와 pageable 의 totalSize 를 보고 count 쿼리를 생략할 수 있다면 count 쿼리를 실행하지 않게 됩니다.</strong></p>
<p><br><br><br></p>
<h1 id="4-spring-data-jpa-가-제공하는-querydsl-기능">4. Spring Data JPA 가 제공하는 QueryDSL 기능</h1>
<blockquote>
<p>여기서 소개하는 기능은 제약이 커서 복잡한 실무 환경에서 사용하기에는 많이 부족하다고 합니다.</p>
</blockquote>
<h2 id="4-1-querydslpredicateexecutor">4-1. QuerydslPredicateExecutor</h2>
<h4 id="-인터페이스-">[ 인터페이스 ]</h4>
<pre><code class="language-java">public interface QuerydslPredicateExecutor&lt;T&gt; {
    Optional&lt;T&gt; findOne(Predicate predicate);
    Iterable&lt;T&gt; findAll(Predicate predicate);
    Page&lt;T&gt; findAll(Predicate predicate, Pageable pageable);
    long count(Predicate predicate);
    boolean exists(Predicate predicate);
    ...
}</code></pre>
<h4 id="-테스트-코드-">[ 테스트 코드 ]</h4>
<pre><code class="language-java">@Test
void querydslPredicateExecutor() {
    QMember member = QMember.member;
    Iterable&lt;Member&gt; result = memberRepository.findAll(
                                    member.age.between(10, 40)
                                        .and(member.username.eq(&quot;member1&quot;)));
}</code></pre>
<p>JpaRepository 를 상속 받는 인터페이스에서 해당 인터페이스를 상속 받으면 인터페이스가 제공하는 모든 기능을 사용할 수 있으며, 파라미터로 QueryDSL 조건을 넣을 수 있게 됩니다.</p>
<p>Pagable, Sort를 모두 지원하고 정상적으로 동작하지만 left join 이 불가능하며, 서비스 클래스가 QueryDSL 이라는 구현 기술에 의존해야 합니다.</p>
<p><br><br></p>
<h2 id="4-2-querydslrepositorysupport">4-2. QuerydslRepositorySupport</h2>
<h4 id="-생성-및-쿼리-작성-">[ 생성 및 쿼리 작성 ]</h4>
<pre><code class="language-java">public class MemberRepositoryCustomImpl extends QuerydslRepositorySupport 
                                        implements MemberRepositoryCustom {

    public MemberRepositoryCustomImpl() {
        super(Member.class);
    }

    @Override
    public List&lt;MemberTeamDto&gt; search(MemberSearchCond condition) {
        return from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                )
                .select(new QMemberTeamDto(
                        member.id,
                        member.username,
                        member.age,
                        team.id,
                        team.name
                ))
                .fetch();
    }
}</code></pre>
<p>기존에는 EntityManager 를 주입 받고 JpaQueryFactory 를 생성했는데 해당 인터페이스가 엔티티 매니저를 주입 받기 때문에 <code>super(Member.class)</code> 만 하면 됩니다.</p>
<p>JpaQueryFactory 없이 <code>from</code> 으로 시작하도록 하고 마지막에 <code>select</code> 를 넣는 형식으로 쿼리를 작성할 수 있습니다.</p>
<br>


<h4 id="-엔티티-매니저와-쿼리-팩토리-">[ 엔티티 매니저와 쿼리 팩토리 ]</h4>
<pre><code class="language-java">public class MemberRepositoryCustomImpl extends QuerydslRepositorySupport {

    private final JPAQueryFactory queryFactory;

    // queryFactory 사용
    public MemberRepositoryCustomImpl(EntityManager em) {
        super(Member.class);
        this.queryFactory = new JPAQueryFactory(em);
    }

    // entityManager
    private final EntityManager entityManager = getEntityManager();
}</code></pre>
<p>생성자에서 EntityManager 를 주입 받아 JpaQueryFactory 를 사용할 수 있으며, <code>getEntityManager()</code> 를 통해 엔티티 매니저를 사용할 수 있습니다.</p>
<br>


<h4 id="-페이징-">[ 페이징 ]</h4>
<pre><code class="language-java">@Override
public void searchPage(MemberSearchCond condition, Pageable pageable) {
    JPQLQuery&lt;MemberTeamDto&gt; query = 
            from(member)
            .leftJoin(member.team, team)
            .where(
                    usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe()))
            .select(new QMemberTeamDto(
                    member.id,
                    member.username,
                    member.age,
                    team.id,
                    team.name
            ));

    JPQLQuery&lt;MemberTeamDto&gt; pagingQuery = getQuerydsl()
                                            .applyPagination(pageable, query);
    List&lt;MemberTeamDto&gt; result = pagingQuery.fetch();
}</code></pre>
<p><code>getQuerydsl().applyPagination()</code> 을 사용하면 Spring Data 가 제공하는 페이징을 QueryDSL 로 편리하게 변환할 수 있으며, offset 과 limit 를 자동으로 넣어주게 됩니다. </p>
<p>이 메서드로 스프링 데이터가 제공하는 페이징을 Querydsl로 편리하게 변환할 수 있지만 Sort는 오류가 발생합니다.</p>
<br>


<h4 id="-단점-">[ 단점 ]</h4>
<ol>
<li><p>Querydsl 3.x 버전을 대상으로 만들었기 때문에 Querydsl 4.x에 나온 JPAQueryFactory로 시작할 수 없습니다.</p>
</li>
<li><p>Spring Data 가 제공하는 Sort 기능이 정상적으로 동작하지 않습니다.</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[QueryDSL] 3. 순수 JPA 와 QueryDSL]]></title>
            <link>https://velog.io/@hj_/QueryDSL-3.-%EC%88%9C%EC%88%98-JPA-%EC%99%80-QueryDSL</link>
            <guid>https://velog.io/@hj_/QueryDSL-3.-%EC%88%9C%EC%88%98-JPA-%EC%99%80-QueryDSL</guid>
            <pubDate>Wed, 13 Mar 2024 03:27:47 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한 님의 <a href="https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84/dashboard">실전! Querydsl</a> 강의를 보고 작성한 내용입니다.</p>
</blockquote>
<br>



<h1 id="1-순수-jpa-repository">1. 순수 JPA Repository</h1>
<h2 id="1-1-생성하기">1-1. 생성하기</h2>
<pre><code class="language-java">@Repository
public class MemberJpaRepository {

    private final EntityManager em;
    private final JPAQueryFactory queryFactory;

    public MemberJpaRepository(EntityManager em) {
        this.em = em;
        this.queryFactory = new JPAQueryFactory(em);
    }

    public List&lt;Member&gt; findByUsername_QueryDSL(String username) {
        return queryFactory
                .selectFrom(member)
                .where(member.username.eq(username))
                .fetch();
    }
}</code></pre>
<p>순수 JPA 를 이용한 Repository 이기 때문에 JPA 에 접근하기 위해 EntityManger 가 필요합니다. 또 QueryDSL 을 사용하려면 JpaQueryFactory 가 필요한데 EntityManager 를 파라미터로 넘겨주어 생성합니다. </p>
<br>


<h2 id="1-2-스프링-빈-등록하기">1-2. 스프링 빈 등록하기</h2>
<pre><code class="language-java">@Bean
JPAQueryFactory jpaQueryFactory(EntityManager em) {
    return new JPAQueryFactory(em);
}</code></pre>
<p>생성자를 사용하지 않고, JpaQueryFactory 를 스프링 빈으로 등록해서 사용할 수도 있습니다. 이때 Repository 에서는 <code>@RequiredArgsConstructor</code> 를 사용해서 주입받으면 됩니다.</p>
<p>스프링 빈은 싱글톤이기 때문에 같은 객체를 모든 멀티스레드에서 사용하기 때문에 동시성 문제에 대해 걱정할 수 있는데 전혀 문제가 되지 않습니다.</p>
<p>JpaQueryFactory 에 대한 모든 동시성 문제는 엔티티 매니저에 의존하는데, 엔티티 매니저를 스프링과 함께 사용하면 동시성 문제랑 전혀 관계 없이 트랜잭션 단위로 분리돼서 동작합니다. </p>
<p>여기서 스프링이 주입해주는 엔티티 매니저는 실제 동작 시점에 진짜 엔티티 매니저를 찾아주는 프록시용 가짜 엔티티 매니저입니다. 이 <strong>가짜 엔티티 매니저는 실제 사용 시점에 트랜잭션 단위로 실제 엔티티 매니저( 영속성 컨텍스트 )를 할당</strong>해주기 때문에 동시성 문제는 걱정하지 않아도 됩니다.</p>
<p><br><br><br></p>
<h1 id="2-준비하기">2. 준비하기</h1>
<h2 id="2-1-dto-생성">2-1. DTO 생성</h2>
<pre><code class="language-java">@Data
public class MemberTeamDto {
    private Long memberId;
    private String username;
    private int age;
    private Long teamId;
    private String teamName;

    @QueryProjection
    public MemberTeamDto(Long memberId, String username, int age, 
                         Long teamId, String teamName) {
        this.memberId = memberId;
        this.username = username;
        this.age = age;
        this.teamId = teamId;
        this.teamName = teamName;
    }
}</code></pre>
<p><code>@QueryProjection</code> 을 생성자에 붙여 Q 클래스를 생성합니다. 해당 어노테이션을 사용하면 select 에서 new 를 통해 DTO 를 바로 생성할 수 있지만 DTO 가 QueryDSL 라이브러리에 의존하게 된다는 단점이 있습니다.</p>
<br>


<h2 id="2-2-검색-조건-생성">2-2. 검색 조건 생성</h2>
<pre><code class="language-java">@Data
public class MemberSearchCond {
    private String username;
    private String teamName;
    private Integer ageGoe;
    private Integer ageLoe;
}</code></pre>
<p>회원명, 팀명, 나이를 검색 조건에 사용하기 위해 생성합니다.</p>
<p><br><br><br></p>
<h1 id="3-동적-쿼리와-dto-조회">3. 동적 쿼리와 DTO 조회</h1>
<h2 id="3-1-builder-사용">3-1. Builder 사용</h2>
<pre><code class="language-java">public List&lt;MemberTeamDto&gt; searchByBuilder(MemberSearchCond condition) {

    BooleanBuilder builder = new BooleanBuilder();

    if (StringUtils.hasText(condition.getUsername())) {
        builder.and(member.username.eq(condition.getUsername()));
    }


    if (condition.getAgeGoe() != null) {
        builder.and(member.age.goe(condition.getAgeGoe()));
    }

    return queryFactory
            .select(new QMemberTeamDto(
                    member.id,
                    member.username,
                    member.age,
                    team.id,
                    team.name))
            .from(member)
            .leftJoin(member.team, team)
            .where(builder)
            .fetch();
}</code></pre>
<ol>
<li><p>검색조건에서 문자는 null 이거나 빈 문자열이 들어올 수 있는데 <code>StringUtils.hasText()</code> 가 그런 것을 다 체크해줍니다.</p>
</li>
<li><p><code>@QueryProjection</code> 를 사용했기 때문에 new 키워드와 생성자를 통해 바로 DTO 를 조회할 수 있습니다.</p>
</li>
<li><p><strong>어노테이션을 사용하지 않는다면 현재 사용하는 DTO 와 엔티티들의 필드명이 다르기 때문에 <code>as</code> 를 사용해서 DTO 와 엔티티의 필드명을 맞춰주어야 합니다.</strong></p>
</li>
</ol>
<hr>

<p>참고로 검색 조건이 없다면 모든 데이터를 가져오게 됩니다. 하지만 데이터가 많이 쌓이다보면 모든 것을 가져오는게 성능 상 좋지 않습니다. 그래서 기본 조건을 넣거나, limit 를 설정하는 것이 좋습니다.</p>
<br>


<h2 id="3-2-where-절-파라미터-사용">3-2. where 절 파라미터 사용</h2>
<pre><code class="language-java">public List&lt;MemberTeamDto&gt; search(MemberSearchCond condition) {
    return queryFactory
            .select(new QMemberTeamDto(
                    member.id,
                    member.username,
                    member.age,
                    team.id,
                    team.name))
            .from(member)
            .leftJoin(member.team, team)
            .where(
                    usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe()))
            .fetch();
}

private BooleanExpression usernameEq(String username) {
    return hasText(username) ? member.username.eq(username) : null;
}

private BooleanExpression ageGoe(Integer ageGoe) {
    return ageGoe == null ? null : member.age.goe(ageGoe);
}</code></pre>
<ol>
<li><p>where 절 다중 파라미터에 각 검색 조건들에 대한 메서드를 사용합니다.</p>
</li>
<li><p>각 메서드는 null 과 빈 문자열에 주의해서 검색 조건을 작성합니다. 이때 hasText 는 StringUtils 의 메서드입니다.</p>
</li>
<li><p><strong>작성된 검색 조건 메서드들은 재사용이 가능하며, Predicate 가 아닌 BooleanExpression 을 사용하면 검색 조건들을 조합할 수 있습니다.</strong></p>
</li>
</ol>
<p><br><br><br></p>
<h1 id="참고-프로파일-분리">참고&gt; 프로파일 분리</h1>
<h4 id="-설정-파일-작성-">[ 설정 파일 작성 ]</h4>
<pre><code class="language-yml">spring:
  profiles:
    active: local    # main/resources/application.yml

spring:
  profiles:
    active: test    # test/resources/application.yml</code></pre>
<p>로컬에서의 실행과 테스트에서의 실행 프로파일을 분리하기 위해 test/resources 에 application.yml 파일을 추가합니다. 추가 후에는 위처럼 로컬과 테스트 프로파일을 각각 설정합니다.</p>
<hr>


<h4 id="-프로파일-적용-">[ 프로파일 적용 ]</h4>
<pre><code class="language-java">@Profile(&quot;local&quot;)
@Component
@RequiredArgsConstructor
public class InitMember {
    ...
}</code></pre>
<p>위처럼 <code>@Profile(&quot;local&quot;)</code> 을 붙여주면 local 프로파일이 실행될 때만 동작하게 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[QueryDSL] 2. QueryDSL 중급 문법]]></title>
            <link>https://velog.io/@hj_/QueryDSL-2.-QueryDSL-%EC%A4%91%EA%B8%89-%EB%AC%B8%EB%B2%95-u7lobay2</link>
            <guid>https://velog.io/@hj_/QueryDSL-2.-QueryDSL-%EC%A4%91%EA%B8%89-%EB%AC%B8%EB%B2%95-u7lobay2</guid>
            <pubDate>Tue, 12 Mar 2024 02:59:03 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한 님의 <a href="https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84/dashboard">실전! Querydsl</a> 강의를 보고 작성한 내용입니다.</p>
</blockquote>
<br>



<h1 id="1-프로젝션-결과-반환-dto">1. 프로젝션 결과 반환 DTO</h1>
<blockquote>
<p>프로젝션 대상이 하나라면 타입을 명확히 지정할 수 있습니다.</p>
<p>대상이 둘 이상인 경우 <strong>튜플</strong>이나 <strong>DTO</strong> 를 통해 반환 받게 됩니다.</p>
</blockquote>
<h2 id="1-1-순수-jpa-에서-dto-반환">1-1. 순수 JPA 에서 DTO 반환</h2>
<pre><code class="language-java">List&lt;MemberDto&gt; result = em.createQuery(
                &quot;select new study.querydsl.dto.MemberDto(m.username, m.age) &quot; +
                &quot;from Member m&quot;, MemberDto.class)
            .getResultList();</code></pre>
<p>순수 JPA 에서 DTO 를 조회할 때는 new 명령어를 사용해서 생성자를 통해 반환합니다. 또한 DTO 의 패키지 경로까지 모두 작성해야 합니다. </p>
<br>


<h2 id="1-2-querydsl-빈--bean-population-">1-2. QueryDSL 빈 ( Bean population )</h2>
<p>QueryDSL 에서 DTO 를 쉽게 조회할 수 있도록 빈 생성 방식을 제공합니다. 이때 3가지 방법으로 DTO 에 값을 담아 반환할 수 있습니다.</p>
<blockquote>
<ol>
<li><p>프로퍼티 접근( setter )</p>
</li>
<li><p>필드 직접 접근</p>
</li>
<li><p>생성자 사용</p>
</li>
</ol>
</blockquote>
<br>


<h3 id="1-2-1-프로퍼티-접근-setter-">1-2-1. 프로퍼티 접근( setter )</h3>
<pre><code class="language-java">List&lt;MemberDto&gt; result = queryFactory
                .select(Projections.bean(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();</code></pre>
<p><code>Projections.bean()</code> 을 사용해서 DTO 를 바로 조회할 수 있습니다. 첫 번째 파라미터로 <strong>어떤 DTO 인지를 명시</strong>하고, 그 뒤에 <strong>필요한 필드</strong>들을 넣어주면 됩니다. 이때 <strong>DTO 에는 기본 생성자가 필요</strong>합니다.</p>
<br>



<h3 id="1-2-2-필드-직접-접근">1-2-2. 필드 직접 접근</h3>
<pre><code class="language-java">List&lt;MemberDto&gt; result = queryFactory
                .select(Projections.fields(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();</code></pre>
<p><code>Projections.fields</code> 를 통해 필드에 값을 바로 넣어버리기 때문에 DTO 에 getter, setter 는 필요하지 않습니다. 형태는 프로퍼티 접근과 동일합니다.</p>
<br>



<h3 id="1-2-3-생성자-접근">1-2-3. 생성자 접근</h3>
<pre><code class="language-java">List&lt;MemberDto&gt; result = queryFactory
                .select(Projections.constructor(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();</code></pre>
<p><code>Projections.constructor</code> 를 통해 생성자 접근 방식으로 DTO 를 생성할 수 있습니다. 이때 <strong>전달하는 username 과 age 는 DTO 에 선언된 username, age 와 타입이 일치해야 합니다.</strong></p>
<br>



<h2 id="1-3-필드명이-다른-경우">1-3. 필드명이 다른 경우</h2>
<p>Projection 에서 사용한 username, age 이 필드명 그대로 MemberDto 에 존재하기 때문에 사용할 수 있었습니다. 즉, <strong>엔티티와 DTO 의 필드명이 동일하게 매칭되었기 때문에 가능</strong>했습니다.</p>
<p>하지만 만약 username 이 아닌 name 이라는 필드를 가진 DTO 가 있으면 어떻게 될까요?</p>
<h4 id="-userdto-">[ UserDto ]</h4>
<pre><code class="language-java">public class UserDto {
    private String name;
    private int age;
}</code></pre>
<h4 id="-테스트-코드-">[ 테스트 코드 ]</h4>
<pre><code class="language-java">@Test
void findUserDto() {
    List&lt;UserDto&gt; result = queryFactory
            .select(Projections.fields(UserDto.class,
                    member.username,
                    member.age))
            .from(member)
            .fetch();

    for (UserDto userDto : result) {
        System.out.println(&quot;userDto = &quot; + userDto);
    }
}</code></pre>
<h4 id="-출력-결과-">[ 출력 결과 ]</h4>
<blockquote>
<p>userDto = UserDto(name=null, age=10)
userDto = UserDto(name=null, age=20)
userDto = UserDto(name=null, age=30)
userDto = UserDto(name=null, age=40)</p>
</blockquote>
<p>테스트 실행 결과를 보면 username 과 name 이 매칭이 되지 않기 때문에 name 에 null 값이 들어간 것을 볼 수 있습니다.</p>
<p><strong>생성자의 경우 DTO 에 생성자가 존재하면 문제없이 동작하지만, 프로퍼티나 필드 접근 생성 방식에서 이름이 다를 때는 문제가 발생하며, 이를 해결할 수 있는 방법은 2가지가 존재</strong>합니다.</p>
<hr>


<h4 id="-해결-방법-1-별칭-사용-">[ 해결 방법 1. 별칭 사용 ]</h4>
<pre><code class="language-java">@Test
void findUserDto() {
    List&lt;UserDto&gt; result = queryFactory
            .select(Projections.fields(UserDto.class,
                    member.username.as(&quot;name&quot;),
                    member.age))
            .from(member)
            .fetch();
}</code></pre>
<p>엔티티 필드에 <code>.as(&quot;dto 필드명&quot;)</code> 를 붙이면 정상적으로 username 이 name 필드에 들어가게 됩니다.</p>
<hr>

<h4 id="-해결-방법-2-expressionutils-사용-">[ 해결 방법 2. ExpressionUtils 사용 ]</h4>
<pre><code class="language-java">@Test
void findUserDto() {
    QMember memberSub = new QMember(&quot;memberSub&quot;);

    List&lt;UserDto&gt; result = queryFactory
            .select(Projections.fields(UserDto.class,
                    member.username.as(&quot;name&quot;),
                    ExpressionUtils.as(
                        JPAExpressions
                            .select(memberSub.age.max())
                            .from(memberSub), &quot;age&quot;)
                        )
                    )
            .from(member)
            .fetch();
}</code></pre>
<p><code>ExpressionUtils.as(source, alias)</code> 형태로 필드나 서브 쿼리에 별칭을 지정하는 형태로 이름이 다른 문제를 해결할 수 있습니다.</p>
<p><strong>필드의 경우는 별칭을 사용하는 방식으로 해결하고, 서브 쿼리의 경우 ExpressionUtils 를 사용해서 해결합니다.</strong></p>
<p><br><br><br></p>
<h2 id="1-4-queryproection">1-4. @QueryProection</h2>
<h4 id="-dto-">[ DTO ]</h4>
<pre><code class="language-java">@Data
public class MemberDto {
    @QueryProjection
    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}</code></pre>
<p>응답으로 사용할 DTO 의 생성자에 <code>@QueryProection</code> 을 붙이면 Q 클래스가 생성되고, 그 내부에 생성자를 가지게 됩니다.</p>
<hr>


<h4 id="-테스트-코드--1">[ 테스트 코드 ]</h4>
<pre><code class="language-java">@Test
void findDtoByQueryProjection() {
   List&lt;MemberDto&gt; result = queryFactory
        .select(new QMemberDto(member.username, member.age))
        .from(member)
        .fetch();
}</code></pre>
<p>그 후 select 절에서 <code>new</code> 를 통해 Q 클래스를 생성하면 기존 DTO 가 생성돼서 반환됩니다.</p>
<p><strong><code>@QueryProjection</code> 을 사용하면 컴파일 시점에 오류를 잡을 수 있는 반면, <code>Projections.constructor</code> 를 사용하면 런타임에 오류가 발생하게 됩니다.</strong></p>
<p>하지만 DTO 에 QueryDSL 어노테이션을 유지해야 하는 점과 DTO 까지 Q 클래스를 생성해야 하는 단점이 있습니다.</p>
<p><br><br><br></p>
<h1 id="2-동적-쿼리">2. 동적 쿼리</h1>
<h2 id="2-1-booleanbuilder">2-1. BooleanBuilder</h2>
<pre><code class="language-java">private List&lt;Member&gt; searchMember1(String usernameCond, Integer ageCond) {
    BooleanBuilder builder = new BooleanBuilder();

    if (usernameCond != null) {
        builder.and(member.username.eq(usernameCond));
    }

    if (ageCond != null) {
        builder.and(member.age.eq(ageCond));
    }

    return queryFactory
            .selectFrom(member)
            .where(builder)
            .fetch();
}</code></pre>
<p><code>BooleanBuilder</code> 를 생성해서 동적 쿼리를 작성할 수 있습니다. builder 에 조건을 추가할 수 있는데 null 인지 판단하는 로직을 추가하면 동적 쿼리를 작성할 수 있습니다. 그 후 where 절 안에 builder 를 넣으면 자동으로 조건이 생성됩니다.</p>
<p>만약 usernameCond 가 필수라면 <code>new BooleanBuilder(member.username.eq(usernameCond))</code> 로 작성해서 초기 조건을 지정할 수 있습니다.</p>
<p>또 where 절에 <code>builder.and()</code> 와 같이 계속해서 조건을 작성할 수 있습니다.</p>
<br>



<h2 id="2-2-where-절-다중-파라미터">2-2. where 절 다중 파라미터</h2>
<pre><code class="language-java">private List&lt;Member&gt; searchMember2(String usernameCond, Integer ageCond) {
    return queryFactory
            .selectFrom(member)
            .where(usernameEq(usernameCond), ageEq(ageCond))
            .fetch();
}

private BooleanExpression usernameEq(String usernameCond) {
    return usernameCond == null ? null : member.username.eq(usernameCond);
}

private BooleanExpression ageEq(Integer ageCond) {
    return ageCond == null ? null : member.age.eq(ageCond);
}</code></pre>
<p><strong>where 에 조건을 여러 개 나열하면 and 조건으로 들어가고, null 이 들어가면 무시됩니다.</strong> 
이 방식을 사용하면 메서드를 다른 쿼리에서도 재활용 할 수 있다는 장점이 있습니다.</p>
<p>또 아래처럼 두 조건을 조합해서 사용할 수 있지만, null 체크에 주의해야 합니다.</p>
<pre><code class="language-java">private BooleanExpression allEq(String usernameCond, Integer ageCond) {
    return usernameEq(usernameCond).and(ageEq(ageCond));
}</code></pre>
<p><br><br><br></p>
<h1 id="3-수정-삭제-벌크-연산">3. 수정, 삭제 벌크 연산</h1>
<h4 id="-수정-">[ 수정 ]</h4>
<pre><code class="language-java">long count = queryFactory
        .update(member)
        .set(member.username, &quot;성인&quot;)
        .where(member.age.lt(19))
        .execute();</code></pre>
<p><code>execute()</code> 를 사용하며, 반환값은 영향을 받은 로우의 수가 됩니다.</p>
<hr>


<h4 id="-삭제-">[ 삭제 ]</h4>
<pre><code class="language-java">long count = queryFactory
        .delete(member)
        .where(member.age.gt(18))
        .execute();</code></pre>
<p>영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에 벌크연산을 실행하고 나면 영속성 컨텍스트를 초기화 하는 것이 좋습니다.</p>
<p><br><br><br></p>
<h1 id="4-sql-function-호출">4. SQL Function 호출</h1>
<pre><code class="language-java">String result = queryFactory
        .select(Expressions.stringTemplate(
                    &quot;function(&#39;replace&#39;, {0}, {1}, {2})&quot;, 
                        member.username, &quot;member&quot;, &quot;M&quot;))
        .from(member)
        .fetchFirst();</code></pre>
<p>member 를 M 으로 변경하는 replace 함수를 호출하는 코드입니다. SQL function 은 JPA 와 같이 Dialect 에 등록된 내용만 호출할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[QueryDSL] 1. QueryDSL 기본 문법]]></title>
            <link>https://velog.io/@hj_/QueryDSL-1.-QueryDSL-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95</link>
            <guid>https://velog.io/@hj_/QueryDSL-1.-QueryDSL-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95</guid>
            <pubDate>Mon, 11 Mar 2024 07:57:39 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한 님의 <a href="https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84/dashboard">실전! Querydsl</a> 강의를 보고 작성한 내용입니다.</p>
</blockquote>
<br>

<h1 id="1-jpql-vs-querydsl">1. JPQL vs QueryDSL</h1>
<h2 id="1-1-비교하기">1-1. 비교하기</h2>
<h4 id="-jpql-">[ JPQL ]</h4>
<pre><code class="language-java">@Test
public void jpql() {
    String jpql = &quot;select m from Member m where m.username = :username&quot;;
    Member findMember = em.createQuery(jpql, Member.class)
                .setParameter(&quot;username&quot;, &quot;member1&quot;)
                .getSingleResult();

    assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;);
}</code></pre>
<h4 id="-querydsl-">[ QueryDSL ]</h4>
<pre><code class="language-java">@Test
void queryDSL() {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    QMember m = new QMember(&quot;m&quot;);

    Member findMember = queryFactory
                .select(m)
                .from(m)
                .where(m.username.eq(&quot;member1&quot;))
                .fetchOne();
    assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;);
}</code></pre>
<p><code>JpaQueryFactory</code> 의 생성자에 <code>EntityManager</code> 를 넘겨줍니다. 그러면 QueryFactory 가 Entitymanager 를 가지고 데이터를 찾거나 하는 등의 작업을 수행합니다.</p>
<p>쿼리는 일반적인 sql 처럼 select, from, where 를 사용하여 작성하면 되고, QueryDSL 은 자동으로 preparedStatement 와 파라미터 바인딩 방식을 사용해서 기존에 setParameter 로 수행했던 과정을 자동으로 수행해줍니다.</p>
<p><strong>쉽게 생각하면 Querydsl은 JPQL 빌더이며, 가장 큰 차이점으로는 JPQL 은 문자로 작성해야 하지만 QueryDSL 은 코드로 작성하기 때문에 컴파일 시점에 오류를 잡아낼 수 있습니다.</strong></p>
<br>


<h2 id="1-2-jpaqueryfactory를-필드로">1-2. JPAQueryFactory를 필드로</h2>
<pre><code class="language-java">public class QuerydslBasicTest {

    @PersistenceContext
    EntityManager em;

    JPAQueryFactory queryFactory;

    @BeforeEach
    public void before() {
        queryFactory = new JPAQueryFactory(em);
        ...
    }
}</code></pre>
<p>JPAQueryFactory 를 필드로 빼고, 이를 생성할 때 entityManager 를 넘겨주는 방식으로 사용해도 됩니다.</p>
<p><strong>스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager 에 접근해도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공</strong>하기 때문에 JPAQueryFacytory 는 멀티 스레드 환경에서 동시성 문제 없이 동작합니다.</p>
<p><br><br><br></p>
<h1 id="2-q-type">2. Q-Type</h1>
<p>Q 클래스 인스턴스를 사용하는 방법에는 2가지가 존재합니다.</p>
<pre><code class="language-java">QMember m = new QMember(&quot;m&quot;);   // 별칭 직접 사용
QMember qMember = QMember.member; // Q 클래스 내부에 자동으로 생성된 인스턴스를 사용</code></pre>
<br>


<h2 id="2-1-인스턴스-사용">2-1. 인스턴스 사용</h2>
<pre><code class="language-java">import static study.querydsl.entity.QMember.*;

@Test
void queryDSL() {
    Member findMember = queryFactory
                .select(member)
                .from(member)
                .where(member.username.eq(&quot;member1&quot;))
                .fetchOne();
    assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;);
}</code></pre>
<p>위의 코드는 인스턴스를 사용 + static import 를 사용하는 방식입니다. select 내부에 <code>QMember.member</code> 를 작성한 후 QMember 를 static import 를 하면 위처럼 사용할 수 있습니다.</p>
<br>


<h2 id="2-2-별칭-사용">2-2. 별칭 사용</h2>
<p>QueryDSL 은 JPQL 의 빌더 역할을 하며, QueryDSL 로 작성된 것은 JPQL 로 변환되어 실행됩니다. 실행되는 JPQL 을 확인하고 싶다면 아래 설정을 추가하면 됩니다.</p>
<pre><code class="language-yml">spring:
  jpa:
    properties:
      hibernate:
        use_sql_comments: true</code></pre>
<hr>



<p>설정을 추가하고 실행되는 JPQL 을 살펴보면 아래와 같습니다.</p>
<pre><code class="language-sql">select member1
from Member member1
where member1.username = ?1 </code></pre>
<p>현재 별칭이 member1 이라고 지정된 것을 볼 수 있습니다. 이는 QMember 를 들어가보면 왜 member1 이라고 지정되는지 알 수 있습니다.</p>
<hr>


<p><img src="https://velog.velcdn.com/images/hj_/post/51668596-bc97-444f-9387-9bfa7a48aa50/image.png" alt=""></p>
<p>QMember 에서 인스턴스 (member )를 만들 때 member1 이라는 이름으로 생성하기 때문에 JPQL 에서 member1 이라고 표시됩니다.</p>
<hr>


<pre><code class="language-java">@Test
void queryDSL() {
    QMember m = new QMember(&quot;m&quot;);

    Member findMember = queryFactory
                .select(m)
                .from(m)
                .where(m.username.eq(&quot;member1&quot;))
                .fetchOne();
    assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;);
}</code></pre>
<pre><code class="language-sql">select m
from Member m
where m.username = ?1</code></pre>
<p>만약 별칭을 사용하도록 테스트 코드를 수정하고 실행하면 member1 으로 나오던 것이 별칭으로 지정한 m 으로 변경됩니다.</p>
<p>하지만 같은 테이블을 조인해야 하는 경우가 아니면 기본 인스턴스를 사용하는 것을 권장하신다고 합니다.</p>
<p><br><br><br></p>
<h1 id="3-querydsl-기본-문법">3. QueryDSL 기본 문법</h1>
<h2 id="3-1-검색-조건">3-1. 검색 조건</h2>
<p>QueryDSL 은 JPQL 이 지원하는 모든 검색 조건을 제공합니다. 아래 예시 몇 가지가 있습니다.</p>
<pre><code class="language-java">member.username.isNotNull()             // 이름이 is not null

member.age.in(10, 20)                   // age in (10,20)

member.age.goe(30)                      // age &gt;= 30
member.age.gt(30)                       // age &gt; 30

member.username.like(&quot;member%&quot;)         // like 검색
member.username.contains(&quot;member&quot;)      // like ‘%member%’ 검색
member.username.startsWith(&quot;member&quot;)    // like ‘member%’ 검색</code></pre>
<p>이때 <strong>여러 조건을 한 번에 사용하기 위해 and() 와 파라미터 처리를 제공</strong>합니다.</p>
<hr>


<h4 id="-and-">[ and ]</h4>
<pre><code class="language-java">@Test
void search() {
    Member findMember = queryFactory
            .selectFrom(member)
            .where(member.username.eq(&quot;member1&quot;)
                    .and(member.age.eq(10)))
            .fetchOne();
    assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;);
    assertThat(findMember.getAge()).isEqualTo(10);
}</code></pre>
<p>where 안에 여러 개의 조건을 and 로 묶을 수 있습니다. 참고로 and 말고 or 도 가능하며, select 와 from 을 <code>selectFrom()</code> 으로 줄여서 사용할 수 있습니다.</p>
<hr>


<h4 id="-파라미터-">[ 파라미터 ]</h4>
<pre><code class="language-java">@Test
void search() {
    Member findMember = queryFactory
            .selectFrom(member)
            .where(member.username.eq(&quot;member1&quot;), (member.age.eq(10)))
            .fetchOne();
    assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;);
    assertThat(findMember.getAge()).isEqualTo(10);
}</code></pre>
<p>and 로 묶지 않고 쉼표를 통해 끊어서 사용할 수 있습니다. 이 경우에는 <strong>AND 조건으로 묶이게 되며 null 값은 무시</strong>됩니다. 이 기능과 메서드 추출을 활용해서 동적 쿼리를 깔끔하게 만들 수 있습니다.</p>
<p><br><br></p>
<h2 id="3-2-결과-조회">3-2. 결과 조회</h2>
<blockquote>
<p><strong>fetch</strong> : 리스트를 조회, 없으면 빈 리스트가 반환</p>
<p><strong>fetchOne</strong> : 단건 조회, 결과가 없으면 null, 둘 이상이면 NonUniqueResultException </p>
<p><strong>fetchFirst</strong> : <code>limit(1).fetchOne()</code> 을 실행</p>
<p><strong>fetchResults</strong> : 페이징 정보 포함한 결과 반환, count 쿼리가 추가로 실행됨</p>
<p><strong>fetchCount</strong> : count 를 조회</p>
</blockquote>
<hr>


<pre><code class="language-java">//페이징에서 사용
QueryResults&lt;Member&gt; results = queryFactory
                .selectFrom(member)
                .fetchResults();

long limit = results.getLimit();
long offset = results.getOffset();
long total = results.getTotal();
List&lt;Member&gt; content = results.getResults();</code></pre>
<p>fetchResult 의 결과에서 <code>getTotal()</code> 로 count 쿼리의 결과를 얻을 수 있고, <code>getResult()</code> 로 데이터를 가져올 수 있습니다. 이때 쿼리는 count 쿼리와 데이터를 가져오는 쿼리 총 2번이 실행됩니다.</p>
<hr>


<p><strong>fetchCount</strong> 와 <strong>fetchResult</strong> 는 개발자가 작성한 select 쿼리를 기반으로 count 용 쿼리를 내부에서 만들어서 실행합니다.</p>
<p>그런데 이 기능은 단순히 select 구문을 count 처리하는 용도로 바꾸는 정도입니다. 따라서 단순한 쿼리에서는 잘 동작하지만, 복잡한 쿼리에서는 제대로 동작하지 않는다고 합니다.</p>
<p>또 <strong>fetchResults 와 fetchCount 는 Querydsl 5.0 부터 Deprecated 되었습니다. 그 대안으로 fetch 를 사용하고, count 쿼리는 필요한 경우 직접 작성하며, fetchOne 을 실행</strong>하면 됩니다.</p>
<p><br><br></p>
<h2 id="3-3-정렬">3-3. 정렬</h2>
<pre><code class="language-java">@Test
void sort() {
    List&lt;Member&gt; result = queryFactory
            .selectFrom(member)
            .where(member.age.eq(100))
            .orderBy(member.age.desc(),
                    member.username.asc().nullsLast())
            .fetch();            
}</code></pre>
<p>위의 예시는 나이 내림차순, 이름 오름차순 + null 은 가장 마지막에 위치를 기준으로 정렬한 예시입니다.</p>
<p>정렬은 <code>orderBy()</code> 에 지정할 수 있으며, 쉼표를 통해 여러 개를 지정할 수 있습니다. 또 null 데이터의 순서를 지정할 수 있는데 <code>nullsLast()</code>, <code>nullsFirst()</code> 가 있습니다.</p>
<p><br><br></p>
<h2 id="3-4-페이징">3-4. 페이징</h2>
<pre><code class="language-java">@Test
void paging1() {
    List&lt;Member&gt; result = queryFactory
            .selectFrom(member)
            .orderBy(member.username.desc())
            .offset(0)  // 0부터 시작
            .limit(2)
            .fetch();
}</code></pre>
<p>offset 과 limit 를 사용해서 페이징 쿼리를 작성할 수 있으며, offset 은 0 부터 시작합니다.</p>
<p><strong>페이징 쿼리를 작성할 때, 데이터를 조회하는 쿼리는 여러 테이블을 조인해야 하지만, count 쿼리는 조인이 필요 없는 경우도 있습니다</strong>. 만약 <code>fetchResult()</code> 를 사용한다면 자동화된 count 쿼리가 원본 쿼리처럼 모두 조인을 해버리기 때문에 성능이 좋지 않을 수도 있습니다. 따라서 count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면, count 전용 쿼리를 별도로 작성해야 합니다.</p>
<p><br><br></p>
<h2 id="3-5-집합-함수">3-5. 집합 함수</h2>
<pre><code class="language-java">@Test
void group() {
    List&lt;Tuple&gt; result = queryFactory
            .select(team.name, 
                    member.age.avg(),
                    member.age.sum(),
                    member.age.avg(),
                    member.age.max(),
                    member.age.min()
                    )
            .from(member)
            .join(member.team, team)
            .groupBy(team.name)
            .fetch();

Tuple teamA = result.get(0);

assertThat(teamA.get(team.name)).isEqualTo(&quot;teamA&quot;);
assertThat(teamA.get(member.age.avg())).isEqualTo(15);
}</code></pre>
<p>기본적인 집계 함수들을 다 사용할 수 있으며, <strong>Tuple</strong> 이 반환됩니다. tuple 안에 담긴 데이터를 가져올 때는 <code>get()</code> 내부에 조회한 것을 넣어주면 됩니다. 추가로 <code>having()</code> 을 통해 그룹된 결과를 제한할 수 있습니다.</p>
<p><br><br><br></p>
<h1 id="4-조인">4. 조인</h1>
<h2 id="4-1-기본-조인">4-1. 기본 조인</h2>
<blockquote>
<p>join(조인대상, 별칭으로 사용할 Q타입)</p>
</blockquote>
<pre><code class="language-java">@Test
void join() {
    List&lt;Member&gt; result = queryFactory
            .selectFrom(member)
            .join(member.team, team)
            .where(team.name.eq(&quot;teamA&quot;))
            .fetch();
}</code></pre>
<p>JPQL 에서 <code>join m.team t</code> 로 조인을 사용했는데 QueryDSL 도 <strong>조인대상</strong>을 지정하고 <strong>별칭처럼 Q 타입</strong>을 지정합니다. join 외에도 innerJoin, leftJoin 과 같은 다른 조인도 사용할 수 있습니다.</p>
<hr>


<pre><code class="language-sql">select
    member1 
from
    Member member1   
inner join
    member1.team as team 
where
    team.name = ?1</code></pre>
<p>테스트 코드 실행 결과 위와 같은 JPQL 이 생성되고 실행됩니다. </p>
<p><br><br></p>
<h2 id="4-2-세타-조인">4-2. 세타 조인</h2>
<pre><code class="language-java">@Test
void theta_join() {
    List&lt;Member&gt; result = queryFactory
            .select(member)
            .from(member, team)
            .where(member.username.eq(team.name))
            .fetch();
}</code></pre>
<p>연관관계가 없어도 세타 조인을 통해 조인을 실행할 수 있습니다. 이때는 from 절에 여러 개의 엔티티를 사용하면 됩니다. 하지만 세타 조인은 외부 조인 불가능한데 뒤에 나오는 조인 on 을 사용하면 외부 조인이 가능합니다.</p>
<p><br><br></p>
<h2 id="4-3-조인-on-절">4-3. 조인 ON 절</h2>
<p>ON 절을 활용하면 아래 두 가지를 수행할 수 있습니다</p>
<blockquote>
<ol>
<li>조인 대상 필터링</li>
<li>연관관계가 없는 엔티티 외부 조인</li>
</ol>
</blockquote>
<br>


<h4 id="-조인-대상-필터링-">[ 조인 대상 필터링 ]</h4>
<pre><code class="language-java">@Test
void join_on_filtering() {
    List&lt;Tuple&gt; result = queryFactory
            .select(member, team)
            .from(member)
            .leftJoin(member.team, team)
            .on(team.name.eq(&quot;teamA&quot;))
            .fetch();
}</code></pre>
<blockquote>
<p>tuple = [Member(id=1, username=member1, age=10), Team(id=1, name=teamA)]
tuple = [Member(id=2, username=member2, age=20), Team(id=1, name=teamA)]
tuple = [Member(id=3, username=member3, age=30), null]
tuple = [Member(id=4, username=member4, age=40), null]</p>
</blockquote>
<p>테스트 코드를 수행한 결과는 위와 같습니다. <strong>ON 절을 통해 team.name 에 조건을 걸었기 때문에 teamA 만 출력</strong>되었고, left join 이기 때문에 teamB 에 해당하는 Member3, Member4 는 team 이 null 로 출력되었습니다.</p>
<p>on 절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인을 사용하면 where 절에서 필터링 하는 것과 기능이 동일합니다. 따라서 내부조인이면 where 절로 해결하고, 정말 외부조인이 필요한 경우에만 이 기능을 사용하는 것을 권장한다고 하십니다.</p>
<br>



<h4 id="-연관관계가-없는-엔티티-외부-조인-">[ 연관관계가 없는 엔티티 외부 조인 ]</h4>
<pre><code class="language-java">@Test
void join_on_no_relation() {
    List&lt;Tuple&gt; result = queryFactory
            .select(member, team)
            .from(member)
            .leftJoin(team)
            .on(member.username.eq(team.name))
            .fetch();

    for (Tuple tuple : result) {
        System.out.println(&quot;tuple = &quot; + tuple);
    }
}</code></pre>
<p>하이버네이트 5.1부터 on 을 사용해서 <strong>서로 관계가 없는 필드로 내부 조인, 외부조인을 하는 기능이 추가</strong>되었습니다. </p>
<hr>


<pre><code class="language-sql">select
    m1_0.member_id, m1_0.age, m1_0.team_id, m1_0.username, t1_0.team_id, t1_0.name 
from
    member m1_0 
left join
    team t1_0 
on m1_0.username=t1_0.name</code></pre>
<p>기존에는 <code>leftJoin(member.team, team)</code> 을 사용했는데 이렇게 하면 <strong>조인의 on 절에 id 값</strong>이 들어가게 됩니다.</p>
<p>하지만 이번에는 <code>leftJoin(team)</code> 하나 밖에 없는 것을 볼 수 있습니다. 이렇게 하면 <strong>id 매칭이 없어지기 때문에 on 절에 적힌 것처럼 username 와 name 만으로 매칭</strong>됩니다.</p>
<p><br><br></p>
<h2 id="4-4-패치-조인">4-4. 패치 조인</h2>
<pre><code class="language-java">@Test
void fetchJoin() {
    Member findMember = queryFactory
            .selectFrom(member)
            .join(member.team, team)
            .fetchJoin()
            .where(member.username.eq(&quot;member1&quot;))
            .fetchOne();
}</code></pre>
<p>기존과 동일하게 join 을 사용하고, 뒤에 <code>fetchJoin()</code> 을 사용하면 연관된 엔티티까지 한 번에 조회할 수 있습니다.</p>
<p><br><br><br></p>
<h1 id="5-서브쿼리">5. 서브쿼리</h1>
<h2 id="5-1-where-절-서브쿼리">5-1. where 절 서브쿼리</h2>
<pre><code class="language-java">@Test
void subQuery_where() {
    QMember memberSub = new QMember(&quot;memberSub&quot;);

    List&lt;Member&gt; result = queryFactory
                .selectFrom(member)
                .where(member.age.eq(
                        JPAExpressions
                            .select(memberSub.age.max())
                            .from(memberSub)
                ))
                .fetch();
}</code></pre>
<p>QueryDSL 에서 서브쿼리를 사용하려면 <strong>JPAExpressions</strong> 를 사용합니다. <strong>이때 서브쿼리와 메인쿼리의 alias 가 겹치면 안되기 때문에 Q 객체를 새로 생성해서 사용해야 합니다.</strong> 일반적인 SQL 에서 별칭을 다르게 하는 것과 동일한 원리입니다.</p>
<p><code>eq</code> 외에도 앞에서 보았던 <code>in</code>, <code>goe</code> 와 같은 것들도 사용할 수 있습니다.</p>
<p><br><br></p>
<h2 id="5-2-select-절-서브쿼리">5-2. select 절 서브쿼리</h2>
<pre><code class="language-java">@Test
void subQuery_select() {
    QMember memberSub = new QMember(&quot;memberSub&quot;);

    List&lt;Tuple&gt; result = queryFactory
                .select(member.username,
                        JPAExpressions
                                .select(memberSub.age.avg())
                                .from(memberSub))
                .from(member)
                .fetch();
}</code></pre>
<p>하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원합니다. 따라서 Querydsl도 하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원합니다.</p>
<p>JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리는 지원하지 않는데 Hibernate 6 부터 from 절에서의 서브쿼리를 지원합니다. 하지만 QueryDSL 에서는 아직 지원하지 않는 것 같다고 합니다.</p>
<p><br><br><br></p>
<h1 id="6-기타-문법">6. 기타 문법</h1>
<h2 id="6-1-case-문">6-1. Case 문</h2>
<p>case 문은 select, where, order by 에서 사용할 수 있습니다.</p>
<pre><code class="language-java">@Test
void queryDSLCase() {
    // 단순 조건
    List&lt;String&gt; result1 = queryFactory
                .select(member.age
                        .when(10).then(&quot;열살&quot;)
                        .when(20).then(&quot;스무살&quot;)
                        .otherwise(&quot;기타&quot;))
                .from(member)
                .fetch();

    // 복잡한 조건
    List&lt;String&gt; result1 = queryFactory
                .select(new CaseBuilder()
                        .when(member.age.between(0, 20)).then(&quot;0 ~ 20살&quot;)
                        .when(member.age.between(21, 30)).then(&quot;21 ~ 30살&quot;)
                        .otherwise(&quot;기타&quot;))
                .from(member)
                .fetch();
}</code></pre>
<p>복잡한 조건을 사용할 때는 <code>CaseBuilder</code> 를 사용해서 case 조건을 줄 수 있습니다. then 에서 문자가 아닌 숫자를 반환한다면 <strong>CaseBuilder 의 반환형을 따로 뽑아 정렬 기준으로 사용할 수 있습니다.</strong></p>
<p><br><br></p>
<h2 id="6-2-상수">6-2. 상수</h2>
<pre><code class="language-java">Tuple result = queryFactory
        .select(member.username, Expressions.constant(&quot;A&quot;))
        .from(member)
        .fetchFirst();</code></pre>
<p>상수가 필요한 경우 <code>Expressions.constant()</code> 를 사용합니다. </p>
<p><br><br></p>
<h2 id="6-3-문자">6-3. 문자</h2>
<pre><code class="language-java">String result = queryFactory
        .select(member.username.concat(&quot;_&quot;).concat(member.age.stringValue()))
        .from(member)
        .where(member.username.eq(&quot;member1&quot;))
        .fetchOne();</code></pre>
<p>문자를 더할 때는 <code>concat()</code> 을 사용합니다. 또 문자가 아닌 다른 타입을 문자로 변환할 때는 <code>stringValue()</code> 를 사용하는데 특히 ENUM 을 사용할 때 주로 사용됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Data JPA] Spring Data JPA 분석]]></title>
            <link>https://velog.io/@hj_/Spring-Data-JPA-Spring-Data-JPA-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@hj_/Spring-Data-JPA-Spring-Data-JPA-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Fri, 08 Mar 2024 07:23:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한 님의 <a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84/dashboard">실전! 스프링 데이터 JPA</a> 강의를 보고 작성한 내용입니다.</p>
</blockquote>
<br>


<h1 id="1-spring-data-jpa-구현체-분석">1. Spring Data JPA 구현체 분석</h1>
<p>IDE 를 통해 JpaRepository 에서 찾아보면 보면 <strong>SimpleJpaRepository</strong> 가 나오는데 바로 이것이 Spring Data JPA 의 구현체입니다.</p>
<pre><code class="language-java">@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository&lt;T, ID&gt; implements JpaRepositoryImplementation&lt;T, ID&gt; {

    private final JpaEntityInformation&lt;T, ?&gt; entityInformation;
    private final EntityManager entityManager;
    private final PersistenceProvider provider;
    ...
    public SimpleJpaRepository(Class&lt;T&gt; domainClass, EntityManager entityManager) {
        this(JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager), entityManager);
    }
    ...
    @Override
    public Optional&lt;T&gt; findById(ID id) {
        ...
        if (metadata == null) {
            return Optional.ofNullable(entityManager.find(domainType, id));
        }
        ...
        return Optional.ofNullable(type == null ? entityManager.find(domainType, id, hints) : entityManager.find(domainType, id, type, hints));
    }
    ...
}</code></pre>
<h4 id="-entitymanager-">[ EntityManager ]</h4>
<p>해당 클래스를 살펴보면 내부적으로 <code>EntityManager</code> 를 가지고 있는 것을 볼 수 있으며, findById() 를 실행할 때도 em.find() 를 실행해서 가져오는 것을 볼 수 있습니다. 결국 Spring Data JPA 는 JPA 내부 기능들을 활용해서 동작하는 것입니다.</p>
<br>


<h4 id="-repository-">[ @Repository ]</h4>
<p>또 <code>@Repository</code> 가 붙어있는 것을 볼 수 있는데 이로 인해 스프링 빈의 컴포넌트 스캔 대상이 되며, <strong>JDBC 나 JPA 는 다른 예외들이 발생하게 되는데 해당 어노테이션을 사용함으로써 예외가 발생했을 때 스프링에서 사용할 수 있는 예외로 변환</strong>됩니다.</p>
<p>그래서 스프링에서 제공하는 예외가 전달되기 때문에 JDBC, JPA 와 같은 하부 기술을 변경해도 Service 계층에서 예외를 처리하는 로직은 변경하지 않아도 됩니다.</p>
<br>


<h4 id="-transactional-">[ @Transactional ]</h4>
<p>그 다음에 <code>@Transational</code> 이 붙어있는데 Service 계층에서 <code>@Transactional</code> 을 사용했다면 <strong>해당 트랜잭션을 이어 받아서 동작</strong>하지만, 트랜잭션이 없어도 Spring Data JPA 는 자기 리포지토리 계층에서 트랜잭션을 시작합니다. </p>
<p>트랜잭션에 <code>readOnly=true</code> 로 되어 있는데, save 와 같은 메서드를 보면 별도로 readOnly 옵션 없이 트랜잭션 어노테이션이 사용되고 있습니다. 그래서 <code>@Transactional</code> 을 걸지 않아도 Spring Data JPA 가 트랜잭션을 걸고 시작하기 때문에 정상적으로 동작합니다. </p>
<p><strong>하지만 save 를 하고 나오는 순간 영속성 컨텍스트가 사라지기 때문에 영속성 컨텍스트로 인해 사용할 수 있는 기능들을 사용할 수 없게 됩니다.</strong></p>
<p>참고로 데이터를 단순히 조회만 하고 변경하지 않는 트랜잭션에서 <code>readOnly=true</code> 옵션을 사용하면 플러시를 생략해서( 변경감지가 일어나지 않음 ) 약간의 성능 향상을 얻을 수 있다고 합니다.</p>
<p><br><br><br></p>
<h1 id="2-새로운-엔티티를-구별하는-방법">2. 새로운 엔티티를 구별하는 방법</h1>
<h2 id="2-1-save-메서드">2-1. save 메서드</h2>
<p>SimpleJpaRepository 의 save 메서드를 보면 아래와 같습니다.</p>
<pre><code class="language-java">@Transactional
@Override
public &lt;S extends T&gt; S save(S entity) {
    Assert.notNull(entity, &quot;Entity must not be null&quot;);

    if (entityInformation.isNew(entity)) {
        entityManager.persist(entity);
        return entity;
    } else {
        return entityManager.merge(entity);
    }
}</code></pre>
<p>전달 받은 엔티티가 새로운 엔티티라면 <code>persist()</code> 를 호출하고, 새로운 엔티티가 아니면 <code>merge()</code> 를 호출합니다. merge 에 대한 내용은 <a href="https://velog.io/@hj_/JPA-%ED%99%9C%EC%9A%A9-1%ED%8E%B8-%EC%9B%B9-%EA%B3%84%EC%B8%B5-%EA%B0%9C%EB%B0%9C#3-4-merge-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%A4%80%EC%98%81%EC%86%8D-%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%88%98%EC%A0%95">이전 게시글</a>에서 확인할 수 있습니다.</p>
<p>그렇다면 새로운 엔티티인지를 판단하는 기준은 무엇일까요?</p>
<br>



<h2 id="2-2-새로운-엔티티인지-판단하는-기준">2-2. 새로운 엔티티인지 판단하는 기준</h2>
<p>새로운 엔티티를 판단하는 기본 전략은 아래와 같습니다. 참고로 식별자는 <code>persist()</code> 를 하면 엔티티 안에 들어가게 됩니다.</p>
<blockquote>
<ol>
<li><p>식별자가 Long 과 같은 객체일 때 <code>null</code> 로 판단</p>
</li>
<li><p>식별자가 int 와 같은 기본 타입일 때 <code>0</code> 으로 판단</p>
</li>
<li><p>Persistable 인터페이스를 구현해서 판단 로직 변경 가능</p>
</li>
</ol>
</blockquote>
<br>


<h4 id="-generatedvalue-사용-">[ @GeneratedValue 사용 ]</h4>
<pre><code class="language-java">public class Member {
    @Id @GeneratedValue
    private Long id;
}</code></pre>
<p>예를 들면 Member 엔티티의 @Id 에 @GeneratedValue 를 사용하고 save 호출하면 아래처럼 동작합니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/c53bafb2-e96d-4e0b-9234-209dc373e258/image.png" alt=""></p>
<p>save 에 break point 를 찍고, repository.save() 를 호출하도록 한 뒤 디버깅을 돌리면 나오는 모습입니다. Member entity 의 id 값이 <code>null</code> 인 것을 볼 수 있습니다. 그래서 persist() 가 호출됩니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/9012a293-4889-4803-b0ac-40f51f180e3f/image.PNG" alt=""></p>
<p>그 후 한 단계 더 실행하고 나면 Member 의 id 에 1 이라는 값이 들어간 것을 확인할 수 있습니다. 즉, 위에서 언급한 것첢 식별자는 persist() 이후에 엔티티에 들어가게 됩니다.</p>
<br>


<h4 id="-generatedvalue-미사용-">[ @GeneratedValue 미사용 ]</h4>
<pre><code class="language-java">public class Item {
    @Id
    private String id;
}
// ----------------------------
@Test
void save() {
    Item item = new Item(&quot;1&quot;);
    itemRepository.save(item);
}</code></pre>
<p>만약 위처럼 Id 값을 직접 지정하고 save() 를 호출한다면 결과는 아래와 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/183f1c2e-3772-4ebc-b891-2ee749b34b37/image.png" alt=""></p>
<p>동일하게 save 임에도 불구하고 이전과는 다르게 persist() 가 아닌 merge() 를 호출합니다. 왜냐하면 이미 <strong>Item 은 객체이고, 이미 객체에 id 값이 들어있기 때문에 새로운 객체라고 판단하지 않은 것</strong>입니다.</p>
<p><strong><code>merge()</code>는 우선 DB를 호출해서 값을 확인하고, DB에 값이 없으면 새로운 엔티티로 인지</strong>하므로 매우 비효율적입니다. 따라서 <code>Persistable</code> 을 사용해서 새로운 엔티티 확인 여부를 직접 구현하게는 좋습니다.</p>
<p><br><br></p>
<h2 id="2-3-persistable">2-3. Persistable</h2>
<pre><code class="language-java">@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable&lt;String&gt; {
    @Id
    private String id;

    @CreatedDate
     private LocalDateTime createdDate;

    public Item(String id) {
        this.id = id;
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public boolean isNew() {
        return createdDate == null;
    }
}</code></pre>
<p>Persistable 에는 PK 의 타입을 지정합니다. 그리고 getId 와 isNew 를 오버라이딩 해야 하는데 <code>isNew()</code> 메서드에 <strong>어떤 기준으로 새로운 객체인지 판단할 것인지를 작성</strong>하면 됩니다.</p>
<p>강사님이 자주 사용하는 방식은 <code>@CreatedDate</code> 를 사용하는 것이라고 합니다. <code>@CreatedDate</code> 도 JPA 의 이벤트인데, persist 되기 전에 호출됩니다. 그래서 이 값이 null 인지를 기준으로 새로운 객체인지를 판단할 수 있게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/40f211ac-5846-4e32-9b51-3306a9eaba25/image.png" alt=""></p>
<p>그래서 아까와 동일한 테스트를 실행시켰을 때 createdDate 가 null 이기 때문에 새로운 객체라고 판단해서 persist 가 실행됩니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/72571467-f855-49fa-a58d-1af46461c49a/image.png" alt=""></p>
<p>persist 이후에 값을 확인해보면 createdDate 에 값이 정상적으로 들어있는 것을 확인할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Data JPA] 3. 다양한 확장 기능들]]></title>
            <link>https://velog.io/@hj_/Spring-Data-JPA-3.-%EB%8B%A4%EC%96%91%ED%95%9C-%ED%99%95%EC%9E%A5-%EA%B8%B0%EB%8A%A5%EB%93%A4</link>
            <guid>https://velog.io/@hj_/Spring-Data-JPA-3.-%EB%8B%A4%EC%96%91%ED%95%9C-%ED%99%95%EC%9E%A5-%EA%B8%B0%EB%8A%A5%EB%93%A4</guid>
            <pubDate>Fri, 08 Mar 2024 04:59:02 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한 님의 <a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84/dashboard">실전! 스프링 데이터 JPA</a> 강의를 보고 작성한 내용입니다.</p>
</blockquote>
<br>


<h1 id="1-사용자-정의-인터페이스-구현">1. 사용자 정의 인터페이스 구현</h1>
<p>Spring Data JPA 는 인터페이스만 정의하면 구현체는 스프링이 자동으로 생성해줍니다. 인터페이스를 직접 구현하면 개발자가 구현해야 하는 기능이 너무 많습니다. </p>
<p>인터페이스 메서드를 직접 구현하기 위해 JPA 를 직접 사용하거나 MyBatis 를 사용할 수 있도록 사용자 정의 리포지토리라는 기능을 제공합니다.</p>
<p>이 기능은 인터페이스만으로 해결되지 않을 때, 예를 들어 NamedQuery 나 <code>@Query</code> 로 해결할 수 있는 경우가 아니고 복잡한 동적 쿼리를 작성해야 할 때 사용합니다. 대표적으로 QueryDSL 을 사용할 때 사용한다고 합니다.</p>
<br>

<h4 id="-1-인터페이스-생성-">[ 1. 인터페이스 생성 ]</h4>
<pre><code class="language-java">public interface MemberRepositoryCustom {
    List&lt;Member&gt; findMemberCustom();
}</code></pre>
<p>먼저 인터페이스를 생성하고, 사용할 메서드를 정의합니다.</p>
<br>

<h4 id="-2-구현체-생성-">[ 2. 구현체 생성 ]</h4>
<pre><code class="language-java">@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {

    private final EntityManager em;

    @Override
    public List&lt;Member&gt; findMemberCustom() {
        return em.createQuery(&quot;select m from Member m&quot;, Member.class).getResultList();
    }
}</code></pre>
<p>1번에서 구현한 인터페이스를 상속 받아, JPA 를 사용하여 실행할 메서드를 구현합니다.</p>
<p>여기서 중요한 점은 1번의 인터페이스 이름은 아무거나 지정해도 되지만, 구현체의 이름은 <code>JpaRepository 를 상속 받는 인터페이스명 + Impl</code> 형태로 작성해야 합니다.</p>
<p>Spring Data 2.x 부터는 <code>1번에서 구현한 인터페이스명 + Impl</code> ( MemberRepositoryCustomImpl ) 형태도 가능합니다. 강사님은 새롭게 변경된 이 방식이 더 직관적이기 때문에 권장한다고 하십니다.</p>
<br>

<h4 id="-3-인터페이스-상속-">[ 3. 인터페이스 상속 ]</h4>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt;, MemberRepositoryCustom {
    ...
}</code></pre>
<p>Spring Data JPA 인터페이스에 1번에서 만든 인터페이스를 상속 받도록 합니다.</p>
<p>결과적으로 MemberRepository 에서 <code>findMemberCustom()</code> 을 호출하면 2번에서 구현한 메서드가 실행되는데 이 기능은 자바에서 되는건 아니고 Spring Data JPA 가 이렇게 동작하도록 엮어주는 것입니다.</p>
<br>


<h4 id="-참고-">[ 참고 ]</h4>
<p>MemberRepositoryCustom 을 인터페이스가 아닌 클래스로 만들고 스프링 빈으로 등록해서 그냥 직접 사용해도 된다고 합니다. 물론 이 경우 스프링 데이터 JPA와는 아무런 관계 없이 별도로 동작한다고 합니다.</p>
<p><br><br><br></p>
<h1 id="2-auditing">2. Auditing</h1>
<p>엔티티를 생성, 변경할 때 등록일, 등록시간, 수정일, 수정시간과 같이 변경한 사람와 시간을 추적하고 싶을 때 사용합니다.</p>
<h2 id="2-1-순수-jpa-사용">2-1. 순수 JPA 사용</h2>
<pre><code class="language-java">@MappedSuperclass
public class JpaBaseEntity {
    @Column(updatable = false)
    private LocalDateTime createdDate;
    private LocalDateTime updatedDate;

    @PrePersist
    public void prePersist() {
        LocalDateTime now = LocalDateTime.now();
        createdDate = now;
        updatedDate = now;
    }

    @PreUpdate
    public void preUpdate() {
        updatedDate = LocalDateTime.now();
    }
}</code></pre>
<p>생성된 시간에는 <code>updatable = false</code> 라는 옵션을 주었습니다. 이렇게 하면 실수로 <strong>생성시간을 변경해도 값이 업데이트 되지 않습니다.</strong></p>
<p><code>@PrePersist</code> 는 persist 하기 전에 이벤트가 발생하는 것이고, <code>@PreUpdate</code> 는 업데이트 하기 전에 이벤트가 발생하는 것입니다.</p>
<p>JPA 에서 진짜 상속 관계가 있고, 속성만 받는 상속 관계가 존재하는데 지금 같은 경우에는 속성만 받는 상속관계이며, 이런 경우에는 <code>@MappedSuperclass</code> 를 사용합니다.</p>
<p>해당 속성들을 사용할 엔티티에서 <code>extends JpaBaseEntity</code> 를 작성하면 테이블이 실행될 때 createdDate, updateDate 컬럼이 함께 생성됩니다.</p>
<hr>



<pre><code class="language-sql">create table member (
    age integer not null,
    member_id bigint not null,
    team_id bigint,
    username varchar(255),
    primary key (member_id)
)</code></pre>
<p>만약 JpaBaseEntity 에 <code>@MappedSuperclass</code> 어노테이션이 없다면 위처럼 등록날짜, 수정날짜가 빠진 채로 테이블이 생성됩니다.</p>
<br>



<h2 id="2-2-spring-data-jpa-사용">2-2. Spring Data JPA 사용</h2>
<h4 id="-1-엔티티-생성-">[ 1. 엔티티 생성 ]</h4>
<pre><code class="language-java">@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseEntity {
    // 등록일, 수정일
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;

    // 등록자, 수정자
    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @LastModifiedBy
    private String lastModifiedBy;
}</code></pre>
<p>이벤트를 기반으로 동작한다는 것을 넣어주어야 하기 때문에 <code>@EntityListeners(AuditingEntityListener.class)</code> 를 추가합니다.</p>
<p>해당 어노테이션을 추가하지 않고 동작하도록 할 수 있는데 이것은 강의 자료를 참고하시길 바랍니다.</p>
<p>순수 JPA 에서 추가된 점은 등록자, 수정자를 넣은 것인데 <code>@CreatedBy</code>, <code>@LastModifiedBy</code> 를 사용하며, 그냥 두면 값이 들어가지 않고 이에 대한 처리가 필요합니다.</p>
<br>


<h4 id="-2-application-세팅-">[ 2. Application 세팅 ]</h4>
<pre><code class="language-java">@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(DataJpaApplication.class, args);
    }

    @Bean
    public AuditorAware&lt;String&gt; auditorProvider() {
        return () -&gt; Optional.of(UUID.randomUUID().toString());
    }
}</code></pre>
<p>Spring Data JPA 에서 등록시, 수정시를 넣으려면 <code>xxxApplication</code> 에 <code>@EnableJpaAuditing</code> 어노테이션을 넣어야 합니다.</p>
<p>해당 어노테이션을 붙이면 createdDate, lastModifiedDate 에 값이 채워지게 됩니다. 만약 등록할 때 수정시간을 넣고 싶지 않다면 <code>modifyOnCreate = false</code> 옵션을 넣으면 됩니다.</p>
<p>또 등록자, 수정자에 이름을 넣으려면 <code>AuditorAware</code> 를 스프링 빈으로 등록하고, createdBy, lastModifiedBy 에 채워넣을 값을 반환하면 됩니다.</p>
<p>위처럼 세팅해놓으면 <strong>데이터가 등록되거나 수정될 때마다 위에서 생성한 빈을 호출하고 반환하는 값을 꺼내서 createdBy, lastModifiedBy 에 값이 채워지게 됩니다.</strong></p>
<p>예시에서는 간단하게 UUID 를 사용했는데, 저렇게 구현하지 않고 <strong>SecurityContextHolder 에서 인증 객체를 가져와 사용해야 한다고 하셨습니다.</strong></p>
<p><br><br><br></p>
<h1 id="3-도메인-클래스-컨버터">3. 도메인 클래스 컨버터</h1>
<h4 id="-기존-방식-">[ 기존 방식 ]</h4>
<pre><code class="language-java">public class MemberController {
    @GetMapping(&quot;/members/{id}&quot;)
    public String findMember(@PathVariable(&quot;id&quot;) Long memberId) {
        Member member = memberRepository.findById(memberId).get();
        return member.getUsername();
    }
}</code></pre>
<p>기존에는 id 값을 받아서 memberRepository 를 통해 조회하는 방식을 거쳐 Member 엔티티를 가져왔습니다. 하지만 이 과정을 도메인 클래스 컨버터를 통해 간편하게 줄일 수 있습니다.</p>
<br>


<h4 id="-새로운-방식-">[ 새로운 방식 ]</h4>
<pre><code class="language-java">public class MemberController {
    @GetMapping(&quot;/members/{id}&quot;)
    public String findMember(@PathVariable(&quot;id&quot;) Member member) {
        return member.getUsername();
    }
}</code></pre>
<p>HTTP 요청은 회원 id 를 받지만 <strong>도메인 클래스 컨버터가 중간에 리파지토리를 사용해서 엔티티를 찾고, 회원 엔티티 객체를 반환</strong>합니다.</p>
<p>만약 id 값이 존재하지 않는다면 <code>MissingPathVariableException</code> 예외가 발생하게 됩니다.</p>
<p>도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 이 엔티티는 <strong>단순 조회용으로만 사용해야 합니다</strong>. 트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않습니다.</p>
<p><br><br><br></p>
<h1 id="4-페이징과-정렬">4. 페이징과 정렬</h1>
<h2 id="4-1-pageable">4-1. Pageable</h2>
<pre><code class="language-java">public class MemberController {
    @GetMapping(&quot;/members&quot;)
    public Page&lt;Member&gt; list(Pageable pageable) {
        return memberRepository.findAll(pageable);
    }
}</code></pre>
<p><code>findAll()</code> 에 pageable 을 넘기게 되면 PagingAndSortingRepository 의 <code>findAll()</code> 메서드가 실행되며, 이 메서드는 pageable 을 파라미터로 받습니다.</p>
<p><code>findAll(pageable)</code> 은 파라미터로 받은 <code>pageable</code> 객체가 제공한 페이징 제한을 충족하는 엔티티들의 <code>page</code> 를 반환합니다.</p>
<p>Pageable 을 파라미터로 설정하면 쿼리 파라미터로 page, size, sort 와 같은 key 값들을 받을 수 있습니다.</p>
<blockquote>
<p>ex&gt; /members?page=0&amp;size=3&amp;sort=id,desc&amp;sort=username,desc</p>
</blockquote>
<p><strong>쿼리 파라미터들이 Controller 에서 바인딩될 때 pageable 이 있으면 PageRequest 라는 객체를 생성해서 값을 채워넣고 인젝션해줍니다. Pageable 은 인터페이스이고, PageRequest 는 구현체입니다</strong></p>
<br>


<h2 id="4-2-디폴트-설정">4-2. 디폴트 설정</h2>
<p>만약 쿼리 파라미터로 page 나 size 와 같은 것들을 지정하지 않았다면 기본 디폴트값들이 들어가게 되는데 이 디폴드 값들을 변경할 수 있습니다.</p>
<pre><code class="language-yml"># 글로벌 설정
spring:
  data:
    web:
      pageable:
        default-page-size: 10 # 기본 페이지 사이즈
        max-page-size: 1000   # 최대 페이지 사이즈</code></pre>
<pre><code class="language-java">// 개별 설정
public class MemberController {
    @GetMapping(&quot;/members&quot;)
    public Page&lt;Member&gt; list(@PageableDefault(size = 12, sort = &quot;username&quot;
                            direction = Sort.Direction.DESC) Pageable pageable) {
        return memberRepository.findAll(pageable);
    }
}</code></pre>
<p><code>@PageableDefault</code> 어노테이션을 사용해서 개별적으로 설정할 수 있습니다.</p>
<br>

<h2 id="4-3-페이징-정보가-둘-이상인-경우">4-3. 페이징 정보가 둘 이상인 경우</h2>
<pre><code class="language-java">public class MemberController {
    @GetMapping(&quot;/members&quot;)
    public Page&lt;Member&gt; list(@Qualifier(&quot;member&quot;) Pageable memberPageable,
                             @Qualifier(&quot;order&quot;) Pageable orderPageable) {
        return memberRepository.findAll(pageable);
    }
}</code></pre>
<p>페이징 정보가 둘 이상인 경우 <code>@Qualifier</code> 에 접두사를 추가하여 <code>접두사_xxx</code> 를 통해 구분할 수 있습니다.</p>
<blockquote>
<p>ex&gt; /members?member_page=0&amp;order_page=1</p>
</blockquote>
<p><br><br><br></p>
<h1 id="5-query-by-example">5. Query By Example</h1>
<p>JpaRepository 를 보면 <code>QueryByExampleExecutor</code> 를 상속 받는 것을 알 수 있는데 이 인터페이스가 Example 을 파라미터로 받아 쿼리를 수행할 수 있도록 해줍니다.</p>
<p>Query By Example 을 사용하기 위해서 아래 3가지를 알아야 합니다.</p>
<blockquote>
<p><strong>Probe</strong> : 필드에 데이터가 있는 실제 도메인 객체</p>
<p><strong>ExampleMatcher</strong> : 특정 필드를 일치시키는 상세한 정보 제공, 재사용 가능</p>
<p><strong>Example</strong> : Probe와 ExampleMatcher로 구성, 쿼리를 생성하는데 사용</p>
</blockquote>
<br>


<h2 id="5-1-example">5-1. Example</h2>
<h4 id="-사용-예시-">[ 사용 예시 ]</h4>
<pre><code class="language-java">@Test
void test() {
    Member member = new Member(&quot;m1&quot;);
    Example&lt;Member&gt; example = Example.of(member);

    List&lt;Member&gt; result = memberRepository.findAll(example);

    assertThat(result.get(0).getUsername()).isEqualTo(&quot;m1&quot;);
}</code></pre>
<p><strong>Query By Example 은 도메인 자체가 검색 조건이 됩니다. 도메인으로 Example 객체를 생성한 후 이를 repository 에 넘겨주면 도메인 객체를 가지고 검색 조건을 만들게 됩니다.</strong></p>
<hr>



<h4 id="-쿼리-로그-">[ 쿼리 로그 ]</h4>
<pre><code class="language-sql">select ...
from member m1_0 
where m1_0.username=&#39;m1&#39; and m1_0.age=0;</code></pre>
<p>근데 위에서 분명 username 만 사용했는데 where 조건을 보면 <code>age = 0</code> 이라는 조건이 들어있는 것을 볼 수 있습니다.</p>
<p>생성한 member 의 PK 는 null 이기 때문에 무시되는데, age 는 기본 타입이기 때문에 0 이 들어갔기 때문에 무시하지 않은 것입니다.</p>
<br>


<h2 id="5-2-examplematcher">5-2. ExampleMatcher</h2>
<h4 id="-사용-예시-및-쿼리-로그-">[ 사용 예시 및 쿼리 로그 ]</h4>
<p><code>ExampleMatcher</code> 를 사용하여 age 조건을 사용하지 않도록 할 수 있습니다.</p>
<pre><code class="language-java">@Test
void test() {
    Member member = new Member(&quot;m1&quot;);
    ExampleMatcher matcher = ExampleMatcher.matching().withIgnorePaths(&quot;age&quot;);
    Example&lt;Member&gt; example = Example.of(member, matcher);

    List&lt;Member&gt; result = memberRepository.findAll(example);

    assertThat(result.get(0).getUsername()).isEqualTo(&quot;m1&quot;);
}</code></pre>
<pre><code class="language-sql">select ...
from member m1_0 
where m1_0.username=&#39;m1&#39;;</code></pre>
<p>위의 코드는 age 라는 속성을 무시한다는 의미이고, Example 객체를 생성할 때 도메인 객체와 함께 넘겨줍니다. 이때 수행되는 쿼리를 보면 age 가 조건에 없는 것을 확인할 수 있습니다.</p>
<br>


<h2 id="5-3-조인-사용하기">5-3. 조인 사용하기</h2>
<h4 id="-사용-예시-및-쿼리-로그--1">[ 사용 예시 및 쿼리 로그 ]</h4>
<pre><code class="language-java">@Test
void test() {
    // 조인 사용을 위해 연관관계 설정
    Member member = new Member(&quot;m1&quot;);
    Team team = new Team(&quot;teamA&quot;);
    member.setTeam(team);

    ExampleMatcher matcher = ExampleMatcher.matching().withIgnorePaths(&quot;age&quot;);
    Example&lt;Member&gt; example = Example.of(member, matcher);

    List&lt;Member&gt; result = memberRepository.findAll(example);

    assertThat(result.get(0).getUsername()).isEqualTo(&quot;m1&quot;);
}</code></pre>
<pre><code class="language-sql">select ...
from member m1_0 
join team t1_0 on t1_0.team_id=m1_0.team_id 
where t1_0.name=&#39;teamA&#39; and m1_0.username=&#39;m1&#39;;</code></pre>
<p>위처럼 연관관계를 설정하고 쿼리를 수행하면 쿼리를 수행할 때 member 와 team 의 조인이 이루어지고, team 의 이름이 검색조건으로 함께 들어가게 됩니다.</p>
<p><strong>단, 위와 같은 내부 조인( INNER JOIN) 만 가능하고 외부 조인( LEFT JOIN ) 은 불가능합니다. 또 AND 나 OR 를 사용한 중접 제약조건이 불가하며 문자를 제외하면 <code>=</code> 비교밖에 안됩니다.</strong></p>
<p><br><br><br></p>
<h1 id="6-projections">6. Projections</h1>
<p>Spring Data JPA Projections( 프로젝션 )은 JPA 엔터티의 일부 속성만을 선택적으로 조회하고, 이를 DTO 나 인터페이스 등의 특정 타입으로 매핑하는 기능을 말합니다. Projections 을 사용하면 필요한 데이터만을 가져와서 성능을 최적화하거나 데이터 전송 양을 최소화할 수 있습니다.</p>
<p>Spring Data JPA Projections은 인터페이스 기반의 프로젝션과 클래스 기반의 프로젝션 두 가지 유형이 있습니다.</p>
<h4 id="-1-인터페이스-기반-프로젝션-">[ 1. 인터페이스 기반 프로젝션 ]</h4>
<p>인터페이스를 정의하고, 해당 인터페이스의 메소드들을 이용하여 필요한 속성을 지정합니다. 인터페이스의 메소드 이름은 엔티티의 필드나 그에 상응하는 getter 메소드와 일치해야 합니다.</p>
<h4 id="-2-클래스-기반-프로젝션-">[ 2. 클래스 기반 프로젝션 ]</h4>
<p>특정 클래스를 정의하고, 해당 클래스의 생성자를 통해 필요한 속성을 전달합니다. 이 경우, 생성자 매개변수의 이름은 엔티티의 필드와 일치해야 합니다.</p>
<p><br><br></p>
<h2 id="6-1-인터페이스-기반-closed-projections">6-1. 인터페이스 기반 Closed Projections</h2>
<h4 id="-1-반환하고자-하는-컬럼을-가진-인터페이스-생성-">[ 1. 반환하고자 하는 컬럼을 가진 인터페이스 생성 ]</h4>
<pre><code class="language-java">public interface UserNameOnly {
    String getUsername();   // getter 형식
}</code></pre>
<p>조회할 엔티티의 필드를 getter 형식으로 지정하면 해당 필드만 선택해서 조회합니다.</p>
<br>


<h4 id="-2-인터페이스를-반환하는-메서드-생성-">[ 2. 인터페이스를 반환하는 메서드 생성 ]</h4>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    List&lt;UserNameOnly&gt; findProjectionsByUsername(@Param(&quot;username&quot;) String username);
}</code></pre>
<p>반환타입에 1번에서 생성한 인터페이스를 지정합니다. 이렇게 하면 UserNameOnly 인터페이스에 프록시 객체가 담겨서 반환됩니다.</p>
<br>


<h4 id="-3-테스트-코드-">[ 3. 테스트 코드 ]</h4>
<pre><code class="language-java">@Test
void projections() {
    List&lt;UserNameOnly&gt; result = memberRepository.findProjectionsByUsername(&quot;m1&quot;);

    for (UserNameOnly userNameOnly : result) {
        System.out.println(&quot;userNameOnly = &quot; + userNameOnly);
    }
}</code></pre>
<pre><code class="language-sql">select
    m1_0.username 
from
    member m1_0 
where
    m1_0.username=?</code></pre>
<p>위의 코드를 실행했을 때 동작하는 쿼리를 보면 위와 같습니다. select 절을 보면 딱 username 만 가져온 것을 볼 수 있습니다. 또 출력된 결과를 보면 아래와 같습니다.</p>
<blockquote>
<p>userNameOnly = org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap@4936dca9</p>
</blockquote>
<p>인터페이스만 구현하면 구현 클래스는 Spring Data JPA 가 <strong>프록시 기술을 가지고 가짜 객체를 만들고, gerUsername() 을 확인한 후 구현체에 데이터를 담아서 반환</strong>해줍니다.</p>
<p><br><br></p>
<h2 id="6-2-인터페이스-기반-open-projections">6-2. 인터페이스 기반 Open Projections</h2>
<pre><code class="language-java">public interface UserNameOnly {
    @Value(&quot;#{target.username + &#39; &#39; + target.age + &#39; &#39; + target.team.name}&quot;)
    String getUsername();   // getter 형식
}</code></pre>
<p>인터페이스에 정의할 때 <code>@Value</code> 에 스프링의 SpEL 문법을 사용해서 지정할 수 있습니다. 이렇게 하면 지정한 형식에 맞게 반환됩니다.</p>
<p>이렇게 되면 이전처럼 username 만 select 를 하는 것이 아니라 <strong>DB에서 엔티티 필드를 다 조회해온 다음에 위의 형식에 맞게 데이터를 집어넣고 반환</strong>하게 됩니다. 따라서 JPQL select 절 최적화가 이루어지지 않습니다.</p>
<p><br><br></p>
<h2 id="6-3-클래스-기반-프로젝션">6-3. 클래스 기반 프로젝션</h2>
<pre><code class="language-java">public class UsernameOnlyDTO {

    private final String username;

    public UsernameOnlyDTO(String username) {
        this.username = username;
    }

    public String getUsername() {
        return username;
    }
}</code></pre>
<p>인터페이스 말고 클래스 기반으로도 프로젝션이 가능한데 생성자의 파라미터 이름으로 매칭해서 <strong>해당 하는 컬럼만을 프로젝션</strong>합니다.</p>
<p>인터페이스와는 다르게 구체적인 클래스를 명시하기 때문에 <strong>프록시가 아닌 구체적인 클래스의 객체의 생성자에 값을 넣어서 반환</strong>합니다.</p>
<blockquote>
<p>userNameOnly = study.datajpa.example.UsernameOnlyDTO@22f3b213</p>
</blockquote>
<p><br><br></p>
<h2 id="6-4-동적-projections">6-4. 동적 Projections</h2>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    &lt;T&gt; List&lt;T&gt; findProjectionsByUsername(String username, Class&lt;T&gt; type);
}</code></pre>
<p>제네릭 타입을 주게 되면 조금 더 동적으로 데이터를 가져올 수 있습니다. 예를 들어, 어떤 경우에는 유저의 이름을 가져온다던지, 어떤 경우에는 나이를 가져온다던지 할 때 사용하면 편리합니다.</p>
<br>

<h4 id="-사용-예시--1">[ 사용 예시 ]</h4>
<pre><code class="language-java">List&lt;UsernameOnlyDTO&gt; result = 
    memberRepository.findProjectionsByUsername(&quot;m1&quot;, UsernameOnlyDTO.class);</code></pre>
<p><br><br></p>
<h2 id="6-5-중첩-구조">6-5. 중첩 구조</h2>
<h4 id="-1-인터페이스-정의-">[ 1. 인터페이스 정의 ]</h4>
<pre><code class="language-java">public interface NestedClosedProjections {
    String getUsername();
    TeamInfo getTeam();

    interface TeamInfo {
        String getName();
    }
}</code></pre>
<br>


<h4 id="-2-사용-예시-및-쿼리-">[ 2. 사용 예시 및 쿼리 ]</h4>
<pre><code class="language-java">List&lt;NestedClosedProjections&gt; result = 
    memberRepository.findProjectionsByUsername(&quot;m1&quot;, NestedClosedProjections.class);</code></pre>
<pre><code class="language-sql">select
    m1_0.username,
    t1_0.team_id,
    t1_0.name 
from
    member m1_0 
left join team t1_0 
    on t1_0.team_id=m1_0.team_id 
where
    m1_0.username=?</code></pre>
<p>쿼리 로그를 보면 member 는 username 만 가져왔는데 team 은 모든 컬럼들을 조회한 것을 볼 수 있습니다. </p>
<p><strong>프로젝션 대상이 root 엔티티면, JPQL SELECT 절 최적화 가능하지만 root 가 아니면 left outer join 을 처리하고, 모든 필드를 select 해서 엔티티로 조회한 다음에 계산하게 됩니다.</strong></p>
<p>즉, 하나의 엔티티를 넘어가는 순간( 프로젝션 대상이 root 엔티티를 넘어가면 ) JPQL select 최적화가 불가능합니다.</p>
<p><br><br><br></p>
<h1 id="7-native-query">7. Native Query</h1>
<p>네이티브 쿼리는 JPA 가 제공하는 기능입니다. 하지만 가급적 네이티브 쿼리를 사용하지 않는게 좋다고 합니다. </p>
<h2 id="7-1-jpa-네이티브-쿼리">7-1. JPA 네이티브 쿼리</h2>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    @Query(value = &quot;select * from member where username = ?&quot;, nativeQuery = true)
    Member findByNativeQuery(String username);
}</code></pre>
<p>네이티브 쿼리는 반환형이 굉장히 애매합니다. 예를 들어 username 을 가져온다고 했을 때 이는 Member 타입이 아닙니다. </p>
<p>스프링 데이터 JPA 기반 네이티브 쿼리에서 제공하는 반환형은 <code>Object[]</code>, <code>Tuple</code>, <code>DTO( Projections 포함 )</code> 세 가지 입니다.</p>
<p>또 Sort 파라미터를 통한 정렬이 정상 동작하지 않을 수 있으며, 동적 쿼리도 불가하고, <code>@Query</code> 에 작성함에도 불구하고 애플리케이션 로딩 시점에 문법 확인이 불가능합니다.</p>
<br>



<h2 id="7-2-projections-활용">7-2. Projections 활용</h2>
<pre><code class="language-java">@Query(value = &quot;SELECT m.member_id as id, m.username, t.name as teamName &quot; +
                &quot;FROM member m left join team t ON m.team_id = t.team_id&quot;,
        countQuery = &quot;SELECT count(*) from member&quot;,
        nativeQuery = true)
Page&lt;MemberProjection&gt; findByNativeProjection(Pageable pageable);</code></pre>
<p>6번의 Projections 를 사용할 수 있으며, 페이징 처리도 가능합니다. 페이징 처리 시에는 네이티브 쿼리이기 때문에 countQuery 를 직접 작성해주어야 합니다.</p>
<p>과거에는 Object[] 배열로 받았어야 했는데 Projections 기능이 등장하며 반환형으로 인한 문제가 해결되었습니다.</p>
<br>



<h2 id="7-3-동적-네이티브-쿼리">7-3. 동적 네이티브 쿼리</h2>
<pre><code class="language-java">String sql = &quot;select m.username as username from member m&quot;;
List&lt;MemberDto&gt; result = em.createNativeQuery(sql)
        .setFirstResult(0)
        .setMaxResults(10)
        .unwrap(NativeQuery.class)
        .addScalar(&quot;username&quot;)
        .setResultTransformer(Transformers.aliasToBean(MemberDto.class))
        .getResultList();</code></pre>
<p>하이버네이트를 직접 활용해서 동적 네이티브 쿼리를 사용할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Data JPA] 2. 쿼리 메서드 기능]]></title>
            <link>https://velog.io/@hj_/Spring-Data-JPA-2.-%EC%BF%BC%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9C-%EA%B8%B0%EB%8A%A5</link>
            <guid>https://velog.io/@hj_/Spring-Data-JPA-2.-%EC%BF%BC%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9C-%EA%B8%B0%EB%8A%A5</guid>
            <pubDate>Tue, 05 Mar 2024 07:31:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한 님의 <a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84/dashboard">실전! 스프링 데이터 JPA</a> 강의를 보고 작성한 내용입니다.</p>
</blockquote>
<br>


<h1 id="1-쿼리-메소드">1. 쿼리 메소드</h1>
<p>Spring Data JPA 는 쿼리 메소드 기능을 제공하는데 쿼리를 어떤 방식으로 작성할 것인지 3가지 방식을 제공합니다.</p>
<blockquote>
<ol>
<li><p>메소드 이름으로 쿼리 생성</p>
</li>
<li><p>메소드 이름으로 JPA NamedQuery 호출</p>
</li>
<li><p><code>@Query</code> 어노테이션을 사용해서 Repository 인터페이스에 직접 정의</p>
</li>
</ol>
</blockquote>
<br>

<h2 id="1-1-메서드-이름으로-쿼리-생성">1-1. 메서드 이름으로 쿼리 생성</h2>
<p>만약 특정 이름을 가진 유저 중에 n 살 이상인 회원을 조회하고 싶다면 JPA 의 경우 아래처럼 작성해야 합니다.</p>
<pre><code class="language-java">public List&lt;Member&gt; findByUsernameAndAgeGreaterThan(String username, int age) {
    return em.createQuery(
                &quot;select m from Member m where m.username=:username and m.age&gt;:age&quot;, 
                    Member.class)
                .setParameter(&quot;username&quot;, username)
                .setParameter(&quot;age&quot;, age)
                .getResultList();
}</code></pre>
<p>하지만 Spring Data JPA 는 아래처럼 메서드만 정의해도 정상적으로 실행됩니다.</p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    List&lt;Member&gt; findByUsernameAndAgeGreaterThan(String username, int age);
}</code></pre>
<p>이것이 가능한 이유는 <strong>Spring Data JPA 가 메서드 이름을 분석해서 JPQL 을 실행</strong>하기 때문입니다. 이때 몇 가지 규칙들이 존재하는데 위의 예시로 살펴보면 다음과 같습니다.</p>
<blockquote>
<ol>
<li><p>UsernameAndAge : where 절에서 and 조건으로 묶인다</p>
</li>
<li><p>Age 뒤에 GreaterThan 이 붙었기 때문에 <code>&gt;</code> 로 들어간다</p>
</li>
<li><p>Username 은 다른거 없이 Username 만 사용해서 <code>=</code> 로 들어간다</p>
</li>
</ol>
</blockquote>
<p>그래서 결과적으로 실행된 쿼리 로그는 아래와 같습니다. 사용할 수 있는 규칙들은 <a href="https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html">공식문서</a>에서 확인하실 수 있습니다.</p>
<pre><code class="language-sql">select
    m1_0.member_id,
    m1_0.age,
    m1_0.team_id,
     m1_0.username 
from
    member m1_0 
where
    m1_0.username=? 
    and m1_0.age&gt;?</code></pre>
<br>


<h2 id="1-2-메소드-이름으로-jpa-namedquery-호출">1-2. 메소드 이름으로 JPA NamedQuery 호출</h2>
<p>JPA 강의에서 NamedQuery 에 대해서 알아보았습니다. 이 NamedQuery 를 JPA 에서도, Spring Data JPA 에서도 호출할 수 있는데 이를 알아보도록 하겠습니다.</p>
<h4 id="-namedquery-">[ NamedQuery ]</h4>
<pre><code class="language-java">@Entity
@NamedQuery(
    name=&quot;Member.findByUsername&quot;,
    query=&quot;select m from Member m where m.username = :username&quot;)
public class Member {
    ...
}</code></pre>
<hr>


<h4 id="-jpa-에서-호출-">[ JPA 에서 호출 ]</h4>
<pre><code class="language-java">public List&lt;Member&gt; findByUsername(String username) {
    return em.createNamedQuery(&quot;Member.findByUsername&quot;, Member.class)
                        .setParameter(&quot;username&quot;, username)
                        .getResultList();
}</code></pre>
<p><code>createNamedQuery()</code> 를 통해 NamedQuery 를 호출할 수 있습니다.</p>
<hr>


<h4 id="-spring-data-jpa-에서-호출-">[ Spring Data JPA 에서 호출 ]</h4>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    @Query(name = &quot;Member.findByUsername&quot;)
    List&lt;Member&gt; findByUsername(@Param(&quot;username&quot;) String username);
}</code></pre>
<p><code>@Query</code> 어노테이션을 사용하고, <strong>name</strong> 속성 내부에 NamedQuery 의 이름을 작성하면 됩니다.</p>
<p>Spring Data JPA 는 선언한 <code>도메인 클래스 + . + 메서드 이름</code>으로 NamedQuery 를 찾아서 실행합니다.</p>
<p>만약 실행할 NamedQuery 가 없다면 메서드 이름으로 쿼리 생성 전략을 사용합니다.</p>
<p>실무에서 Named Query를 직접 등록해서 사용하는 일은 드물고 <strong><code>@Query</code> 를 사용해서 리파지토리 메소드에 쿼리를 직접 정의</strong>한다고 합니다.</p>
<br>


<h2 id="1-3-query-를-사용해서-인터페이스에-직접-정의">1-3. @Query 를 사용해서 인터페이스에 직접 정의</h2>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    @Query(&quot;select m from Member m where m.username= :username and m.age = :age&quot;)
    List&lt;Member&gt; findUser(@Param(&quot;username&quot;) String username, @Param(&quot;age&quot;) int age);
}</code></pre>
<p><code>@Query</code> 어노테이션을 이용하는 방법으로 실행할 메서드에 JPQL 을 이용해 정적 쿼리를 직접 작성합니다.</p>
<p>createQuery 에서 JPQL 을 작성하는 것은 문자이기 때문에 오타가 있어도 오류가 발생하지 않는데 <code>@Query</code> 는 JPA Named 쿼리처럼 <strong>애플리케이션 실행 시점에 문법 오류를 발견할 수 있다는 장점</strong>이 있습니다.</p>
<p><br><br><br></p>
<h1 id="2-query">2. @Query</h1>
<h2 id="2-1-조회하기">2-1. 조회하기</h2>
<h4 id="-값-조회하기-">[ 값 조회하기 ]</h4>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    @Query(&quot;select m.username from Member m&quot;)
    List&lt;String&gt; findUsernameList();
}</code></pre>
<p>엔티티에서 특정한 값만을 가져오고 싶을 때 사용합니다.</p>
<hr>


<h4 id="-dto-조회하기-">[ DTO 조회하기 ]</h4>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    @Query(&quot;select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) &quot; +
            &quot;from Member m join m.team t&quot;)
    List&lt;MemberDto&gt; findMemberDto();
}</code></pre>
<p>DTO 를 바로 조회할 수 있는데 이때는 <strong>반드시 <code>new</code> 키워드를 사용해야 하고, 패키지경로까지 함께 적어주어야 합니다.</strong> 또한 DTO 에는 <strong>전달 받을 컬럼들을 가진 생성자가 필요</strong>합니다.</p>
<br>


<h2 id="2-2-파라미터-바인딩">2-2. 파라미터 바인딩</h2>
<p>파라미터 바인딩은 JPA 강의에서 나온 것처럼 위치 기반과 이름 기반이 있는데 코드 가독성과 유지보수를 위해 <strong>이름 기반 파라미터 바인딩을 사용하는 것이 좋다고 합니다.</strong></p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    @Query(&quot;select m from Member m where m.username in :names&quot;)
    List&lt;Member&gt; findByNames(@Param(&quot;names&quot;) List&lt;String&gt; names);
}</code></pre>
<p>Spring Data JPA 에서는 <code>@Param</code> 을 이용해서 파라미터 바인딩을 할 수 있습니다. 또 <strong>컬렉션을 넘겨주어 IN 연산자를 사용할 수 있습니다.</strong></p>
<p><br><br><br></p>
<h1 id="3-반환타입">3. 반환타입</h1>
<pre><code class="language-java">Member findByUsername(String name); // 엔티티
Optional&lt;Member&gt; findByUsername(String name); // 엔티티 Optional
List&lt;Member&gt; findByUsername(String name); // 엔티티 컬렉션</code></pre>
<p>Spring Data JPA 는 여러 가지 반환 타입을 사용할 수 있습니다. <strong>컬렉션을 반환하는 경우 결과가 없다면 빈 컬렉션이 반환됩니다.</strong></p>
<p>하지만 하나만 조회하는 경우, JPQL 의 <code>getSingleResult()</code> 를 호출하는데, 이 메서드는 <strong>조회 결과가 없을 때 <code>NoResultException</code> 이 발생하는데 이를 <code>null</code> 로 반환</strong>해줍니다. </p>
<p>2개 이상이라면 <code>NonUniqueResultException</code> 예외가 발생합니다. 이때 Spring Data JPA 가 Spring 의 <code>IncorrectResultSizeDataAccessException</code> 으로 변환해서 반환합니다.</p>
<p>Repository 에 사용하는 기술은 JPA 나 몽고DB 와 같은 기술들을 사용할 수 있는데, 서비스 계층에서 해당 기술에 의존하는게 아니라 <strong>스프링이 추상화한 예외에 의존하면 스프링은 동일하게 데이터가 맞지 않으면 Repository 에 사용하는 기술을 변경해도 해당 예외를 발생시키기 때문에 서비스 코드를 변경하지 않아도 된다는 장점</strong>이 있습니다.</p>
<p><br><br><br></p>
<h1 id="4-spring-data-jpa-페이징과-정렬">4. Spring Data JPA 페이징과 정렬</h1>
<h4 id="-jpa-">[ JPA ]</h4>
<pre><code class="language-java">// Repository
public List&lt;Member&gt; findByPage(int age, int offset, int limit) {
    return em.createQuery(
        &quot;select m from Member m where m.age = :age order by m.username desc&quot;, Member.class)
            .setParameter(&quot;age&quot;, age)
            .setFirstResult(offset)
            .setMaxResults(limit)
            .getResultList();
}

public long totalCount(int age) {
        return em.createQuery(
            &quot;select count(m) from Member m where m.age = :age&quot;, Long.class)
                .setParameter(&quot;age&quot;, age)
                .getSingleResult();
    }</code></pre>
<p>JPA 에서는 페이징을 할 때 offset 과 limit 를 사용하는데 이는 <strong>어디서부터 시작해서( offset ) 몇 개를 가져올지( limit )</strong>를 나타냅니다. </p>
<p>이렇게 JPQL 을 작성해놓으면 JPA 는 방언을 기반으로 동작하기 때문에 현재 DB 에 맞는 SQL 을 생성하고 실행하여 DB 에서 페이징 처리를 한 데이터를 가져옵니다.</p>
<p>또 페이징 처리에는 총 몇 개의 데이터가 있는지도 필요한데 이를 count 쿼리를 통해 구할 수 있는데 Spring Data JPA 는 이러한 것들을 편리하게 사용할 수 있도록 제공해줍니다.</p>
<br>

<h2 id="4-1-파라미터와-반환-타입">4-1. 파라미터와 반환 타입</h2>
<h4 id="-페이징과-정렬-파라미터-">[ 페이징과 정렬 파라미터 ]</h4>
<blockquote>
<p>org.springframework.data.domain.Sort : 정렬 기능
org.springframework.data.domain.Pageable : 페이징 기능 ( 내부에 Sort 포함 )</p>
</blockquote>
<p>패키지를 보면 springframework.data 인 것을 볼 수 있는데 이것은 <strong>DB 에 관계없이 페이징을 공통화 시켰다</strong>는 의미입니다.</p>
<hr>


<h4 id="-반환-타입-">[ 반환 타입 ]</h4>
<blockquote>
<p>org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징
org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능
List : 추가 count 쿼리 없이 결과만 반환</p>
</blockquote>
<p><code>Page</code> 는 totalCount 와 같이 페이징 처리에 필요한 데이터를 가지고 있습니다. 데이터를 가져오는 쿼리와 count 쿼리가 함께 실행됩니다.</p>
<p><code>Slice</code> 는 TotalCount 가 필요 없는 페이징 처리할 때 사용하는데 무한 스크롤 같은 곳에서 활용할 수 있는 것 같습니다.</p>
<p><code>List</code> 를 반환타입으로 사용하면 페이징 처리와 관련된 함수들은 사용할 수 없고 페이징 처리된 데이터만 가져오게 됩니다.</p>
<br>


<h2 id="4-2-page-반환">4-2. Page 반환</h2>
<h4 id="-repository-">[ Repository ]</h4>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    Page&lt;Member&gt; findByAge(int age, Pageable pageable);
}</code></pre>
<p><code>Pageable</code> 을 파라미터로 전달 받게 되는데 내부에 몇 번 페이지인지와 같은 정보가 들어있고, 반환 타입은 <code>Page</code> 로 작성합니다.</p>
<hr>


<h4 id="-테스트-코드-">[ 테스트 코드 ]</h4>
<pre><code class="language-java">@Test
void paging() {
    // Pageable 구현체
    PageRequest pageRequest = PageRequest
                                .of(0, 3, Sort.by(Sort.Direction.DESC, &quot;username&quot;));

    Page&lt;Member&gt; page = memberRepository.findByAge(10, pageRequest);    // 쿼리 실행 결과

    List&lt;Member&gt; content = page.getContent(); // 조회된 데이터

    assertThat(content.size()).isEqualTo(3); // 조회된 데이터 수
    assertThat(page.getTotalElements()).isEqualTo(5); // 전체 데이터 수
    assertThat(page.getNumber()).isEqualTo(0); // 페이지 번호
    assertThat(page.getTotalPages()).isEqualTo(2); // 전체 페이지 번호
    assertThat(page.isFirst()).isTrue(); // 첫번째 항목인가?
    assertThat(page.hasNext()).isTrue(); // 다음 페이지가 있는가?
}</code></pre>
<p>Spring Data JPA 에서 페이징은 페이지 번호가 0부터 시작합니다. <code>PageRequest</code> 를 만들어 넘겨주었는데 이를 따라가보면 Pageable 인터페이스를 구현한 것임을 알 수 있습니다.</p>
<p>이렇게 Pageable 인터페이스만 전달하면 페이징 처리에 필요한 모든 데이터가 담겨서 들어오게 됩니다.</p>
<hr>


<h4 id="-쿼리-로그-">[ 쿼리 로그 ]</h4>
<pre><code class="language-sql">select m1_0.member_id,m1_0.age,m1_0.team_id,m1_0.username 
from member m1_0 
where m1_0.age=10 
order by m1_0.username desc 
offset 0 rows fetch first 3 rows only;
----------------------------------
select
    count(m1_0.member_id) 
from
    member m1_0 
where
    m1_0.age=10
</code></pre>
<p>데이터를 가져오는 쿼리를 수행하고 난 후에 자동으로 count 쿼리가 실행된 것을 확인할 수 있습니다.</p>
<p>위에서 언급한 것처럼 <code>Page</code> 를 반환할 때는 count 쿼리가 추가로 실행되고, 실행 결과 데이터가 <code>Page</code> 에 담기게 됩니다.</p>
<br>


<h2 id="4-3-slice-반환">4-3. Slice 반환</h2>
<p>Slice 는 totalCount 를 가지고 오지 않기 때문에 count 쿼리가 추가로 실행되지 않습니다. </p>
<p>또 slice 는 쿼리를 수행할 때 만약 3개를 요청했으면 1개를 더해서 4개를 요청하게 됩니다.</p>
<hr>


<h4 id="-쿼리-로그--1">[ 쿼리 로그 ]</h4>
<pre><code class="language-sql">select m1_0.member_id,m1_0.age,m1_0.team_id,m1_0.username 
from member m1_0 
where m1_0.age=10 
order by m1_0.username desc 
offset 0 rows fetch first 4 rows only;</code></pre>
<p>위의 코드에서 Page 를 반환하지 않고 Slice 를 반환하면 쿼리가 위처럼 실행됩니다. 3개를 요청했는데 4개의 데이터를 가져오는 것을 확인할 수 있습니다.</p>
<p>또 count 쿼리가 나가지 않기 때문에 <code>getTotalElements()</code>, <code>getTotalPages()</code> 를 사용할 수 없게 됩니다.</p>
<br>



<h2 id="4-4-count-쿼리-분리">4-4. count 쿼리 분리</h2>
<pre><code class="language-java">@Query(value = &quot;select m from Member m left join m.team t&quot;)
Page&lt;Member&gt; findByAge(int age, Pageable pageable);</code></pre>
<p>쿼리에 조인이 걸려있고, 이를 Page 로 반환하면 총 데이터 개수를 가져오기 위해 count 쿼리를 수행할 때도 조인을 수행하게 됩니다.</p>
<p><strong>하지만 하이버네이트 6 부터는 이렇게 의미없는 left join을 최적화 해버리기 때문에 조인을 수행하지 않습니다.</strong></p>
<hr>


<h4 id="-쿼리-로그--2">[ 쿼리 로그 ]</h4>
<pre><code class="language-sql">select m1_0.member_id,m1_0.age,m1_0.team_id,m1_0.username 
from member m1_0 
order by m1_0.username desc 
offset 0 rows fetch first 3 rows only;
--------------------------------------------------------------
select count(m1_0.member_id) 
from member m1_0;</code></pre>
<p>쿼리 로그를 살펴보면 join 을 통해 조회하는데 join 이 실행되지 않은 것을 볼 수 있습니다. 왜냐하면 JPQL 을 보았을 때 <strong>Team 과 조인을 하지만 전혀 사용하지 않고 있기 때문</strong>입니다.</p>
<p>만약 select 나 where 절에서 <strong>team 에 대한 조건을 사용하거나 left join fetch 를 사용</strong>하면 join 되는 쿼리가 수행됩니다.</p>
<hr>


<h4 id="-join-fetch-쿼리-로그-">[ join fetch 쿼리 로그 ]</h4>
<pre><code class="language-sql">select m1_0.member_id,m1_0.age,t1_0.team_id,t1_0.name,m1_0.username 
from member m1_0 
left join team t1_0 on t1_0.team_id=m1_0.team_id 
order by m1_0.username desc 
offset 0 rows fetch first 3 rows only;
--------------------------------------------------------------
select count(m1_0.member_id) 
from member m1_0;</code></pre>
<p>하지만 left join fetch 를 사용해도 <strong>count 쿼리를 수행할 때 join 을 하지 않는 것은 동일</strong>합니다. 이유는 앞서 말했듯이 하이버네이트 6 부터는 이렇게 의미없는 left join을 최적화 해버리기 때문입니다.</p>
<hr>


<h4 id="-countquery-별도-지정-방법-">[ countQuery 별도 지정 방법 ]</h4>
<pre><code class="language-java">@Query(value = &quot;select m from Member m left join m.team t&quot;, 
        countQuery = &quot;select count(m.username) from Member m&quot;)
Page&lt;Member&gt; findByAge(int age, Pageable pageable);</code></pre>
<p>Page 를 반환하면서 <code>@Query</code> 어노테이션을 사용해서 <strong>countQuery</strong> 속성에 count 쿼리를 분리해서 사용할 수 있습니다.</p>
<br>


<h2 id="4-5-페이징을-유지하면서-dto-로-변환하기">4-5. 페이징을 유지하면서 DTO 로 변환하기</h2>
<pre><code class="language-java">Page&lt;Member&gt; page = memberRepository.findByAge(10, pageRequest);
Page&lt;MemberDto&gt; dtoPage = page.map(m -&gt; new MemberDto());</code></pre>
<p>page 내부에는 member 가 있기 때문에 <code>map()</code> 을 통해 member 를 MemberDto 로 변환할 수 있습니다.</p>
<hr>


<h4 id="-사이드-프로젝트에-적용-">[ 사이드 프로젝트에 적용 ]</h4>
<pre><code class="language-java">// 변경 전 코드
Page&lt;Comment&gt; pagingData = commentRepository.findAllByParticipationId(participationId, pageable);
List&lt;Comment&gt; entityList = pagingData.getContent();
List&lt;CommentResponseDTO&gt; dtoList = entityList.stream().map(CommentResponseDTO::new).toList();

// 변경 후 코드
Page&lt;Comment&gt; pagingData = commentRepository.findAllByParticipationId(participationId, pageable);
List&lt;CommentResponseDTO&gt; dtoList1 = pagingData.map(CommentResponseDTO::new).stream().toList();</code></pre>
<p>위 코드는 제가 사이드 프로젝트를 진행하면서 작성한 코드인데 이 방법을 몰라서 <code>getContent()</code> 로 데이터를 가져오고, 이를 반복하며 DTO 로 변환해주는 과정을 했었습니다. </p>
<p>저는 DTO 내부에 세팅해야 하는 데이터가 있어서 List 로 변환했는데 <code>page.map()</code> 을 통해 변환하고, 반환할 때 <code>getContent()</code> 를 쓰는 것도 좋을 것 같습니다. 데이터 말고 Page 자체를 반환해도 됩니다.</p>
<p><br><br><br></p>
<h1 id="5-벌크성-수정-쿼리">5. 벌크성 수정 쿼리</h1>
<p>JPA 에서는 변경 감지 기능이 있어서 엔티티의 데이터를 변경하면 트랜잭션 커밋 시점에 update 쿼리가 수행됩니다. </p>
<p>이 방식은 하나씩 하는 방법이고, 여러 건의 데이터를 한 번에 변경하고 싶을 때는 벌크 연산을 사용합니다.</p>
<p>JPA 기본편에서 다루었는데 <code>createQuery()</code> 로 JPQL 을 작성하고, <code>executeUpdate()</code> 를 통해 수행합니다.</p>
<hr>


<h4 id="-repository--1">[ Repository ]</h4>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    @Modifying
    @Query(&quot;update Member m set m.age = m.age + 1 where m.age &gt;= :age&quot;)
    int bulkAgePlus(@Param(&quot;age&quot;) int age);
}</code></pre>
<p>반환되는 데이터는 영향을 받은 로우 수이며, 타입은 int 로 설정합니다.</p>
<p>벌크성 수정, 삭제 쿼리는 반드시 <code>@Modifying</code> 어노테이션을 사용해야 하며, 사용하지 않는다면 <code>QueryExecutionRequestException</code> 이 발생하게 됩니다. 왜냐하면 해당 어노테이션이 없다면 <code>executeUpdate()</code> 를 호출하는 것이 아닌 <code>getResultList()</code> 나 <code>getSingleResult()</code> 를 호출하기 때문입니다.</p>
<p><strong>벌크 연산은 영속성 컨텍스트를 무시하고 DB 에 직접 쿼리가 날라가게 되는데 기존에 영속성 컨텍스트에 존재하던 엔티티의 데이터는 변경되지 않습니다. 그래서 벌크 연산 수행 후에는 반드시 영속성 컨텍스트를 초기화</strong>해야 하는데 이때 사용하는 옵션이 clearAutomatically 입니다.</p>
<p><code>@Modifying(clearAutomatically = true)</code> 로 설정하면 벌크성 쿼리를 실행한 후에 영속성 컨텍스트를 초기화합니다. 디폴트는 false 입니다.</p>
<p><br><br><br></p>
<h1 id="6-entitygraph">6. @EntityGraph</h1>
<h2 id="6-1-지연로딩과-fetch-join">6-1. 지연로딩과 fetch join</h2>
<p>연관관계를 지연로딩으로 설정하면 처음 조회할 때는 지연 로딩이 걸린 엔티티에 대해서는 쿼리가 실행되지 않고 실제 사용되는 시점에 쿼리가 수행됩니다. 이 경우 <code>N + 1 문제</code>가 발생합니다.</p>
<p>fetch join 을 사용하면 연관된 엔티티까지 한 번의 쿼리로 가져오는데 이때는 프록시 객체를 가져오는 것이 아닌 진짜 엔티티를 가져오게 됩니다. ( <a href="https://velog.io/@hj_/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-10.-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%BF%BC%EB%A6%AC-JPQL-%EC%A4%91%EA%B8%89-%EB%AC%B8%EB%B2%95#2-%EC%97%94%ED%8B%B0%ED%8B%B0-%ED%8C%A8%EC%B9%98-%EC%A1%B0%EC%9D%B8">이전 게시글</a> 참고 )</p>
<p>Spring Data JPA 에서 <code>@Query</code> 내부에 <code>join fetch</code> 를 사용해서 패치조인을 수행할 수 있지만, 이 방법 외에도 연관된 엔티티를 한 번에 조회할 수 있는 기능을 제공합니다.</p>
<br>



<h2 id="6-2-entitygraph-사용">6-2. @EntityGraph 사용</h2>
<h4 id="1-공통-메서드-오버라이드">1. 공통 메서드 오버라이드</h4>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    @Override
    @EntityGraph(attributePaths = {&quot;team&quot;})
    List&lt;Member&gt; findAll();
}</code></pre>
<p>JpaRepository 의 <code>findAll()</code> 메서드를 오버라이딩하면서 <code>@EntityGraph</code> 를 사용하는 방법입니다. <strong>attributePaths</strong> 에는 연관관계가 설정된 이름을 지정하면 됩니다.</p>
<p>위의 메서드를 실행하면 Member 를 조회하면서 연관된 Team 까지 함께 조회하게 됩니다.</p>
<hr>



<h4 id="2-jpql과-함께-사용">2. JPQL과 함께 사용</h4>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    @EntityGraph(attributePaths = {&quot;team&quot;})
    @Query(&quot;select m from Member m&quot;)
    List&lt;Member&gt; findMemberEntityGraph();
}</code></pre>
<p><code>@Query</code> 에 JPQL 을 작성하고, <code>@EntityGraph</code> 를 적용하는 방법도 가능합니다.</p>
<hr>


<h4 id="3-메서드-이름으로-쿼리-생성-시-사용">3. 메서드 이름으로 쿼리 생성 시 사용</h4>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    @EntityGraph(attributePaths = {&quot;team&quot;})
    List&lt;Member&gt; findByUsername(String username)
}</code></pre>
<p>메서드 이름으로 쿼리가 자동으로 생성되게 설정한 후에 <code>@EntityGraph</code> 를 적용하는 방법도 가능합니다.</p>
<br>


<h2 id="6-3-namedentitygraph">6-3. @NamedEntityGraph</h2>
<p><code>@EntityGraph</code> 는 JPA 에서 제공하는 기능입니다. JPA 에서 이 방법 외에도 <code>@NamedEntityGraph</code> 도 제공합니다.</p>
<pre><code class="language-java">@NamedEntityGraph(name = &quot;Member.all&quot;, 
                attributeNodes = @NamedAttributeNode(&quot;team&quot;))
@Entity
public class Member {
    ...
}</code></pre>
<p><code>@NamedEntityGraph</code> 에 이름을 지정하고, <code>@NamedAttributeNode</code> 에 연관관계가 설정된 필드명을 지정합니다.</p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    @EntityGraph(&quot;Member.all&quot;)
    @Query(&quot;select m from Member m&quot;)
    List&lt;Member&gt; findMemberEntityGraph();
}</code></pre>
<p>그 후 Repository 에서 <code>@NamedEntityaGraph</code> 에서 지정한 이름으로 EntityGraph 기능을 사용할 수 있습니다.</p>
<p><br><br><br></p>
<h1 id="7-jpa-hint--lock">7. JPA Hint &amp; Lock</h1>
<h2 id="7-1-jpa-쿼리-힌트">7-1. JPA 쿼리 힌트</h2>
<p>JPA 쿼리 힌트는 SQL 힌트가 아닌 JPA 구현체( 하이버네이트 )에게 제공하는 힌트를 의미합니다. 힌트가 무엇인지는 예제를 통해 살펴보겠습니다.</p>
<h4 id="-repository--2">[ Repository ]</h4>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    @QueryHints(value = @QueryHint(name = &quot;org.hibernate.readOnly&quot;, value = &quot;true&quot;))
    Member findReadOnlyByUsername(String username);
}</code></pre>
<p>하이버네이트는 <strong>readOnly</strong> 라는 기능을 제공하는데 이를 true 로 설정했을 때 테스트와 쿼리 실행 로그를 살펴보겠습니다.</p>
<hr>


<h4 id="-테스트-코드--1">[ 테스트 코드 ]</h4>
<pre><code class="language-java">@Test
public void queryHint() throws Exception {
    memberRepository.save(new Member(&quot;member1&quot;, 10));
    em.flush();
    em.clear();

    Member member = memberRepository.findReadOnlyByUsername(&quot;member1&quot;);
    member.setUsername(&quot;member2&quot;);
    em.flush(); // Update Query 실행 X
}</code></pre>
<pre><code class="language-sql">select m1_0.member_id,m1_0.age,m1_0.team_id,m1_0.username 
from member m1_0 
where m1_0.username=&#39;member1&#39;;</code></pre>
<p>Member 를 저장하고, 영속성 컨텍스트를 초기화시킨 후에 조회합니다. 그 후 <strong>Member 를 변경하는데 수행되는 쿼리를 보면 select 쿼리만 수행되고, update 쿼리를 수행되지 않는 것을 볼 수 있습니다.</strong></p>
<p><code>findById()</code> 를 사용해서 조회한 후에 변경을 하면 당연히 update 쿼리가 수행됩니다. 이때 변경감지가 일어나는데 변경 감지 기능을 수행하기 위해서는 결국 비교할 대상이 되는 객체도 가지고 있어야 합니다. 내부적으로 최적화가 되어 있어도 결국 <strong>사용하는 객체와 기준이 되는 객체 2가지를 관리해야 하기 때문에 결국 메모리를 사용</strong>하게 됩니다.</p>
<p>하지만 위처럼 readOnly 를 설정한 메서드로 가지고 오게 되면 변경 감지를 하지 않기 때문에 <strong>변경 감지에 사용하는 스냅샷을 만들지 않게 되고, 성능 최적화가 이루어지게 되는 것입니다.</strong></p>
<br>


<h2 id="7-2-lock">7-2. Lock</h2>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    List&lt;Member&gt; findLockByUsername(String name);
}</code></pre>
<p>JPA 가 Lock 을 지원하고, Spring Data JPA 에서 편리하게 사용할 수 있도록 어노테이션을 지원합니다.</p>
<pre><code class="language-sql">select m1_0.member_id, m1_0.age, m1_0.team_id, m1_0.username 
from member m1_0 
where m1_0.username=? for update</code></pre>
<p>쿼리 로그를 보면 자동으로 뒤에 for update 라는 키워드가 붙은 것을 확인할 수 있습니다. select 를 하는 중에 다른 사람들이 손댈 수 없도록 lock 을 걸어놓은 것입니다. 방언에 따라서 동작 방식은 달라지게 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Data JPA] 1. 공통 인터페이스]]></title>
            <link>https://velog.io/@hj_/Spring-Data-JPA-1.-%EA%B3%B5%ED%86%B5-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-9qyopfe3</link>
            <guid>https://velog.io/@hj_/Spring-Data-JPA-1.-%EA%B3%B5%ED%86%B5-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-9qyopfe3</guid>
            <pubDate>Thu, 29 Feb 2024 06:45:52 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한 님의 <a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84/dashboard">실전! 스프링 데이터 JPA</a> 강의를 보고 작성한 내용입니다.</p>
</blockquote>
<br>


<h1 id="1-공통-인터페이스">1. 공통 인터페이스</h1>
<h2 id="1-1-인터페이스">1-1. 인터페이스</h2>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; { }</code></pre>
<p>Spring Data JPA 를 사용할 때는 위처럼 <code>JpaRepository</code> 인터페이스를 상속받는 인터페이스를 만들어 사용합니다. 이 인터페이스를 사용하면 이전에 JPA 를 이용한 구현체가 하던 작업들을 수행할 수 있습니다.</p>
<p>인터페이스만 만들고, 구현체는 만들지 않았는데 테스트 코드에서 MemberRepository 를 주입 받고, 출력해보면 아래처럼 결과가 나오게 됩니다.</p>
<blockquote>
<p>memberRepository = class jdk.proxy3.$Proxy122</p>
</blockquote>
<p>구현체를 만들지 않았는데 프록시 객체가 출력됩니다. 이것은 <strong>Spring Data JPA 가 인터페이스를 보고 구현 클래스를 만들어서 넣어준 것</strong>입니다. </p>
<hr>


<p><img src="https://velog.velcdn.com/images/hj_/post/bdbfa129-a85b-4fe2-8348-e4e975021a58/image.png" alt=""></p>
<p>이를 그림으로 보면 위와 같은데 <strong>애플리케이션 로딩 시점에 Spring Data JPA 관련 인터페이스를 가지고 있으면 구현 클래스를 만들게 됩니다.</strong></p>
<p><code>@Repository</code> 은 컴포넌트 스캔의 대상이 되도록 하고, JPA 예외를 스프링 예외로 변환해주는데 Spring Data JPA 가 컴포넌트 스캔까지 자동으로 처리해주기 때문에 <code>@Repository</code> 를 생략할 수 있고, 예외 변환도 자동으로 처리해줍니다.</p>
<br>



<h2 id="1-2-인터페이스-분석">1-2. 인터페이스 분석</h2>
<pre><code class="language-java">package org.springframework.data.jpa.repository;

@NoRepositoryBean
public interface JpaRepository&lt;T, ID&gt; extends ListCrudRepository&lt;T, ID&gt;, 
                                            ListPagingAndSortingRepository&lt;T, ID&gt;, 
                                            QueryByExampleExecutor&lt;T&gt; {
    ...
}</code></pre>
<p>Repositroy 인터페이스를 생성할 때 상속 받는 JpaRepository 를 살펴보면 위와 같습니다. <strong>T 는 엔티티 타입을 의미하고, ID 는 식별자 타입</strong>을 의미합니다.</p>
<p>패키지를 보면 <code>springframework.data.jpa</code> 인 것을 볼 수 있는데 Spring data 프로젝트는 기본적으로 공통의 CRUD 를 제공하는데 그 중에서 <strong>JPA 특화된 기능들이 있는 곳이 JpaRepository</strong> 입니다.</p>
<p>JpaRepository 가 상속 받는 ListPagingAndSortingRepository 를 보면 <code>springframework.data.repository</code> 패키지에 속해있는 것을 확인할 수 있는데 paging 과 sorting 은 어떤 RDB 에서도 제공하는 기본 기능이기 때문에 공통 인터페이스가 제공됩니다.</p>
<p>또 ListCrudRepository 를 들어가보면 CrudRepository 를 상속 받는 것을 확인할 수 있는데 바로 여기에 <code>save()</code>, <code>findById()</code>, <code>count()</code> 와 같은 기본 CRUD 메서드들이 존재합니다.</p>
<p><code>delete()</code>, <code>findById()</code> 같은 메서드들은 내부에서 <code>EntityManager.remove()</code>, <code>EntityManger.find()</code> 와 같은 메서드들을 호출해서 삭제, 조회 기능을 제공해줍니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/2f1c352b-9712-457b-aa1a-ac6a3028814e/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA 기본편] 10. 객체 지향 쿼리( JPQL ) - 중급 문법]]></title>
            <link>https://velog.io/@hj_/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-10.-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%BF%BC%EB%A6%AC-JPQL-%EC%A4%91%EA%B8%89-%EB%AC%B8%EB%B2%95</link>
            <guid>https://velog.io/@hj_/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-10.-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%BF%BC%EB%A6%AC-JPQL-%EC%A4%91%EA%B8%89-%EB%AC%B8%EB%B2%95</guid>
            <pubDate>Mon, 26 Feb 2024 14:50:27 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한 님의 <a href="https://www.inflearn.com/course/ORM-JPA-Basic/dashboard">자바 ORM 표준 JPA 프로그래밍 - 기본편</a> 강의를 보고 작성한 내용입니다.</p>
</blockquote>
<br>


<h1 id="1-경로-표현식">1. 경로 표현식</h1>
<h2 id="1-1-상태필드-연관필드">1-1. 상태필드, 연관필드</h2>
<pre><code class="language-sql">select m.username   -- 상태 필드
from Member m
join m.team t       -- 단일 값 연관 필드
join m.orders o     -- 컬렉션 값 연관 필드</code></pre>
<p>경로 표현식이란 <code>.</code> 으로 객체 그래프를 탐색하는 것을 의미하고, 어떤 필드로 가느냐에 따라서 내부의 동작이 달라지게 됩니다.</p>
<p><strong>상태 필드</strong>란 단순히 값을 저장하기 위한 필드를 의미합니다. 상태 필드는 이후에 <code>.</code> 을 통해 탐색할 수 있는 것이 없기 때문에 경로 탐색의 끝이며, 이후 탐색이 발생하지 않습니다.</p>
<p><strong>연관 필드</strong>란 연관관계를 위한 필드를 의미하는데 <strong>단일 값 연관 필드</strong>, <strong>컬렉션 값 연관 필드</strong>로 나뉩니다.</p>
<br>


<h2 id="1-2-단일-값-연관-필드">1-2. 단일 값 연관 필드</h2>
<pre><code class="language-sql">select m.team.name
from Member m</code></pre>
<p>단일 값 연관 필드는 <code>@xToOne</code> 처럼 <strong>대상이 엔티티</strong>인 것을 의미합니다. 그래서 select 절에 <code>m.team.name</code> 과 같이 <code>m.team</code> 이후에 <code>.</code> 을 통해 <strong>탐색을 더 할 수 있습니다</strong>. 이러한 경우 <strong>묵시적 내부 조인</strong>이 발생합니다. ( 내부 조인만 가능 )</p>
<hr>


<pre><code class="language-java">String jpql = &quot;select m.team from Member m&quot;;
List&lt;Team&gt; resultList = em.createQuery(jpql, Team.class).getResultList();</code></pre>
<pre><code class="language-sql">select
    t1_0.id,
    t1_0.name 
from
    Member m1_0 
join 
    Team t1_0 
        on t1_0.id=m1_0.TEAM_ID</code></pre>
<p>JPQL 에서 m.team 을 통해 Team 을 가져오려 했는데, 실행되는 로그를 보면 <strong>Member 와 Team 을 join 을 하고, 프로젝션에 team 의 필드들을 나열</strong>합니다.</p>
<p>객체 입장에서는 <code>.</code> 을 통해 갈 수 있지만, DB 에서는 이처럼 사용하려면 조인이 필요하기 때문에 조인이 발생하고, 이러한 것을 <strong>묵시적 내부 조인</strong>이라고 합니다.</p>
<p>묵시적 조인이 되면 어디서 조인이 발생했는지 모르기 때문에 <code>join</code> 키워드를 직접 사용하는 <strong>명시적 조인을 사용하는 것이 좋습니다.</strong></p>
<br>


<h2 id="1-3-컬렉션-값-연관-필드">1-3. 컬렉션 값 연관 필드</h2>
<pre><code class="language-java">String jpql = &quot;select t.members from Team t&quot;;
List&lt;Collection&gt; result = em.createQuery(jpql, Collection.class).getResultList();</code></pre>
<p>컬렉션 값 연관 필드는 <code>@xToMany</code> 처럼 대상이 컬렉션인 것을 의미합니다. 단일 값 연관 필드처럼 <strong>묵시적 내부 조인이 발생하지만, 이후 탐색은 불가능</strong>합니다.</p>
<p><code>@OneToMany</code> 와 같은 것은 컬렉션이여서 데이터가 여러 개 존재합니다. 그렇게 때문에 어떤 데이터의 어떤 필드를 가져올지 선택할 수 없기 때문에 이후 탐색은 불가능합니다.</p>
<hr>


<pre><code class="language-java">String jpql = &quot;select m.username from Team t join t.members m&quot;;</code></pre>
<p>위처럼 명시적 조인을 통해 별칭을 얻으면 이후 탐색이 가능합니다.</p>
<p><br><br><br></p>
<h1 id="2-엔티티-패치-조인">2. 엔티티 패치 조인</h1>
<p>패치조인이란 SQL 의 조인이 아닌 JPQL 에서 <strong>성능 최적화</strong>를 위해 제공하는 기능으로 <strong>연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회</strong>하는 기능이며 <code>join fetch</code> 명령어를 사용합니다.</p>
<p>엔티티 패치 조인은 <code>@ManyToOne</code> 으로 이루어진 연관관계를 조인하는 것입니다.</p>
<h2 id="2-1-일반-조인">2-1. 일반 조인</h2>
<pre><code class="language-java">List&lt;Member&gt; members = em.createQuery(
                        &quot;select m from Member m join m.team&quot;, Member.class)
                        .getResultList();

for (Member findMember : members) {
    System.out.println(&quot;---------------findMember.getUsername() = &quot; 
                        + findMember.getUsername());
    System.out.println(&quot;---------------findMember.getTeam().getName() = &quot; 
                        + findMember.getTeam().getName());
}</code></pre>
<pre><code class="language-sql">select
    m1_0.id,
    m1_0.age,
    m1_0.TEAM_ID,
    m1_0.username 
from Member m1_0 
join Team t1_0 
    on t1_0.id=m1_0.TEAM_ID
---------------findMember.getUsername() = memberA
select
    t1_0.id,
    t1_0.name 
from Team t1_0 
where
    t1_0.id=?
---------------findMember.getTeam().getName() = teamA</code></pre>
<p><code>join</code> 키워드만 사용했을 때는 Team 과 조인하지만 <strong>Member 에 관한 컬럼들만</strong> 가져옵니다. 그래서 Team 의 이름을 출력할 때 <strong>지연로딩에 의해 다시 Team 에 대한 쿼리가 실행</strong>됩니다.</p>
<p>만약 Memebr 의 Team 이 전부 다른 데이터라면 Team 을 조회하는 쿼리가 계속 실행되고, <code>N + 1 문제</code>가 발생합니다.</p>
<p>쉽게 말해서 <strong>일반 조인 실행 시, 지연 로딩에 의해 연관된 엔티티를 함께 조회하지 않습니다.</strong></p>
<br>



<h2 id="2-2-패치-조인">2-2. 패치 조인</h2>
<pre><code class="language-java">List&lt;Member&gt; members = em.createQuery(
                        &quot;select m from Member m join fetch m.team&quot;, Member.class)
                        .getResultList();
for (Member findMember : members) {
    System.out.println(&quot;---------------findMember.getUsername() = &quot; 
                        + findMember.getUsername());
    System.out.println(&quot;---------------findMember.getTeam().getName() = &quot; 
                        + findMember.getTeam().getName());
}                        </code></pre>
<pre><code class="language-sql">select
    m1_0.id,
    m1_0.age,
    t1_0.id,
    t1_0.name,
    m1_0.username 
from Member m1_0 
join Team t1_0 
    on t1_0.id=m1_0.TEAM_ID
---------------findMember.getUsername() = memberA
---------------findMember.getTeam().getName() = teamA</code></pre>
<p>패치조인은 <strong>Member 와 Team 의 컬럼들을 모두 가져오게 됩니다</strong>. 그래서 Team 의 이름을 출력할 때 다시 쿼리가 나가지 않고 바로 출력됩니다. 즉, <strong>패치조인으로 Member 와 Team 을 함께 조회해서 지연 로딩이 이루어지지 않았습니다.</strong></p>
<p>해당 로그는 즉시로딩으로 가져오는 것과 비슷하게 출력되는데 즉시로딩은 <strong>연관된 모든 것을 여러 번의 쿼리로</strong> 가져옵니다. </p>
<p>하지만 <strong>패치조인은 원하는 것만을 한 번의 쿼리로 가져오도록 설정할 수 있습니다. 그래서 일반조인과 달리 <code>N + 1 문제</code>가 발생하지 않습니다.</strong></p>
<p>지연로딩보다 패치조인이 우선이기 때문에 패치조인이 이루어지고, 이 시점에 지연로딩과는 다르게 Team 은 프록시가 아닌 실제 엔티티입니다.</p>
<br>


<h2 id="2-3-참고-즉시로딩">2-3. 참고&gt; 즉시로딩</h2>
<pre><code class="language-sql">-- MEMBER
select
    m1_0.id,
    m1_0.age,
    m1_0.TEAM_ID,
    m1_0.username 
from Member m1_0 
join Team t1_0 
    on t1_0.id=m1_0.TEAM_ID
-- TEAM    
select
    t1_0.id,
    t1_0.name 
from Team t1_0 
where t1_0.id=?
---------------findMember.getUsername() = memberA
---------------findMember.getTeam().getName() = teamA</code></pre>
<p>연관된 엔티티를 모두 가져온 다음에 실행됩니다. 이때 조인은 <code>join</code> 키워드만 사용합니다.</p>
<p><br><br><br></p>
<h1 id="3-컬렉션-패치-조인">3. 컬렉션 패치 조인</h1>
<p>컬렉션 패치조인은 <code>@OneToMany</code> 의 관계를 패치조인 하는 것을 말합니다.
현재 데이터는 member1 과 member2 가 teamA 에 속해있는 상태입니다.</p>
<pre><code class="language-java">List&lt;Team&gt; teams = em.createQuery(
                    &quot;select t from Team t join fetch t.members tm&quot;, Team.class)
                    .getResultList();

for (Team team : teams) {
    System.out.println(&quot;-----------------------team.getName() = &quot; + 
                        team.getName());
    System.out.println(&quot;-----------------------team.getMembers().size() = &quot; + 
                        team.getMembers().size());
}                    </code></pre>
<pre><code class="language-sql">select
    t1_0.id,
    m1_0.Team_id,
    m1_1.id,
    m1_1.age,
    m1_1.TEAM_ID,
    m1_1.username,
    t1_0.name 
from Team t1_0 
join Team_Member m1_0 
    on t1_0.id=m1_0.Team_id 
join Member m1_1 
    on m1_1.id=m1_0.members_id 
where t1_0.name=&#39;teamA&#39;
-----------------------team.getName() = teamA
-----------------------team.getMembers().size() = 2</code></pre>
<p><strong>일대다 조인의 경우, 데이터가 늘어나서 teamA 가 두 번 출력되어야 하는데 한 번만 출력</strong>됩니다. 이것은 Hibernate 6 부터 distinct 를 자동으로 해주기 때문입니다.</p>
<p>JPQL 의 distinct 는 SQL 에 distinct 를 추가함과 동시에 <strong>애플리케이션에서 같은 식별자를 가진 엔티티의 중복을 제거</strong>합니다.</p>
<p>SQL 의 distinct 는 완전히 데이터가 동일해야 제거되는데 Member 의 PK 가 다르기 때문에 SQL 의 distinct 로는 제거가 안되고, 애플리케이션의 중복 제거를 통해 제거됩니다.</p>
<p><br><br><br></p>
<h1 id="4-패치조인-정리">4. 패치조인 정리</h1>
<h2 id="4-1-일반조인-vs-패치조인">4-1. 일반조인 vs 패치조인</h2>
<p>JPQL 은 결과를 반환할 때 연관관계 고려하지 않고 SELECT 절에 지정한 엔티티만 조회합니다. 그래서 <strong>일반조인은 실행 시 연관된 엔티티를 함께 조회하지 않게 됩니다.</strong></p>
<p><strong>패치조인을 사용할 때만 연관된 엔티티도 함께 조회</strong>하게 됩니다. 즉, 패치조인은 객체 그래프를 SQL 한 번에 조회하는 개념입니다.</p>
<br>


<h2 id="4-2-패치조인-한계">4-2. 패치조인 한계</h2>
<h4 id="1-패치조인-대상에는-별칭을-줄-수-없습니다--하이버네이트는-가능-권장-x-">1. 패치조인 대상에는 별칭을 줄 수 없습니다. ( 하이버네이트는 가능, 권장 X )</h4>
<p>왜냐하면 패치조인은 연관된 모든 것들을 가지고 오는 것입니다. 중간에 가져오고 싶지 않은 것들을 위해 사용한다면 차라리 따로 조회를 하는 것이 더 좋습니다. from 절에 있는 엔티티에 대해 조건을 거는 것은 가능합니다.</p>
<h4 id="2-둘-이상의-컬렉션은-패치조인할-수-없습니다">2. 둘 이상의 컬렉션은 패치조인할 수 없습니다.</h4>
<p>만약 Team 에 List 로 members 와 orders 가 있다고 했을 때 둘 중 하나만 패치조인을 할 수 있습니다.</p>
<h4 id="3-컬렉션을-패치조인하면-페이징을-사용할-수-없습니다">3. 컬렉션을 패치조인하면 페이징을 사용할 수 없습니다.</h4>
<p><code>@xToOne</code> 과 같은 연관관계는 패치조인해도 페이징이 가능합니다. 다대일과 같은 관계는 조인을 해도 데이터가 늘어나지 않기 때문입니다.</p>
<p>하지만 <code>@xToMany</code> 와 같은 관계의 컬렉션 패치조인은 페이징이 불가능합니다. 이를 해결할 수 있는 방법이 있는데 <a href="https://velog.io/@hj_/JPA-%ED%99%9C%EC%9A%A9-2%ED%8E%B8-3.-%EC%BB%AC%EB%A0%89%EC%85%98-%EC%A1%B0%ED%9A%8C-%EC%B5%9C%EC%A0%81%ED%99%94#3-2-2-%ED%8E%98%EC%9D%B4%EC%A7%95-%EB%B6%88%EA%B0%80%EB%8A%A5">API 2편 게시글</a>에 설명되어 있습니다.</p>
<br>


<h2 id="4-3-패치조인-특징">4-3. 패치조인 특징</h2>
<ol>
<li><p>연관된 엔티티들을 SQL 한 번으로 조회하기 때문에 성능 최적화에 좋습니다.</p>
</li>
<li><p>엔티티에 직접 적용하는 지연 로딩과 같은 글로벌 로딩 전략보다 우선 시 됩니다.</p>
</li>
<li><p>모든 것을 패치조인으로 해결할 수는 없고 패치조인은 객체 그래프를 유지해서 <code>.</code>을 통해 찾아가는 것이 필요할 때 사용하면 효과적입니다.</p>
</li>
<li><p>여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 패치조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO 로 반환하는 것이 효과적입니다.</p>
</li>
</ol>
<p><br><br><br></p>
<h1 id="5-namedquery">5. NamedQuery</h1>
<p>Named Query 는 미리 정의해서 이름을 부여해두고 사용하는 JPQL 로, 정적 쿼리만 가능합니다.</p>
<p>어노테이션이나 XML 에 정의해서 사용할 수 있는데 <strong>애플리케이션 로딩 시점에 JPA 나 Hibernate 가 쿼리를 SQL 로 파싱하고 캐싱</strong>합니다.</p>
<p>JPQL 은 SQL 로 파싱돼서 실행되어야 하는데 미리 파싱하고 캐싱을 하기 때문에 실행할 때마다 파싱하는 코스트를 줄일 수 있습니다.</p>
<p>또 JPQL 을 문자로 작성하면 쿼리가 실행되는 런타임에 오류가 발생할 수 있는데 Named Query 를 사용하면 <strong>애플리케이션 로딩 시점에 쿼리를 검증</strong>할 수 있습니다.</p>
<hr>


<h4 id="-사용-예시-">[ 사용 예시 ]</h4>
<pre><code class="language-java">@Entity
@NamedQuery(name = &quot;Member.findByUsername&quot;,
            query=&quot;select m from Member m where m.username = :username&quot;)
public class Member {
    ...
}
// ----------------------------------------------------------------------
List&lt;Member&gt; result = 
em.createNamedQuery(&quot;Member.findByUsername&quot;, Member.class)
    .setParameter(&quot;username&quot;, &quot;회원1&quot;)
    .getResultList();</code></pre>
<p><code>@NamedQuery</code> 어노테이션을 통해 쿼리를 생성하면서 이름을 부여하고, 사용할 때도 이름으로 쿼리를 사용할 수 있습니다. 이름을 작성할 때 관례로 <code>엔티티명.쿼리명</code> 으로 많이 사용합니다.</p>
<p><br><br><br></p>
<h1 id="6-벌크-연산">6. 벌크 연산</h1>
<p>영속성 컨텍스트에 의해 엔티티가 변경되면 자동으로 update 쿼리가 나간다고 했습니다. 하지만 이런 변경 감지 기능으로는 대량의 데이터를 수정하기도 어렵고 많은 SQL 이 실행되어야 합니다. 이때 사용할 수 있는게 <strong>벌크 연산</strong>입니다.</p>
<p><code>executeUpdate()</code> 로 UPDATE, DELETE 를 수행할 수 있으며, 쿼리 한 번으로 여러 엔티티를 변경할 수 있습니다. 실행 결과로 <strong>영향 받은 엔티티 수</strong>가 반환됩니다.</p>
<p>참고로 하이버네이트는 <code>INSERT INTO ... SELECT</code> 문도 지원합니다.</p>
<hr>


<h4 id="-사용-예시--1">[ 사용 예시 ]</h4>
<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>
<hr>



<h4 id="-주의점-">[ 주의점 ]</h4>
<p>commit 을 하거나, query 가 나가거나, 강제로 flush 를 호출하면 <strong>FLUSH</strong> 되는데 <strong>벌크 연산은 영속성 컨텍스트를 무시하고 DB 에 직접 쿼리</strong>가 날라가게 됩니다. </p>
<p>셋 중에 벌크 연산은 Query 를 날리는거에 해당하므로 <strong>FLUSH 후에 벌크 연산이 실행</strong>되기 때문에 영속성 컨텍스트에 있는 엔티티들은 걱정하지 않아도 됩니다.</p>
<pre><code class="language-java">Member member = new Member();
member.setAge(10);
em.persist(member);

String qlString = &quot;update Product p &quot; + 
                  &quot;set p.price = p.price * 1.1 &quot; +  
                  &quot;where p.stockAmount &lt; :stockAmount&quot;;  
// FLUSH -&gt; DB 에 반영                  
int resultCount = em.createQuery(qlString) 
                    .setParameter(&quot;stockAmount&quot;, 10)  
                    .executeUpdate();  

Member findMember = em.find(Member.class, member.getId());
System.out.println(&quot;member age = &quot; + findMember.getAge());   // 10</code></pre>
<p>하지만 위의 경우 문제가 생기는데 <code>executeUpdate()</code> 로 인해 DB 에서 Member 의 나이는 20살이지만, 위의 출력 결과는 10 이 됩니다. </p>
<p>왜냐하면 <strong>FLUSH 를 한다고 영속성 컨텍스트가 지워지는 것이 아닌 <code>em.clear()</code> 를 해야 지워지기 때문</strong>입니다. 그래서 영속성 컨텍스트에 존재하는 10 이 출력되는 것입니다.</p>
<p>이로 인한 문제를 방지하기 위해 벌크 연산을 먼저 수행하거나, <strong>벌크 연산 수행 후에 <code>em.clear()</code> 를 통해 영속성 컨텍스트를 초기화</strong>해야 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA 기본편] 9. 객체 지향 쿼리( JPQL ) - 기본 문법]]></title>
            <link>https://velog.io/@hj_/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-9.-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%BF%BC%EB%A6%AC-JPQL-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95</link>
            <guid>https://velog.io/@hj_/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-9.-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%EC%BF%BC%EB%A6%AC-JPQL-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95</guid>
            <pubDate>Mon, 26 Feb 2024 07:09:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한 님의 <a href="https://www.inflearn.com/course/ORM-JPA-Basic/dashboard">자바 ORM 표준 JPA 프로그래밍 - 기본편</a> 강의를 보고 작성한 내용입니다.</p>
</blockquote>
<br>


<h1 id="1-jpa-가-지원하는-쿼리">1. JPA 가 지원하는 쿼리</h1>
<h2 id="1-1-jpql">1-1. JPQL</h2>
<p>JPA 를 사용하면 엔티티 객체를 중심으로 개발을 하기 때문에, 검색할 때 테이블이 아닌 엔티티 객체를 대상으로 검색해야 합니다.</p>
<p>그래서 JPA 는 <strong>SQL 을 추상화한 JPQL 이라는 객체 지향 쿼리 언어</strong>를 지원하는데 SQL을 추상화했기 때문에 특정 데이터베이스 SQL 에 의존하지 않습니다.</p>
<p>또 JPQL 을 작성하면 SQL 로 번역되어 실행되는데  ANSI 표준 SQL 이 지원하는 모든 문법을 지원하며 <strong>엔티티 객체를 대상으로 쿼리</strong>합니다. ( SQL 은 테이블 대상 )</p>
<br>


<h2 id="1-2-jpa-criteria">1-2. JPA Criteria</h2>
<p>JPQL 은 문자로 작성해야 하기 때문에 동적 쿼리를 작성하기 어렵고, 쿼리가 실행되는 런타임에 오류가 발생할 수도 있습니다.</p>
<p>Criteria 는 쿼리를 코드로 작성할 수 있어 컴파일 시점에 오류를 파악할 수 있고, JPQL 보다 동적쿼리를 작성하기 편리합니다. 하지만 코드가 복잡하고 가독성이 좋지 않습니다.</p>
<br>


<h2 id="1-3-querydsl">1-3. QueryDSL</h2>
<p>Criteria 와 동일하게 <strong>JPQL 빌더</strong> 역할을 하며, 코드로 작성할 수 있어 컴파일 시점에 오류를 파악할 수 있습니다. 무엇보다 단순하고 쉬우며 동적 쿼리 작성도 편리합니다.</p>
<br>


<h2 id="1-4-네이티브-sql">1-4. 네이티브 SQL</h2>
<p>JPA 에서 SQL 을 직접 사용하는 기능인데 <code>createNativeQuery()</code> 를 사용하며,  JPQL 로 해결할 수 없는 특정 데이터베이스에 의존적인 기능이 필요할 때 사용합니다.</p>
<br>


<h2 id="1-5-jdbc-api-직접-사용">1-5. JDBC API 직접 사용</h2>
<p>JPA 를 사용하면서 JDBC API 를 직접 사용하거나, Spring JdbcTemplate, MyBatis 를 함께 사용할 수 있습니다.</p>
<p>이때 SQL 을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시하는 것처럼 영속성 컨텍스트를 적절한 시점에 강제로 플러시 하는게 필요합니다.</p>
<p><br><br><br></p>
<h1 id="2-jpql-기본-문법">2. JPQL 기본 문법</h1>
<h2 id="2-1-jpql-형태">2-1. JPQL 형태</h2>
<pre><code class="language-sql">select m from Member m where m.age &gt; 20
select COUNT(m), SUM(m.age), MAX(m.age) from Member m</code></pre>
<p>JPQL 에서 select, from 과 같은 키워드는 대소문자를 구분하지 않지만 엔티티를 표현할 때는 대문자를 사용하고, 속성을 표현할 때는 소문자를 사용합니다.</p>
<p>select 절에 위와 같은 <strong>집계 함수</strong> 사용이 가능합니다.</p>
<p>from 절에는 <strong>엔티티 이름</strong>을 사용하며, <strong>별칭은 필수</strong>이나 <code>as</code> 는 생략할 수 있습니다. <strong>엔티티 이름이란 클래스명이 아닌 <code>@Entity</code> 에 지정된 이름</strong>이며, 기본은 클래스명과 동일합니다.</p>
<p><br><br></p>
<h2 id="2-2-typedquery-query">2-2. TypedQuery, Query</h2>
<p><code>createQuery()</code> 를 사용했을 때 반환되는 타입은 <strong>TypedQuery</strong> 와 <strong>Query</strong> 가 있습니다.</p>
<h4 id="-typedquery-">[ TypedQuery ]</h4>
<pre><code class="language-java">TypedQuery&lt;Member&gt; query = em.createQuery(&quot;select m from Member m&quot;, Member.class);
TypedQuery&lt;String&gt; str = em.createQuery(&quot;select m.name from Member m&quot;, String.class);</code></pre>
<p><code>createQuery()</code> 의 두 번째 파라미터로 <strong>응답 클래스에 대한 타입 정보</strong>를 넘겨줄 수 있는데 이때 반환되는 것이 TypedQuery 이며 반환 타입이 명확할 때 사용됩니다.</p>
<p>첫 번째 예시는 Member 엔티티를 반환하기 때문에 Member 를 가지고, 두 번째 <code>m.name</code> 은 String 이기 때문에 String 타입을 주었고 제네릭에 String 이 있는 것을 확인할 수 있습니다.</p>
<hr>

<h4 id="-query-">[ Query ]</h4>
<pre><code class="language-java">Query query = em.createQuery(&quot;select m from Member m&quot;);
Query query2 = em.createQuery(&quot;select m.name, m.age from Member m&quot;);</code></pre>
<p>반대로 Query 는 두 번째 파라미터로 엔티티 정보를 넘겨주지 않았을 때, 반환 타입이 명확하지 않을 때 사용됩니다.</p>
<p>두 번째 예시를 보면 <code>m.name</code> 과 <code>m.age</code> 를 사용했는데 name 은 String 이고, age 는 int 입니다. 두 개의 타입이 다르기 때문에 타입 정보를 넘겨줄 수 없고, 이때 <strong>Query</strong> 가 반환됩니다.</p>
<p><br><br></p>
<h2 id="2-3-결과-조회-api">2-3. 결과 조회 API</h2>
<pre><code class="language-java">TypedQuery&lt;Member&gt; query = em.createQuery(&quot;select m from Member m&quot;,Member.class);
List&lt;Member&gt; members = query.getResultList();
Member resultMember = query.getSingleResult();</code></pre>
<p><code>getResultList()</code> 는 결과가 하나 이상일 때 사용하며, 리스트를 반환합니다. 결과가 없으면 빈 리스트가 반환됩니다.</p>
<p><code>getSingleResult()</code> 는 결과가 <strong>정확히 하나</strong>일 때 사용하며, 단일 객체를 반환합니다. 만약 <strong>결과가 없거나 둘 이상</strong>이면 예외가 발생합니다.</p>
<p><br><br></p>
<h2 id="2-4-파라미터-바인딩">2-4. 파라미터 바인딩</h2>
<pre><code class="language-java">em.createQuery(&quot;select m from Member m where m.name = :username&quot; ,Member.class)
                .setParameter(&quot;username&quot;, &quot;kim&quot;);

em.createQuery(&quot;select m from Member m where m.username = ?1&quot;, Member.class)
                .setParameter(1, &quot;kim&quot;);</code></pre>
<p>파라미터 바인딩하는 첫 번째 방법은 이름을 기준으로 하는 방법입니다. 파라미터 이름 앞에 <code>:</code> 를 붙여서 나타내며 <code>setParameter()</code> 를 통해 지정할 수 있습니다.</p>
<p>두 번째 방법은 위치를 기준으로 하는 방법인데 <code>?위치</code> 로 JPQL 을 작성하면 되는데 이 방법은 추천하지 않습니다.</p>
<p><br><br></p>
<h2 id="2-5-프로젝션">2-5. 프로젝션</h2>
<p>프로젝션이란 select 절에 조회할 대상을 지정하는 것인데 엔티티, 연관관계 엔티티, 임베디드 타입 숫자와 문자 같은 스칼라 타입을 지정할 수 있습니다.</p>
<h4 id="-엔티티-프로젝션-">[ 엔티티 프로젝션 ]</h4>
<pre><code class="language-java">em.createQuery(&quot;select m from Member m&quot;, Member.class);
em.createQuery(&quot;select m.team from Member m&quot;, Team.class);
em.createQuery(&quot;select t from Member m join m.team t&quot;, Team.class);</code></pre>
<p>엔티티 프로젝션의 경우, <strong>조회되는 데이터 모두 영속성 컨텍스트에서 관리</strong>됩니다.</p>
<p><code>m.team</code> 을 조회하는 경우, <code>createQuery()</code> 의 두 번째 파라미터로 <code>Team.class</code> 를 넘겨주어야 합니다. 그렇게 되면 Member 와 Team 이 조인되는 쿼리가 실행됩니다.</p>
<p>참고로 두 번째 예시와 세 번째 예시는 동일한 SQL 이 실행되는데, 두 번쨰의 경우 JOIN 예측이 안되기 때문에 세 번째처럼 사용하는 것이 좋습니다.</p>
<hr>


<h4 id="-임베디드-스칼라-">[ 임베디드, 스칼라 ]</h4>
<pre><code class="language-java">em.createQuery(&quot;select o.address from Order o&quot;, Address.class); // 임베디드
em.createQuery(&quot;select a from Address a&quot;, Address.class); // 불가능한 예시
em.createQuery(&quot;select m.name from Member m&quot;, Member.class);  // 스칼라</code></pre>
<p>Order 내부에 <strong>임베디드 타입을 조회하려면 응답 타입을 임베디드 타입으로 지정</strong>해야 합니다. 임베디드 타입은 <strong>하나의 테이블에 컬럼이 있는 형태이기 때문에 문제가 없습니다.</strong></p>
<p>임베디드 타입은 특정 테이블에 소속되어 있기 때문에 두 번째 예시처럼 사용할 수 없습니다.</p>
<p><br><br></p>
<h2 id="2-6-여러-타입-조회">2-6. 여러 타입 조회</h2>
<p>여러 개의 타입을 조회해야 하는 경우가 있는데 이때 3가지 방법이 존재합니다.</p>
<h4 id="-query--1">[ Query ]</h4>
<pre><code class="language-java">List resultList = em.createQuery(&quot;select m.username, m.age from Member m&quot;)
                    .getResultList();
Object obj = resultList.get(0);
Object[] result = (Object[]) obj;
System.out.println(&quot;username = &quot; + result[0]);
System.out.println(&quot;age = &quot; + result[1]);</code></pre>
<p>첫 번째는 Query 를 사용하는 방법입니다. 위의 List 안에는 <code>Object</code> 가 들어 있습니다. 조회 결과로 반환되는 것은 두 개이기 때문에 이를 <code>Object[]</code> 로 캐스팅하고, 배열 내부에서 조회 결과를 가져올 수 있습니다.</p>
<hr>


<h4 id="-object-">[ Object[] ]</h4>
<pre><code class="language-java">List&lt;Object[]&gt; resultList = em.createQuery(
                    &quot;select m.username, m.age from Member m&quot;)
                    .getResultList();
Object[] result = resultList.get(0);
System.out.println(&quot;username = &quot; + objects[0]);
System.out.println(&quot;age = &quot; + objects[1]);</code></pre>
<p>두 번째는 <code>Object[]</code> 를 사용하는 방법입니다. 반환되는 List 의 제네릭을 <code>Object[]</code> 로 지정하면 캐스팅 작업 없이 바로 <code>Object[]</code> 를 받을 수 있습니다.</p>
<hr>


<h4 id="-new-">[ new ]</h4>
<pre><code class="language-java">List&lt;MemberDTO&gt; dto = em.createQuery(
                        &quot;SELECT new jpabook.jpql.MemberDTO(m.username, m.age) &quot; +
                        &quot;FROM Member m&quot;, MemberDTO.class)
                    .getResultList();</code></pre>
<p>엔티티가 아닌 다른 타입으로 조회를 하는 경우 반드시 <code>new</code> 키워드를 사용해서 생성해야 합니다. 생성할 때는 <strong>패키지명을 포함한 전체 클래스명을 입력해야 하며, 순서와 타입이 일치하는 생성자가 필수</strong>로 필요합니다.</p>
<p><br><br></p>
<h2 id="2-7-페이징">2-7. 페이징</h2>
<pre><code class="language-java">em.createQuery(&quot;select m from Member m order by m.age desc&quot;, Member.class)
   .setFirstResult(10)
   .setMaxResults(20)
   .getResultRest();</code></pre>
<p><code>setFirstResult()</code> 로 조회 시작 위치를 지정할 수 있으며, 0 부터 시작합니다.</p>
<p><code>setMaxResults()</code> 로 조회할 데이터의 수를 지정할 수 있습니다.</p>
<p>위의 함수를 작성하면 JPA 가 <strong>각 데이터베이스 방언에 맞게 쿼리를 작성해서 실행</strong>하게 됩니다.</p>
<p><br><br><br></p>
<h1 id="3-조인">3. 조인</h1>
<p>JPQL 은 엔티티를 중심으로 동작하기 때문에 객체 스타일로 조인 문법을 작성해야 합니다.</p>
<h2 id="3-1-내부조인-외부조인">3-1. 내부조인, 외부조인</h2>
<pre><code class="language-java">// 내부조인
String jpql = &quot;select m from Member m inner join m.team t&quot;;
em.createQuery(jpql, Member.class);
// 외부조인
String jpql = &quot;select m from Member m left outer join m.team t&quot;;
em.createQuery(jpql, Member.class);</code></pre>
<p>inner 와 outer 는 생략할 수 있습니다.</p>
<br>



<h2 id="3-2-세타조인">3-2. 세타조인</h2>
<p>세타조인은 <strong>전혀 연관관계가 없는 엔티티를 조인</strong>하는 것입니다. 아래 예시들은 Member 가 4명, Team 이 2개가 저장되어 있고, 둘의 연관관계는 없는 상태입니다.</p>
<h4 id="-조건-x-">[ 조건 X ]</h4>
<pre><code class="language-java">String jpql = &quot;select m, t from Member m, Team t&quot;;
List resultList = em.createQuery(jpql).getResultList();
System.out.println(&quot;resultList.size() = &quot; + resultList.size());   // 결과 : 8</code></pre>
<p>위의 예시에서는 <strong>조인 조건이 없으므로 카테시안 곱으로 인한 모든 데이터</strong>가 함께 나오게 되기 때문에 8이 출력됩니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/0bf2525c-269b-41a5-af37-96de774d287a/image.png" alt=""></p>
<hr>


<h4 id="-조건-o-">[ 조건 O ]</h4>
<pre><code class="language-java">String jpql = &quot;select m, t from Member m, Team t where m.username = t.name&quot;;
List resultList = em.createQuery(jpql).getResultList();
System.out.println(&quot;resultList.size() = &quot; + resultList.size());   // 결과 : 2</code></pre>
<p>만약 여기서 조건을 추가해서 실행하면 2가 출력됩니다. 왜냐하면 <strong>세타조인은 내부조인만 가능</strong>하기 때문입니다. 전체 8개 중에 Member 와 Team 이 동일한 것은 최대 Team 의 데이터 수만큼 가능하기 때문에 결과가 2가 나오게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/e7cd5046-ee2c-4771-866b-661d7d0dc703/image.png" alt=""></p>
<br>




<h2 id="3-3-on-절">3-3. ON 절</h2>
<h4 id="-조인-대상-필터링-">[ 조인 대상 필터링 ]</h4>
<pre><code class="language-java">String jpql = &quot;select m, t from Member m left join m.team t on t.name = &#39;A&#39;&quot;;</code></pre>
<p>JPA 는 조인할 때 <strong>조인 대상을 필터링</strong> 할 수 있는 ON 절을 지원합니다. 
위의 예시는 회원과 팀을 조인할 때 팀의 이름이 A 인 팀만 조인하는 JPQL 입니다.</p>
<hr>



<h4 id="-연관관계-없는-외부조인-">[ 연관관계 없는 외부조인 ]</h4>
<pre><code class="language-java">String jpql = &quot;select m, t from Member m left join m.team t on m.name = t.name&quot;;</code></pre>
<p>위의 세타조인에서는 내부조인만 가능했는데 <strong>연관관계 없는 엔티티 외부 조인</strong>도 가능합니다.</p>
<p><br><br><br></p>
<h1 id="4-jpa-지원-함수">4. JPA 지원 함수</h1>
<h2 id="4-1-서브쿼리">4-1. 서브쿼리</h2>
<ul>
<li><p>[NOT] EXISTS : 서브쿼리에 결과가 존재하면 참    </p>
</li>
<li><p>[NOT] IN : 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참     </p>
</li>
<li><p>{ ALL | ANY | SOME }</p>
<ul>
<li><p>ALL 은 조건을 모두 만족해야 참</p>
</li>
<li><p>ANY 와 SOME 은 조건을 하나라도 만족하면 참</p>
</li>
</ul>
</li>
</ul>
<p>원래는 select 절, where 절, having 절에서만 서브쿼리를 사용할 수 있었는데 하이버네이트 6 부터는 FROM 절 서브쿼리도 지원합니다.</p>
<br>



<h2 id="4-2-타입">4-2. 타입</h2>
<h4 id="-enum-">[ ENUM ]</h4>
<pre><code class="language-java">// JPQL 에 직접 사용
String jpql = &quot;select m from Member m where m.type = jpabook.MemberType.ADMIN&quot;;

// 파라미터 바인딩 사용
String jpql = &quot;select m from Member m where m.type = :userType&quot;;
em.createQuery(jpql, Member.class)
    .setParameter(&quot;userType&quot;, MemberType.ADMIN);</code></pre>
<p>jpql 에 ENUM 타입을 사용할 수 있는데 JPQL 자체에 작성할 때는 <strong>패키지명을 포함</strong>해서 적어주어야 합니다. 파라미터 바인딩을 사용해서 ENUM 타입을 사용할 수 있습니다.</p>
<hr>


<h4 id="-엔티티-타입-">[ 엔티티 타입 ]</h4>
<pre><code class="language-java">String jpql = &quot;select i from Item i where type(i) = Book&quot;;
em.createQuery(jpql, Item.class);</code></pre>
<p>이전 예시에서 Item 를 상속 받는 엔티티가 Book, Album, Movie 가 있었는데 그 중에 Book 만 가지고 오고 싶을 때 위처럼 <code>type()</code> 을 사용할 수 있습니다.</p>
<p>Item 에는 자식들을 구분하기 위한 <strong>DTYPE</strong> 컬럼이 추가되는데 위의 쿼리가 실행되면 where 절에 <code>item.DTYPE = &#39;BOOK&#39;</code> 이 추가됩니다.</p>
<br>



<h2 id="4-3-조건식-case">4-3. 조건식 Case</h2>
<h4 id="-기본-case-식-">[ 기본 Case 식 ]</h4>
<pre><code class="language-java">String jpql =
        &quot;select &quot; +
        &quot;case when m.age &lt; 8 then &#39;어린이&#39; &quot; +
        &quot;     when m.age &lt; 20 then &#39;학생&#39; &quot; +
        &quot;     else &#39;성인&#39; &quot; +
        &quot;end &quot; +
        &quot;from Member m&quot;;
List&lt;String&gt; resultList = em.createQuery(jpql, String.class).getResultList();</code></pre>
<hr>


<h4 id="-단순-case-식-">[ 단순 Case 식 ]</h4>
<pre><code class="language-java">String jpql =
        &quot;select &quot; +
        &quot;case t.name &quot; +
        &quot;     when &#39;teamA&#39; then &#39;인센티브110%&#39; &quot; +
        &quot;     when &#39;teamB&#39; then &#39;인센티브120%&#39; &quot; +
        &quot;     else &#39;인센티브105%&#39; &quot; +
        &quot;end &quot; +
        &quot;from Team t&quot;;
List&lt;String&gt; resultList = em.createQuery(jpql, String.class).getResultList();</code></pre>
<hr>


<h4 id="-coalesce-">[ COALESCE ]</h4>
<pre><code class="language-sql">select coalesce(m.username,&#39;이름 없는 회원&#39;) from Member m</code></pre>
<p>COALESCE 는 하나씩 조회해서 NULL 이 아니면 반환합니다. 위의 예시는 사용자 이름이 없으면 &quot;이름 없는 회원&quot; 을 반환합니다.</p>
<hr>



<h4 id="-nullif-">[ NULLIF ]</h4>
<pre><code class="language-sql">select NULLIF(m.username, &#39;관리자&#39;) from Member m</code></pre>
<p>NULLIF 는 두 값이 같으면 null 을 반환하고, 다르면 첫 번째 값을 반환합니다. 위의 예시는 사용자 이름이 &quot;관리자&quot;면 null 을 반환하고, 나머지는 본인의 이름을 반환합니다.</p>
<br>


<h2 id="4-4-기타-함수들">4-4. 기타 함수들</h2>
<p><code>IN</code>, <code>AND</code>, <code>OR</code>, <code>NOT</code>, <code>BETWEEN</code>, <code>LIKE</code>, <code>IS NULL</code>, <code>CONCAT</code>, <code>SUBSTRING</code>, <code>TRIM</code>, <code>LOWER</code>, <code>UPPER</code>, <code>LENGTH</code>, <code>LOCATE</code>, <code>ABS</code>, <code>SQRT</code>, <code>MOD</code>, <code>SIZE</code>, <code>INDEX</code> 와 같은 기본 표현과 함수들도 다 사용할 수 있습니다.</p>
<p>DB 내부에 존재하는 <strong>사용자 정의 함수</strong>를 사용할 때는 함수 등록이 필요한데 Hibernate 6 버전 부터는 방식이 변경되었다고 한다 ( <a href="https://www.inflearn.com/questions/1096265/hibernate-6-custom-%ED%95%A8%EC%88%98-%EB%93%B1%EB%A1%9D-%EB%B0%A9%EB%B2%95-%EA%B3%B5%EC%9C%A0">참고</a> )</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA 기본편] 8. 값 타입]]></title>
            <link>https://velog.io/@hj_/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-8.-%EA%B0%92-%ED%83%80%EC%9E%85-yx5w9em3</link>
            <guid>https://velog.io/@hj_/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-8.-%EA%B0%92-%ED%83%80%EC%9E%85-yx5w9em3</guid>
            <pubDate>Sat, 24 Feb 2024 03:07:56 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한 님의 <a href="https://www.inflearn.com/course/ORM-JPA-Basic/dashboard">자바 ORM 표준 JPA 프로그래밍 - 기본편</a> 강의를 보고 작성한 내용입니다.</p>
</blockquote>
<br>


<h1 id="1-jpa-의-데이터-타입-분류">1. JPA 의 데이터 타입 분류</h1>
<h4 id="-엔티티-타입-">[ 엔티티 타입 ]</h4>
<p><code>@Entity</code> 로 정의하는 객체이며, 데이터가 변해도 식별자로 지속해서 추척이 가능합니다. 예를 들어, 회원 엔티티의 이름을 변경해도 식별자로 인식이 가능합니다.</p>
<h4 id="-값-타입-">[ 값 타입 ]</h4>
<p>int, String 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체를 의미합니다. 식별자가 없고 값만 있기 때문에 변경했을 때 추척이 불가능합니다.</p>
<p>값 타입은 또 다시 <strong>기본값 타입, 임베디드 타입, 컬렉션 값 타입</strong>으로 분류할 수 있으며, <strong>모든 값 타입은 값 타입을 소유한 엔티티의 생명주기에 의존</strong>합니다.</p>
<h4 id="-기본값-타입-">[ 기본값 타입 ]</h4>
<p>String 이나 자바 기본 타입인 int 나 double, 래퍼 클래스인 Integer 나 Long 등이 기본값 타입에 해당합니다.</p>
<p>기본값 타입은 <strong>생명주기를 엔티티에 의존</strong>합니다. 예를 들어 회원을 삭제하면 내부의 이름과 나이 필드도 함께 삭제됩니다.</p>
<p>또 <strong>값타입은 공유를 하면 안되는데</strong> 회원 이름을 변경한다고 해서 다른 회원의 이름이 변경되면 안됩니다.</p>
<p>int, double 과 같은 기본 타입은 항상 값을 복사하기 때문에 a = b 이후에 a 를 변경해도 b 는 변경되지 않습니다.</p>
<p>Integer 나 String 과 같은 클래스는 레퍼런스를 사용하기 때문에 공유는 가능하지만 변경은 불가능합니다.</p>
<p><br><br><br></p>
<h1 id="2-임베디드-타입">2. 임베디드 타입</h1>
<p>새로운 값 타입을 직접 정의해서 사용할 수 있는데, JPA 에서는 이를 임베디드 타입이라고 합니다. 임베디드 타입은 int, String 과 같은 <strong>엔티티 내부의 값 타입</strong>입니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/633e6983-f740-4b18-8648-d43a298f382e/image.png" alt=""></p>
<p>위의 그림은 값 타입을 사용하기 전과 후를 나타낸 그림입니다. startData, endDate 를 Period 라는 임베디드 타입을 만들고, 이를 Member 내부에서 사용하는 방식입니다.</p>
<p>JPA 에서 <strong>값 타입을 정의하는 곳에 표시하는 <code>@Embeddable</code></strong> 과 <strong>값 타입을 사용하는 곳에 표시하는 <code>@Embedded</code></strong> 를 사용하는데 임베디드 타입 내부에 <strong>기본 생성자는 필수</strong>입니다.</p>
<p>임베디드 타입은 재사용이 가능하고 응집도가 높아 <code>Period.isWork()</code> 처럼 <strong>해당 값 타입만 사용하는 의미있는 메서드</strong>를 만들 수 있습니다.</p>
<p>임베디드 타입을 사용할 때 매핑만 해주면 임베디드 타입을 사용하지 않을 때와 테이블의 구조는 동일합니다.</p>
<hr>


<h4 id="-attributeoverrides-">[ @AttributeOverrides ]</h4>
<p>한 엔티티에서 같은 값 타입을 사용하면 컬럼명이 중복되는데 <code>@AttributeOverrids</code> 와 <code>@AttributeOverride</code> 를 사용해서 컬러명 속성을 재정의 할 수 있습니다.</p>
<pre><code class="language-java">public class Member {
    @Embedded
    private Address homeAddress;

    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name=&quot;city&quot;, column = @Column(name=&quot;WORK_CITY&quot;)),
        @AttributeOverride(name=&quot;street&quot;, column = @Column(name=&quot;WORK_STREET&quot;)),
        @AttributeOverride(name=&quot;zipcode&quot;, column = @Column(name=&quot;WORK_ZIPCODE&quot;))
    })
    private Address workAddress;
}</code></pre>
<hr>


<h4 id="-임베디드-타입과-연관관계-">[ 임베디드 타입과 연관관계 ]</h4>
<p>임베디드 타입은 값 타입을 포함하거나, 엔티티를 참조할 수 있습니다.</p>
<pre><code class="language-java">@Embeddable
public class Address {
    ...
    @Embedded
    private Zipcode zipcode;    // 임베디드 타입 포함
}

@Embeddable
public class Zipcode {
    private String area;
    ...
}
// ------------------------------------------
@Embeddable
public class PhoneNumber {
    ...
    @ManyToOne
    private PhoneEntity phoneEntity;    // 엔티티 참조
}

@Entity
public class PhoneEntity {
    @Id @GeneratedValue
    private Long id;
    ...
}</code></pre>
<hr>


<h4 id="-null-">[ null ]</h4>
<p>임베디드 타입에 <code>null</code> 을 넣게되면 임베디드 타입에 해당하는 모든 컬럼들은 <code>null</code> 로 저장되게 됩니다.</p>
<pre><code class="language-java">public class Member {
    ...
    @Embedded
    private Address homeAddress = null;
}</code></pre>
<p><br><br><br></p>
<h1 id="3-값-타입과-불변-객체">3. 값 타입과 불변 객체</h1>
<h2 id="3-1-값-타입">3-1. 값 타입</h2>
<p>임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 side effect 가 발생할 수 있어 위험합니다.</p>
<h4 id="-값-타입-공유-">[ 값 타입 공유 ]</h4>
<pre><code class="language-java">Address address = new Address(&quot;city&quot;, &quot;street&quot;, &quot;10000&quot;);

Member member1 = new Member();
member1.setName(&quot;member1&quot;);
member1.setAddress(address);
em.persist(member1);

Member member2 = new Member();
member2.setName(&quot;member2&quot;);
member2.setAddress(address);
em.persist(member2);

member1.getAddress().setCity(&quot;newCity&quot;);</code></pre>
<p>member1 과 member2 가 같은 address 를 공유하는 상황에서 member1 을 통해 address 를 변경하면 insert 이후에 <strong>member1 과 memebr2 모두에게 update 쿼리</strong>가 날라가게 됩니다.
그래서 결국 <strong>member1 의 주소를 변경했지만 member2 의 주소도 newCity 로 변경</strong>됩니다.</p>
<hr>


<h4 id="-값-타입-복사-">[ 값 타입 복사 ]</h4>
<pre><code class="language-java">Address address = new Address(&quot;city&quot;, &quot;street&quot;, &quot;10000&quot;);

Member member1 = new Member();
member1.setName(&quot;member1&quot;);
member1.setAddress(address);
em.persist(member1);

Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());

Member member2 = new Member();
member2.setName(&quot;member2&quot;);
member2.setAddress(copyAddress);
em.persist(member2);

member1.getAddress().setCity(&quot;newCity&quot;);</code></pre>
<p>값 타입 자체를 공유하는 것이 아닌 <strong>값을 복사</strong>해서 위처럼 사용하는 것이 올바른 방법입니다.</p>
<br>



<h2 id="3-2-불변객체">3-2. 불변객체</h2>
<p>자바 기본 타입은 값을 대입하면 값을 복사합니다. 하지만 임베디드 타입처럼 <strong>직접 정의한 값 타입은 자바 기본 타입이 아닌 객체 타입</strong>이고, <strong>객체 타입은 참조 값을 복사</strong>해서 넣게 됩니다. </p>
<p>객체 타입을 수정할 수 없게 만들면 함께 수정되는 부작용을 원천 차단시킬 수 있습니다. 그래서 <strong>값 타입은 불변 객체로 설계</strong>해야 합니다.</p>
<p>불변 객체란 생성 시점 이후 절대 값을 변경할 수 없는 객체입니다. <strong>생성자로만 값을 생성하고 setter 를 만들지 않으면 됩니다.</strong> ( Integer, String 이 대표적인 불변객체입니다. )
만약 값을 수정하고 싶다면 <code>new</code> 키워드를 통해 새로운 객체를 만들어 사용해야 합니다.</p>
<br>


<h2 id="3-3-값-타입-비교">3-3. 값 타입 비교</h2>
<p>예를 들어, int 형 a 와 b 가 동일한 값을 가졌다면 <code>==</code> 비교를 했을 때 동일하다고 판단됩니다. 하지만 Address 의 경우 동일한 값을 가져도 <code>==</code> 비교를 했을 때 false 가 나오게 됩니다.</p>
<p><code>==</code> 비교는 인스턴스의 <strong>참조 값을 비교</strong>하는 동일성 비교이기 때문에 <strong>인스턴스의 값을 비교</strong>하는 <code>equals()</code> 를 이용한 동등성 비교를 해야합니다.</p>
<p>즉, <strong>값 타입은 <code>a.equals(b)</code>를 사용해서 동등성 비교</strong>를 해야하며, <code>equals()</code> 는 기본이 <code>==</code> 비교이기 때문에 값 타입의 <code>equals()</code> 메소드를 모든 필드를 사용하도록 재정의하는 것이 필요합니다.</p>
<pre><code class="language-java">@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Address address = (Address) o;
    return Objects.equals(city, address.city) 
            &amp;&amp; Objects.equals(street, address.street) 
            &amp;&amp; Objects.equals(zipcode, address.zipcode);
}

@Override
public int hashCode() {
    return Objects.hash(city, street, zipcode);
}</code></pre>
<p><code>equals()</code> 와 <code>hashCode()</code> 눈 기본적으로 생성해주는 것을 사용하는 것이 좋습니다.</p>
<p><br><br><br></p>
<h1 id="4-값-타입-컬렉션">4. 값 타입 컬렉션</h1>
<h2 id="4-1-개념">4-1. 개념</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/216ed097-68cd-46ed-8f9c-7ffb45e7a37b/image.png" alt=""></p>
<p>값 타입 컬렉션이란 값 타입을 컬렉션에 담아서 사용하는 것을 말하는데 <strong>값 타입을 하나 이상 저장할 때 사용</strong>하며, <code>@ElementCollection</code>, <code>@CollectionTable</code> 을 사용합니다.</p>
<p>값 타입이 하나만 있을 때는 엔티티의 필드로 넣으면 구현할 수 있었는데, 컬렉션을 사용하면 일대다 개념이기 때문에 RDB 에서 테이블 안에 컬렉션을 담을 수 없습니다.</p>
<p>그래서 <strong>값 타입 컬렉션에 대해서 별도의 테이블을 사용</strong>해야 합니다. 값 타입 테이블에 id 같은 개념을 넣어서 PK 로 쓰면 엔티티가 되어 버리기 때문에 <strong>값들을 묶어서 PK 로 사용</strong>해야 합니다.</p>
<hr>


<pre><code class="language-java">public class Member {
    ...
    @Embedded
    private Address homeAddress;

    @ElementCollection
    @CollectionTable(name = &quot;ADDRESS&quot;, joinColumns = @JoinColumn(name = &quot;MEMBER_ID&quot;))
    private List&lt;Address&gt; addressHistory = new ArrayList&lt;&gt;();
}</code></pre>
<p><code>@CollectionTable</code> 은 <strong>생성될 테이블명을 지정</strong>합니다. 또 <strong>외래키를 지정</strong>할 수 있는데 위에서는 MEMBER_ID 를 외래키로 지정하였습니다.</p>
<br>


<h2 id="4-2-사용-예시">4-2. 사용 예시</h2>
<h4 id="-저장-">[ 저장 ]</h4>
<pre><code class="language-java">Member member = new Member();
member.setName(&quot;member1&quot;);
member.setHomeAddress(new Address(&quot;city&quot;, &quot;street&quot;, &quot;10000&quot;));

member.getAddressHistory().add(new Address(&quot;old1&quot;, &quot;street&quot;, &quot;10000&quot;));
member.getAddressHistory().add(new Address(&quot;old2&quot;, &quot;street&quot;, &quot;10000&quot;));

em.persist(member);</code></pre>
<p><img src="https://velog.velcdn.com/images/hj_/post/93cafdea-c181-4c96-91d8-c7eab39209d1/image.png" alt=""></p>
<p>4-1 에서 지정한 것처럼 ADDRESS 라는 이름으로 테이블이 하나 생성되었고, 값 타입 컬렉션에 담긴 정보들은 ADDRESS 테이블에 저장된 것을 확인할 수 있습니다.</p>
<p>또 member 만 <code>persist()</code> 했는데 값 타입 컬렉션은 자동으로 저장되었습니다. 왜냐하면 값 타입 컬렉션도 스스로 라이프 사이클을 가지지 않기 때문에 라이프 사이클이 member 에 소속되어 다른 테이블임에도 함께 저장된 것입니다.</p>
<hr>


<h4 id="-조회-">[ 조회 ]</h4>
<pre><code class="language-java">Member findMember = em.find(Member.class, member.getId());
System.out.println(&quot;----------------------------findMember 반환&quot;);
for (Address ad : findMember.getAddressHistory()) {
    System.out.println(&quot;history = &quot; + ad.getCity() + &quot;, &quot; 
                                    + ad.getStreet() + &quot;, &quot; 
                                    + ad.getZipcode());
}</code></pre>
<pre><code class="language-sql">select
    m1_0.MEMBER_ID,
    m1_0.city,
    m1_0.street,
    m1_0.zipcode,
    m1_0.name 
from
    Member m1_0 
where
    m1_0.MEMBER_ID=?
----------------------------findMember 반환
select
    ah1_0.MEMBER_ID,
    ah1_0.city,
    ah1_0.street,
    ah1_0.zipcode 
from
    ADDRESS ah1_0 
where
    ah1_0.MEMBER_ID=?</code></pre>
<p>member 에 소속된 homeAddress 에 해당하는 값 타입은 함께 조회되었지만, 값 타입 컬렉션은 조회되지 않았습니다. 이 말은 <strong>값 타입 컬렉션은 지연로딩</strong>이라는 뜻입니다.
그래서 findMember 이후에 값을 사용할 때 쿼리가 나가는 것을 확인할 수 있습니다.</p>
<hr>


<h4 id="-수정-">[ 수정 ]</h4>
<pre><code class="language-java">Member findMember = em.find(Member.class, member.getId());
List&lt;Address&gt; addressHistory = findMember.getAddressHistory();
addressHistory.remove(new Address(&quot;old1&quot;, &quot;street&quot;, &quot;10000&quot;));
addressHistory.add(new Address(&quot;newCity1&quot;, &quot;street&quot;, &quot;10000&quot;));</code></pre>
<p><code>remove()</code> 를 통해 제거할 때 <code>equals()</code> 를 사용하기 때문에 이전에 했던 것처럼 <code>equals()</code> 와 <code>hashCode()</code> 를 제대로 구현해야 합니다.</p>
<p>또 값 타입은 불변 객체여야 하기 때문에 수정할 때 set 을 사용할 수 없으므로 새로운 값 타입 객체를 생성해서 사용합니다.</p>
<p>컬렉션의 값만 변경해도 JPA 가 DB 에 쿼리를 날려줍니다. 그래서 <strong>값 타입 컬렉션은 영속성 전이와 고아 객체 제거 기능</strong>을 가진다고 볼 수 있습니다.</p>
<br>



<h2 id="4-3-값-타입-컬렉션-제약-사항">4-3. 값 타입 컬렉션 제약 사항</h2>
<blockquote>
<p>값 타입 컬렉션에 변경 사항이 발생하면 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장합니다.</p>
</blockquote>
<pre><code class="language-sql">Hibernate: 
    delete from
        ADDRESS 
    where
        MEMBER_ID=?
Hibernate: 
    insert into
        ADDRESS (MEMBER_ID, city, street, zipcode) 
    values (?, ?, ?, ?)
Hibernate: 
    insert into
        ADDRESS (MEMBER_ID, city, street, zipcode) 
    values (?, ?, ?, ?)</code></pre>
<p>위의 수정 코드의 SQL 로그인데 delete 를 보면 해당 member 에 해당하는 <strong>모든 값을 지우는 것</strong>을 확인할 수 있고, 하나의 address 만 추가했는데 <strong>두 개의 insert 문이 실행</strong>되었습니다.</p>
<p>기존 데이터 2개 중 하나만 지웠기 때문에 하나가 남아있게 되고, 신규 insert 와 함께 기존 insert 1번이 실행된 것입니다.</p>
<hr>


<blockquote>
<p>저장할 때도 주의해야 할 점이 있는데 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 합니다. null 도 안되고, 중복 저장도 안됩니다.</p>
</blockquote>
<hr>

<pre><code class="language-java">@Entity
public class AddressEntity {
    @Id @GeneratedValue
    private Long id;
    private Address address;
}
// -----------------------------
@Entity
public class Member {
    ...
    // 일대다 단방향 매핑
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = &quot;MEMBER_ID&quot;)
    private List&lt;AddressEntity&gt; addressHistory = new ArrayList&lt;&gt;();
}</code></pre>
<p>그래서 실무에서는 상황에 따라 <strong>값 타입 컬렉션 대신 일대다 관계를 고려해서 엔티티를 만들고, 여기서 값 타입을 사용하는 것이 좋습니다.</strong> 영속성 전이와 고아 객체 제거 기능을 사용해서 값 타입 컬렉션처럼 사용하면 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA 기본편] 7. 프록시와 영속성 전이]]></title>
            <link>https://velog.io/@hj_/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-7.-%ED%94%84%EB%A1%9D%EC%8B%9C%EC%99%80-%EC%98%81%EC%86%8D%EC%84%B1-%EC%A0%84%EC%9D%B4</link>
            <guid>https://velog.io/@hj_/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-7.-%ED%94%84%EB%A1%9D%EC%8B%9C%EC%99%80-%EC%98%81%EC%86%8D%EC%84%B1-%EC%A0%84%EC%9D%B4</guid>
            <pubDate>Fri, 23 Feb 2024 13:53:04 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한 님의 <a href="https://www.inflearn.com/course/ORM-JPA-Basic/dashboard">자바 ORM 표준 JPA 프로그래밍 - 기본편</a> 강의를 보고 작성한 내용입니다.</p>
</blockquote>
<br>



<h1 id="1-프록시">1. 프록시</h1>
<h2 id="1-1-필요한-이유">1-1. 필요한 이유</h2>
<p>현재 예제에서는 Member 와 Team 이 연관관계를 맺고 있습니다. 만약 JPA 가 Member 와 Team 을 한 번에 가져온다고 했을 때, 둘 다 비즈니스 로직에서 사용한다면 좋겠지만 실제로는 Member 만 사용할 수도 있습니다. </p>
<pre><code class="language-java">Member findMember = em.find(Member.class, member.getId());
System.out.println(&quot;findMember = &quot; + findMember.getName());</code></pre>
<pre><code class="language-sql">select
    m1_0.MEMBER_ID,
    m1_0.city,
    m1_0.name,
    ...
from
    Member m1_0 
left join Team t1_0 
    on t1_0.id=m1_0.team_id 
where
    m1_0.MEMBER_ID=?</code></pre>
<p>Member 의 이름을 조회하는 코드를 실행했을 때 나오는 쿼리입니다. Team 은 전혀 사용을 하지 않는 상황인데 조인을 통해 Team 까지 한 번에 가져오고 있습니다.</p>
<p>JPA 는 이러한 점을 <strong>지연로딩</strong>과 <strong>프록시</strong>라는 것으로 해결하는데 지연 로딩을 이해하기 위해서는 프록시부터 명확하게 이해해야 합니다.</p>
<br>


<h2 id="1-2-프록시">1-2. 프록시</h2>
<p>JPA 에는 <code>em.find()</code> 와 함께 <code>em.getReference()</code> 라는 참조를 가져오는 메서드가 존재합니다. </p>
<blockquote>
<p><code>em.find()</code> : 데이터베이스를 통해서 실제 엔티티 객체를 조회</p>
<p><code>em.getReference()</code> : 데이터베이스 조회를 미루는 가짜( 프록시 )엔티티 객체 조회</p>
</blockquote>
<p>가짜 엔티티를 조회한다는 말은 <strong>DB에 쿼리가 나가지 않는데 객체가 조회되는 것</strong>을 말합니다. </p>
<hr>


<pre><code class="language-java">Member findMember = em.getReference(Member.class, member.getId());
System.out.println(&quot;---------------------findMember 반환됨&quot;);
System.out.println(&quot;findMember = &quot; + findMember.getName());</code></pre>
<pre><code class="language-sql">---------------------findMember 반환됨
select
    m1_0.MEMBER_ID,
    m1_0.city,
    m1_0.name,
    ...
from
    Member m1_0 
left join Team t1_0 
    on t1_0.id=m1_0.team_id 
where
    m1_0.MEMBER_ID=?</code></pre>
<p>위의 코드를 실행시킨 SQL 로그를 보았을 때 findMember 를 반환하는 코드에서 SELECT 쿼리가 나가지 않고 그 이후에 쿼리가 실행된 것을 확인할 수 있습니다.</p>
<p>즉, <strong>findMember 를 반환했는데 DB 에 쿼리가 나가지 않았다는 의미이고 <code>getName()</code> 으로 이름을 가져올 때 DB 에 쿼리가 나간 것입니다.</strong></p>
<blockquote>
<p>findMember.getClass() = Member$HibernateProxy$OFQX4JAg</p>
</blockquote>
<p><code>findMember.getClass()</code> 를 출력해보면 위처럼 출력되는데 반환된 findMember 는 <strong>Hibernate 가 강제로 만든 가짜 클래스</strong>라는 의미입니다. 그리고 이것이 <strong>프록시</strong>라는 것입니다.</p>
<br>


<h2 id="1-3-프록시와-실제-엔티티">1-3. 프록시와 실제 엔티티</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/c48a7483-7f5a-4ec3-a57a-8112d92810b8/image.png" alt=""></p>
<p><code>getReference()</code> 로 가져온 프록시 객체를 그림으로 표현하면 위와 같은데 <strong>프록시 객체는 실제 클래스를 상속 받아서 만들어져</strong> 겉모양이 실제 클래스와 동일합니다. 사용자 입장에서 진짜 객체인지 프록시 객체인지 구분하고 사용하면 됩니다.</p>
<p>프록시 객체는 실제 객체의 참조(target)를 보관하는데 <strong>target 이 바로 진짜 레퍼런스를 가리키며 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출</strong>합니다.</p>
<br>



<h2 id="1-4-프록시-초기화">1-4. 프록시 초기화</h2>
<pre><code class="language-java">Member member = em.getReference(Member.class, &quot;id1&quot;); 
member.getName();</code></pre>
<p>위의 코드에서 member 는 <strong>실제 DB 에서 조회한 적이 없어 target 이 존재하지 않습니다</strong>. 
그 후 <code>getName()</code> 호출을 하면 <strong>프록시 객체는 아래와 같은 초기화 과정</strong>을 거치게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/3fed8ef8-a262-42e1-99e0-80941d0f3494/image.png" alt=""></p>
<ol>
<li><p>처음 <code>getRefercnce()</code> 로 가져온 Proxy 는 target 을 갖고 있지 않습니다.</p>
</li>
<li><p>target 이 비어있는데 <code>getName()</code> 을 호출하면 JPA 가 <strong>영속성 컨텍스트에 초기화를 요청</strong>합니다.</p>
</li>
<li><p>영속성 컨텍스트는 DB 에서 조회를 해서 실제 Entity 객체를 생성합니다.</p>
</li>
<li><p><strong>실제 Entity 가 생성되면 프록시 객체의 target 에 실제 Entity 를 연결</strong>해줍니다.</p>
</li>
<li><p>target( 실제 Entity )의 <code>getName()</code> 을 통해 Member 의 이름이 반환됩니다.</p>
</li>
<li><p>프록시가 초기화되었기( target 이 연결되었기 ) 때문에 이후에 또 <code>getName()</code> 을 호출해도 DB 에 쿼리가 날라가지 않습니다.</p>
</li>
</ol>
<br>



<h2 id="1-5-프록시의-특징">1-5. 프록시의 특징</h2>
<ol>
<li><p>프록시 객체는 <strong>처음 사용할 때 한 번만 초기화</strong>되고, 한 번 초기화되면 초기화된 객체를 계속 사용하게 됩니다.( 실제 Entity 가 연결되었기 때문 )</p>
</li>
<li><p>프록시 객체를 초기화할 때 프록시 객체가 실제 엔티티로 변하는 것은 아닙니다. <strong>초기화되면 프록시 객체를 통해 실제 엔티티에 접근이 가능하게 되는 것</strong>입니다.</p>
</li>
<li><p>프록시 객체는 원본 엔티티를 상속받는 것이기 때문에 타입 체크 시 주의해야 합니다 ( <code>==</code> 비교를 사용하면 실패합니다. <code>instance of</code> 를 사용해야 합니다. )</p>
</li>
<li><p>영속성 컨텍스트에 찾는 엔티티가 이미 존재하면 <code>getReference()</code> 를 호출해도 실제 엔티티가 반환됩니다. ( 그 반대도 마찬가지 )</p>
</li>
<li><p>영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 예외가 발생합니다. ( 하이버네이트의 경우 LazyInitializationException 예외가 발생 )</p>
</li>
</ol>
<br>

<h3 id="-4번-추가-설명-">[ 4번 추가 설명 ]</h3>
<h4 id="-find-vs-getreference-">[ find vs getReference ]</h4>
<pre><code class="language-java">Member m1 = em.find(Member.class, member1.getId());
System.out.println(&quot;m1 = &quot; + m1.getClass());

Memebr m2 = em.getReference(Member.class, member1.getId());
System.out.println(&quot;m2 = &quot; + m2.getClass());</code></pre>
<p>위의 코드를 실행하면 m1 도 Proxy 가 아닌 Member 이고, m2 도 Proxy 가 아닌 Member 입니다. 이 결과가 나오는 것은 두 가지 이유가 존재합니다.</p>
<p>첫 번째는 이미 Member 가 영속성 컨텍스트에 있는데 굳이 프록시로 가져와도 아무 이점이 없기 때문입니다. 원본을 반환하는게 성능 최적화 입장에서도 훨씬 좋기 때문입니다.</p>
<p>두 번째 이유는 <strong>JPA 는 같은 영속성 컨텍스트 안에서, 같은 트랜잭션 레벨 안에서 <code>==</code> 비교를 하면 항상 true 로 출력되어야 하기 때문</strong>입니다.</p>
<p>m1 이 실제 엔티티든 프록시든 상관없이 JPA 에서는 마치 Java 컬렉션에서 가져온 것을 <code>==</code> 비교하듯이, 위의 m1 과 m2 가 <strong>한 영속성 컨텍스트에서 가져온 것이고 PK 가 동일하다면 JPA 는 항상 true 를 반환</strong>해야 합니다.</p>
<p>쉽게 말해서 <strong><code>==</code> 비교를 할 때 true 로 만들어주기 위해서 영속성 컨텍스트에 있다면 프록시가 아닌 실제 엔티티를 반환</strong>하는 것입니다.</p>
<hr>

<h4 id="-getreference-vs-getreference-">[ getReference vs getReference ]</h4>
<pre><code class="language-java">Member m1 = em.getReference(Member.class, member1.getId());
System.out.println(&quot;m1 = &quot; + m1.getClass());

Memebr m2 = em.getReference(Member.class, member1.getId());
System.out.println(&quot;m2 = &quot; + m2.getClass());</code></pre>
<p>위의 코드를 실행해서 출력하면 둘이 <strong>동일한 프록시</strong>가 출력됩니다. 왜냐하면 4번에 의해 <code>==</code> 비교를 했을 때 true 를 출력해주어야 하기 때문입니다.</p>
<hr>

<h4 id="-getreference-vs-find-">[ getReference vs find ]</h4>
<pre><code class="language-java">Member refMember = em.getReference(Member.class, member1.getId());
System.out.println(&quot;refMember = &quot; + refMember.getClass());

Memebr findMember = em.find(Member.class, member1.getId());
System.out.println(&quot;findMember = &quot; + findMember.getClass());</code></pre>
<p>refMember 는 프록시 객체가 출력됩니다. 그 뒤에 findMember 는 <code>find()</code> 로 조회했기 때문에 실제 엔티티가 출력될 것이라고 생각할 수 있지만 findMember 역시 <strong>refMember 와 동일한 프록시 객체</strong>가 출력됩니다. </p>
<p>만약 refMember 가 프록시 객체이고 findMember 가 실제 엔티티라면 <code>==</code> 비교를 했을 때 false 가 출력될 것입니다. 하지만 4번에 의해 <strong>true 가 출력되어야 하고, 그래서 <code>find()</code> 의 결과가 프록시를 반환</strong>합니다.</p>
<br>

<h3 id="-5번-추가-설명-">[ 5번 추가 설명 ]</h3>
<pre><code class="language-java">Member refMember = em.getReference(Member.class, member1.getId());
System.out.println(&quot;refMember = &quot; + refMember.getClass());  // 프록시

em.close(); // or em.detach(refMember) or em.clear()

System.out.println(&quot;name = &quot; + refMember.getName());    // 프록시 객체 초기화 ( 강제 호출 )</code></pre>
<p>만약 위처럼 영속성 컨텍스트를 종료하거나 영속 상태 엔티티를 준영속 상태로 변경하고 프록시 객체를 초기화하면 어떻게 될까요?</p>
<p>위에서 언급한 것처럼 하이버네이트가 <strong>org.hibernate.LazyInitializationException</strong> 예외를 터트려버립니다. </p>
<p>왜냐하면 1-4번에서 <strong>프록시에 대한 초기화 요청은 영속성 컨텍스트를 통해 일어난다고 했는데 해당 객체를 영속성 컨텍스트에서 관리하고 있지 않기 때문</strong>입니다.</p>
<p>참고로 <code>getName()</code> 과 같은 메서드를 써서 프록시 객체를 초기화하는 것을 <strong>강제 초기화</strong>라고 하며, <code>Hibernate.initalize(entity)</code> 를 통해서도 강제로 초기화할 수 있습니다.</p>
<p><br><br><br></p>
<h1 id="2-지연로딩과-즉시로딩">2. 지연로딩과 즉시로딩</h1>
<h2 id="2-1-지연로딩">2-1. 지연로딩</h2>
<p>처음으로 돌아가서 Member 만 사용을 하는데 굳이 Team 까지 같이 가져오는 문제를 해결하는 것이 <strong>지연로딩</strong>이라고 했습니다. <strong>JPA 는 지연로딩 LAZY 를 사용해서 프록시로 조회</strong>합니다.</p>
<pre><code class="language-java">public class Member {
    ...
    @ManyToOne(fetch = FetchType.LAZY)
    private Team team;
}
//-------------------------------------
Member findMember = em.find(Member.class, member.getId());
System.out.println(&quot;---------------------------findMember 반환&quot;);
System.out.println(findMember.getTeam().getName());</code></pre>
<pre><code class="language-sql">Hibernate: 
    select
        m1_0.MEMBER_ID,
        m1_0.name,
    from
        Member m1_0 
    where
        m1_0.MEMBER_ID=?
---------------------------findMember 반환
Hibernate: 
    select
        t1_0.id,
        t1_0.name 
    from
        Team t1_0 
    where
        t1_0.id=?</code></pre>
<p>위처럼 <code>FetchType.LAZY</code> 를 사용하면 Member 클래스만 DB 에서 조회하고, Team 은 <strong>프록시 객체로 조회</strong>합니다. 쿼리 로그를 보면 Member 만 조회하고 Team 에 대한 것은 아무것도 없는 것을 확인할 수 있습니다.</p>
<p>그 이후에 <code>getTeam().getName()</code> 을 하면 프록시 초기화가 이루어져야 하기 때문에 DB 에서 Team 을 조회하게 됩니다. 즉, 실제 Team 을 <strong>사용하는 시점에 DB 에 쿼리가 나가 프록시가 초기화</strong>됩니다.</p>
<br>


<h2 id="2-2-즉시로딩">2-2. 즉시로딩</h2>
<p>만약 Member 와 Team 을 함께 사용하는 일이 많다면 <strong>즉시로딩</strong>을 사용합니다.</p>
<pre><code class="language-java">public class Member {
    ...
    @ManyToOne(fetch = FetchType.EAGER)
    private Team team;
}
//-------------------------------------
Member findMember = em.find(Member.class, member.getId());</code></pre>
<pre><code class="language-sql">Hibernate: 
    select
        m1_0.MEMBER_ID,
        m1_0.name,
    from
        Member m1_0 
    left join
        Team t1_0 
            on t1_0.id=m1_0.team_id 
    where
        m1_0.MEMBER_ID=?</code></pre>
<p>쿼리 로그를 보면 Member 를 DB 에서 조회할 때 Team 을 조인해서 바로 가져오는 것을 확인할 수 있습니다. 한 번에 다 가져오기 때문에 <strong>프록시를 사용하지 않습니다.</strong></p>
<p>하지만 <strong>실무에서는 즉시로딩이 아닌 지연로딩을 사용해야 합니다</strong>. 즉시로딩을 적용하면 예상하지 못한 SQL 이 발생하고, JPQL 에서 <code>N + 1 문제</code>를 일으키기 때문입니다.</p>
<p><code>@ManyToOne</code>, <code>@OneToOne</code>은 기본이 즉시 로딩이고, <code>@OneToMany</code>, <code>@ManyToMany</code>는 기본이 지연 로딩입니다.</p>
<br>



<h3 id="-n--1-문제-">[ N + 1 문제 ]</h3>
<pre><code class="language-java">List&lt;Member&gt; members = em.createQuery(&quot;select m from Member m&quot;, Member.class)
                        .getResultList();</code></pre>
<pre><code class="language-sql">Hibernate: 
    select
        m1_0.MEMBER_ID,
        m1_0.name,
    from
        Member m1_0
Hibernate: 
    select
        t1_0.id,
        t1_0.name 
    from
        Team t1_0 
    where
        t1_0.id=?</code></pre>
<p>즉시로딩을 설정하고 위의 JPQL 을 실행하면 쿼리가 2번 나가게 됩니다. <code>find()</code> 는 PK 를 찍어서 가져오는 것이기 때문에 JPA 가 최적화할 수 있습니다. </p>
<p>하지만 <strong>JPQL 은 그대로 SQL 로 번역</strong>이 이루어지기 때문에 아래처럼 실행되게 됩니다.</p>
<blockquote>
<ol>
<li><p>JPQL 이 Member 를 조회하는 것이기 때문에 <strong>Member 에 대한 select 쿼리</strong>가 실행</p>
</li>
<li><p><strong>Member 를 가지고 왔는데 Team 이 즉시로딩</strong>이 되어 있습니다. </p>
</li>
<li><p>즉시로딩은 데이터를가져올 때 무조건 값이 다 들어있어야 하기 때문에 또 <strong>Team 을 조회하기 위한 쿼리</strong>가 실행됩니다.</p>
</li>
</ol>
</blockquote>
<p>지금은 Member 데이터가 1개만 저장되어 있어서 1번만 실행되었지만 10개의 데이터가 있다면 Team 을 조회하기 위해 10번의 쿼리가 나가게 됩니다. </p>
<p>이것이 <code>N + 1 문제</code>인데 <strong>처음의 쿼리를 1 이라고 하고, 처음 쿼리로 인해 추가 쿼리가 N 개가 나간다고 해서 <code>N + 1</code></strong> 이라고 합니다.</p>
<p><strong>LAZY</strong> 로 설정하고 실행하면 쿼리는 <strong>Member 만 조회하고, Team 에는 프록시</strong>가 들어갑니다. 하지만 member 를 반복하면서 team 을 사용했을 때 쿼리가 반복해서 나가는 것은 동일합니다.</p>
<br>



<h3 id="-n--1-문제-해결-">[ N + 1 문제 해결 ]</h3>
<p>우선 모든 연관관계를 지연로딩으로 설정하고 fetch join 을 사용합니다.</p>
<p>패치조인은 런타임에 동적으로 원하는 객체들을 선택해서 한 번에 가져오는 전략입니다. 
<code>join fetch m.team</code> 을 하면 둘을 조인해서 한 번에 가져옵니다. ( 즉시 로딩처럼 )</p>
<p>이외에도 <code>@EntityGraph</code> 나 batch_size 를 사용하는 방법도 존재합니다.</p>
<p><br><br><br></p>
<h1 id="3-영속성-전이-cascade-">3. 영속성 전이( CASCADE )</h1>
<p>영속성 전이는 <strong>특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용</strong>합니다. </p>
<pre><code class="language-java">@Entity
public class Parent {
    @OneToMany(mappedBy = &quot;parent&quot;, cascade = CascadeType.PERSIST)
    private List&lt;Child&gt; childList = new ArrayList&lt;&gt;();

    public void addChile(Child child) {
        childList.add(child);
        child.setParent(this);
    }
}
//------------------------------------------------------------------
@Entity
public class Child {
    @ManyToOne
    @JoinColumn(name = &quot;PARENT_ID&quot;)
    private Parent parent;
}
//------------------------------------------------------------------
Parent parent = new Parent();
Child child1 = new Child();
Child child2 = new Child();

parent.addChile(child1);
parent.addChile(child2);

em.persist(parent);</code></pre>
<p>위처럼 Parent 와 Child 가 있다고 가정했을 때 <code>cascade = CascadeType.PERSIST</code> 이 없다면 parent 와 child 를 저장하려면 각각 <code>persist()</code> 를 호출해야 저장됩니다.</p>
<p>하지만 해당 속성을 사용함으로써 <strong>parent 만 저장하면 자동적으로 child 도 저장</strong>되게 됩니다.</p>
<p><strong>CASCADE 를 사용할 때 주의해야 할 점은 소유자가 하나일 때 사용해야 한다는 점</strong>입니다. 현재 예시에서는 child 의 소유자가 parent 가 있는데 만약 다른 엔티티에서 child 와 연관관계가 있다면 사용하면 안됩니다.</p>
<hr>

<h4 id="-cascade-종류-">[ CASCADE 종류 ]</h4>
<ul>
<li>ALL : 모두 적용</li>
<li>PERSIST : 영속</li>
<li>REMOVE : 삭제</li>
<li>MERGE : 병합</li>
<li>REFRESH : REFRESH</li>
<li>DETACH : DETACH</li>
</ul>
<p><br><br><br></p>
<h1 id="4-고아객체">4. 고아객체</h1>
<p><strong>부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제</strong>할 때 사용하는 <strong>orphanRemoval</strong> 옵션이 있는데 이를 true 로 설정하면 자동으로 제거됩니다.</p>
<pre><code class="language-java">public class Parent {
    ...
    @OneToMany(mappedBy = &quot;parent&quot;, cascade = CascadeType.PERSIST, orphanRemoval = true)
    private List&lt;Child&gt; childList = new ArrayList&lt;&gt;();
}
//-----------------------------------------------------------------------------------
Parent parent = em.find(Parent.class, id);
parent.getChildren().remove(0);</code></pre>
<p>위처럼 옵션을 추가한뒤, 자식 엔티티를 컬렉션에서 제거하면 둘의 <strong>연관관계가 끊기게 되는데 그렇게 되면 DELETE 쿼리가 수행</strong>됩니다. </p>
<p><strong>이 옵션도 CASCADE 와 마찬가지로 참조하는 곳이 하나일 때만 사용해야 하며, <code>@OneToOne</code>, <code>@OneToMany</code> 만 가능합니다.</strong></p>
<p>참고로 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거되는데 이는 <code>CascadeType.REMOVE</code> 처럼 동작합니다.</p>
<hr>


<p>스스로 생명주기를 관리하는 엔티티는 <code>em.persist()</code> 로 영속화, <code>em.remove()</code> 로 제거합니다.</p>
<p><code>CascadeType.ALL</code> 과 <code>orphanRemoval = true</code> 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA 기본편] 6. 상속관계 매핑]]></title>
            <link>https://velog.io/@hj_/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-6.-%EC%83%81%EC%86%8D%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@hj_/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-6.-%EC%83%81%EC%86%8D%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Fri, 23 Feb 2024 03:16:38 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한 님의 <a href="https://www.inflearn.com/course/ORM-JPA-Basic/dashboard">자바 ORM 표준 JPA 프로그래밍 - 기본편</a> 강의를 보고 작성한 내용입니다.</p>
</blockquote>
<br>


<h1 id="1-상속관계-매핑">1. 상속관계 매핑</h1>
<p>관계형 데이터베이스에 상속관계는 없습니다. 슈퍼타입 서브타입 관계라는 모델링 기법이 객체의 상속과 유사합니다. 그래서 <strong>상속관계를 매핑할 때는 객체의 상속 구조와 DB의 슈퍼타입, 서브타입 관계를 매핑</strong>합니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/7e162573-af90-4948-9040-b856fce97f28/image.png" alt=""></p>
<p>위와 같은 구조가 있다고 가정했을 떄, 슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 3가지 방법이 존재합니다. 객체 입장에서는 상속관계를 지원하기 때문에 하나의 구조가 나오게 되므로 어떤 전략을 사용해도 JPA 에서 매핑이 가능합니다.</p>
<br>


<h2 id="1-1-조인전략">1-1. 조인전략</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/68f34830-a122-4a6b-98ae-484d98062703/image.png" alt=""></p>
<p>조인전략은 각각 테이블로 변환하는 방식인데 <strong>모든 테이블을 생성해 데이터를 나누고 조인으로 구성</strong>하는 것입니다. 필요 시 데이터를 같이 가져올 때 조인으로 가져오면 됩니다. </p>
<p>예를 들어 Album 이 저장될 때 Item 테이블과 Album 테이블에 데이터를 나누어 저장하는 방식입니다. 데이터를 가져올 때는 ITEM_ID 를 통해 가져옵니다.</p>
<p>Item 테이블만 보았을 때 Album 인지, Movie 인지, Book 인지 구분이 안되기 때문에 Item 테이블 안에 <strong>이들을 구별하는 컬럼을 생성</strong>합니다.</p>
<hr>


<h4 id="-구현-코드-">[ 구현 코드 ]</h4>
<pre><code class="language-java">@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public class Item {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private int price;
}
// -----------------------
@Entity
public class Movie extends Item {
    @Id @GeneratedValue
    private Long id;
    private String actor;
    private String director;
}</code></pre>
<p><code>@Inheritance(strategy = InheritanceType.JOINED)</code> 라고 선언하면 위의 그림처럼 설계한 것과 동일하게 테이블이 생성됩니다.</p>
<p><code>@DiscriminatorColumn</code> 은 상위 클래스에 지정하고, name 속성을 지정하지 않으면 DTYPE 이라는 이름의 컬럼이 생성됩니다. </p>
<p><code>@DiscriminatorValue</code> 는 하위 클래스에 지정하고 저장하고 싶은 데이터를 지정할 수 있는데, 어노테이션 자체를 선언하지 않거나 지정하지 않으면 엔티티명이 들어갑니다.</p>
<hr>


<h4 id="-실행-코드-">[ 실행 코드 ]</h4>
<pre><code class="language-java">Movie movie = new Movie();
movie.setActor(&quot;actorA&quot;);
movie.setDirector(&quot;directorA&quot;);
movie.setName(&quot;nameA&quot;);
movie.setPrice(10000);

em.persist(movie);
// -----------------------
Movie findMovie = em.find(Movie.class, movie.getId());</code></pre>
<p>위위 코드를 실행하면 Item 테이블과 Movie 테이블에 각각 Insert 쿼리가 나가게 되고
Item 테이블에 name, price 가, Movie 테이블에 actor, director,ITEM_ID 가 저장됩니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/4490e15f-76a2-4670-af19-ea88eda9d75f/image.png" alt=""></p>
<p>저장된 것을 보면 ITEM 의 ID 와 MOVIE 의 ID 가 동일한 것을 확인할 수 있으며, ITEM 에 DTYPE 이라는 컬럼이 생성되고, Movie 라는 값이 들어있는 것을 확인할 수 있습니다.
만약 Movie 에 <code>@DiscriminatorValue(&quot;M&quot;)</code> 을 선언하면 Movie 가 아닌 M 이라는 값으로 들어가게 됩니다.</p>
<pre><code class="language-sql">-- 조회 SQL 로그
Hibernate: 
    select
        m1_0.id,
        m1_1.name,
        m1_1.price,
        m1_0.actor,
        m1_0.director 
    from
        Movie m1_0 
    join
        Item m1_1 
            on m1_0.id=m1_1.id 
    where
        m1_0.id=?</code></pre>
<p><code>em.find()</code> 를 할 때는 Movie 와 Item 을 inner 조인해서 데이터를 가져오게 됩니다.</p>
<br>



<h2 id="1-2-단일-테이블-전략">1-2. 단일 테이블 전략</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/3e151c89-f138-4779-9140-f0000cefb983/image.png" alt=""></p>
<p>단일 테이블 전략이란 논리 모델을 하나의 테이블로 다 합치는 방법입니다. 조인전략과 마찬가지로 Album 인지, Movie 인지, Book 인지 구분하기 위해 <strong>이들을 구별하는 컬럼을 생성</strong>합니다.</p>
<p>조인전략에서 어노테이션을 <code>@Inheritance(strategy = InheritanceType.SINGLE_TABLE)</code> 로 변경하면 됩니다. 아래 테이블 생성 쿼리를 보면 Movie 에 있는 컬럼들도 Item 에 생성되는 것을 확인할 수 있습니다.</p>
<p>조인전략에서는 <code>@DiscriminatorColumn</code> 가 필수로 필요했지만, 단일 테이블 전략에서는 생략해도 자동으로 생성됩니다.</p>
<pre><code class="language-sql">Hibernate: 
create table Item (
    price integer not null,
    id bigint not null,
    DTYPE varchar(31) not null,
    actor varchar(255),
    director varchar(255),
    name varchar(255),
    primary key (id)
)</code></pre>
<p>단일 테이블 전략은 Insert 도 한 번에 되고, select 도 조인 필요없이 한 테이블에서만 조회하면 되기 때문에 성능상 훨씬 좋습니다.</p>
<br>



<h2 id="1-3-구현-클래스별-테이블-전략">1-3. 구현 클래스별 테이블 전략</h2>
<blockquote>
<p>쓰면 안되는 전략이라고 하셨습니다</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hj_/post/32232d14-6d2c-49f6-8f3e-218d0d649b60/image.png" alt=""></p>
<p>테이블을 생성할 때 각각의 테이블을 생성하고, 모든 테이블이 Item 에 해당하는 name, price 와 같은 컬럼들을 가지게 하는 전략입니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/e2e54639-4ee9-4b91-b020-4f7bf81a0a92/image.png" alt=""></p>
<p><code>@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)</code> 로 선언하면 됩니다. 
이때 Item 은 추상 클래스로 선언해야 하고, 코드 실행 결과, movie 테이블에 name 과 price 컬럼이 들어있는 것을 확인할 수 있습니다.</p>
<p>조회할 때 문제가 발생하는데 <code>em.find(Item.class, movie.getId())</code> 를 실행하면 모든 자식들을 UNION 을 걸어서 전부 확인하게 됩니다.</p>
<p><br><br><br></p>
<h1 id="2-매핑정보-상속">2. 매핑정보 상속</h1>
<blockquote>
<p><code>@MappedSuperclass</code> 라는 어노테이션이 있는데 공통 매핑 정보가 필요할 때 사용합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hj_/post/f79b029a-df97-4d27-b5db-164b63ffbafd/image.png" alt=""></p>
<p>예를 들어, 객체 입장에서 Member 와 Seller 에 id 와 name 이라는 필드가 공통적으로 있을 때 이를 <strong>부모를 만들어 속성만 상속해서 사용하고 싶을 때 사용합니다. 속성만 상속하고 DB 테이블은 따로 사용하게 됩니다.</strong></p>
<hr>


<pre><code class="language-java">@MappedSuperclass
public class BaseEntity {
    private String createdBy;
    private String createdData;
    ...
}
// --------------------------------------
public class Member extends BaseEntity {
    ...
}</code></pre>
<p><code>@MappedSuperclass</code> 가 선언된 클래스가 매핑 정보만 받는 슈퍼 클래스라고 생각하면 됩니다. BaseEntity 는 테이블이 생성되지 않고 Member 테이블이 생성될 때 속성들이 추가되어 생성됩니다.</p>
<p>상속 받는 자식 클래스에 매핑 정보만 제공할 뿐 엔티티가 아니기 때문에 테이블과 매핑되지 않고, 조회나 검색도 불가능합니다. 이를 구현할 때는 추상 클래스를 권장합니다.</p>
<p>참고로 JPA 에서 extends 를 사용할 때는 <code>@Entity</code> 나 <code>@MappedSuperclass</code> 가 붙은 클래스들만 상속 받을 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA 기본편] 5. 다양한 연관관계 매핑]]></title>
            <link>https://velog.io/@hj_/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-5.-%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/@hj_/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-5.-%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>Fri, 23 Feb 2024 01:06:56 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한 님의 <a href="https://www.inflearn.com/course/ORM-JPA-Basic/dashboard">자바 ORM 표준 JPA 프로그래밍 - 기본편</a> 강의를 보고 작성한 내용입니다.</p>
</blockquote>
<br>



<h1 id="1-연관관계-매핑-시-고려사항">1. 연관관계 매핑 시 고려사항</h1>
<p><strong>1. 다중성</strong></p>
<table>
<thead>
<tr>
<th>관계</th>
<th>어노테이션</th>
</tr>
</thead>
<tbody><tr>
<td>다대일</td>
<td>@ManyToOne</td>
</tr>
<tr>
<td>일대다</td>
<td>@OneToMany</td>
</tr>
<tr>
<td>일대일</td>
<td>@OneToOne</td>
</tr>
<tr>
<td>다대다</td>
<td>@ManyToMany</td>
</tr>
</tbody></table>
<hr>


<p><strong>2. 단방향, 양방향</strong></p>
<p><strong>테이블은 외래키 하나로 양쪽 조인이 가능</strong>하므로 사실상 방향 이라는 개념은 없습니다.</p>
<p>반대로 객체는 <strong>참조용 필드가 있는 쪽으로만 참조가 가능</strong>합니다. 한쪽만 참조하면 단방향, 양쪽이 서로 참조하면 양방향입니다.</p>
<hr>


<p><strong>3. 연관관계의 주인</strong></p>
<p><strong>테이블은 외래키 하나로 두 테이블이 연관관계</strong>를 맺습니다. <strong>객체에서 양방향 관계는 두 개의 참조</strong>를 사용해야 하는데, 이 둘 중에서 <strong>어느 참조가 외래키를 관리할 것인지를 지정</strong>해야합니다.</p>
<p>외래키를 관리하는 참조를 연관관계의 주인이라고 하며, 주인의 반대편은 외래키에 영향을 주지 않고 단순 조회만 가능합니다.</p>
<p><br><br><br></p>
<h1 id="2-다대일">2. 다대일</h1>
<p><img src="https://velog.velcdn.com/images/hj_/post/e3868cec-63a9-44bb-ba1e-e24672d792f4/image.png" alt=""></p>
<p>데이터베이스 테이블의 일(1), 다(N) 관계에서 외래 키는 항상 N 쪽에 있습니다. 따라서 <strong>객체 양방향 관계에서 연관관계의 주인은 항상 N 쪽</strong>입니다.</p>
<p>단방향에서 양방향으로 만들기 위해 반대쪽 사이드에 참조를 추가한다고 해서 테이블에 전혀 영향을 주지 않습니다. 왜냐하면 연관관계의 주인이 이미 외래키를 관리하고 있기 때문입니다.</p>
<p><br><br><br></p>
<h1 id="3-일대다">3. 일대다</h1>
<h2 id="3-1-일대다-단방향">3-1. 일대다 단방향</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/9984c9ec-b696-41fc-9320-4fe8fa5384e7/image.png" alt=""></p>
<p>일대다 단방향은 다대일의 반대로, 일대다(1 : N)에서 <strong>일(1)이 연관관계의 주인</strong>입니다. </p>
<p>위의 예시로 살펴보면 Team 은 Member 를 알고 싶은데, Member 는 Team 을 알고 싶지 않은 경우입니다. 하지만 <strong>테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 존재</strong>합니다. </p>
<p>그래서 위의 설계를 구현하게 되면 <strong>Team 의 List memebrs 값을 변경했을 때 Member 테이블이 가진 TEAM_ID 외래키가 업데이트</strong>됩니다. </p>
<hr>


<pre><code class="language-java">public class Team {
    ...
    @OneToMany
    @JoinColumn(name = &quot;TEAM_ID&quot;)   // 외래키를 관리
    private List&lt;Member&gt; members = new ArrayList&lt;&gt;();
}
// ---------------------------------------------------
Member member = new Member();
member.setUsername(&quot;memebr1&quot;);
em.persist(member);

Team team = new Team();
team.setName(&quot;teamA&quot;);
team.getMembers().add(member);

em.persist();</code></pre>
<p>일대다 단방향의 경우, <code>@OneToMany</code> 와 함께 <code>@JoinColumn</code> 을 사용해서 외래키를 관리합니다. 일대다에서 <code>@JoinColumn</code> 은 필수이기 때문에 사용하지 않으면 중간에 테이블을 하나 추가하는 조인 테이블 방식을 사용하게 됩니다.</p>
<p>위의 코드에서 <code>team.getMembers().add(member)</code>가 수행될 때 <strong>Member 테이블의 외래키를 업데이트하는 쿼리</strong>가 날라가게됩니다. 그래서 Member 저장 1번, Team 저장 1번, Member 업데이트 1번해서 <strong>총 3번의 쿼리가 실행</strong>됩니다.</p>
<p>해당 관계는 엔티티가 관리하는 외래키가 다른 테이블에 존재하며, 연관관계 관리를 위해 추가로 Update 쿼리가 실행된다는 단점때문에, <strong>다(N)측이 일(1)로 갈 일이 없어도 일대다 단방향보다는 다대일 양방향을 사용하는 것이 좋습니다.</strong></p>
<br>


<h2 id="3-2-일대다-양방향">3-2. 일대다 양방향</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/9ea7af53-836f-4629-8ac4-34e16e259a9e/image.png" alt=""></p>
<pre><code class="language-java">public class Member {
    ...
    @ManyToOne
    @JoinColum(name = &quot;TEAM_ID&quot;, insertable = false, updatable = false)
    private Team team;
}</code></pre>
<p>일대다 양방향은 다(N)측에 해당하는 Member 의 <code>@JoinColumn</code> 에서 <strong>insertable, updatable 속성을 사용해서 읽기 전용으로 만들어</strong> 구현할 수는 있습니다. </p>
<p>JPA 에서 일대다 양방향은 공식적으로 존재하지 않고, 다대일 양방향을 사용하는 것이 좋습니다.</p>
<p><br><br><br></p>
<h1 id="4-일대일">4. 일대일</h1>
<p>일대일 관계는 그 반대도 일대일 관계이며, <strong>주 테이블이나 대상 테이블 중에서 외래키를 어느 테이블에 넣을지 선택할 수 있습니다.</strong> 외래키에 DB 유니크 제약조건을 추가해야 일대일 관계가 됩니다.</p>
<p>회원은 하나의 사물함만 사용하고, 사물함도 하나의 회원에 의해서만 사용된다는 가정과 함께 아래 예시들을 살펴보겠습니다.</p>
<h2 id="4-1-주-테이블">4-1. 주 테이블</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/2afa81f7-4274-4e87-b7c1-8c19baa99b3c/image.png" alt=""></p>
<p><strong>주 테이블에 외래키 단방향</strong>의 경우 Member 에 LOCKER_ID 라는 외래키를 넣고 유니크 제약조건을 걸었을 때, Member 에 Locker 라는 참조를 선언하고 외래키를 매핑하면 됩니다.</p>
<p>양방향으로 만드려면 반대쪽에도 <code>@OneToOne</code> 어노테이션을 사용하고, <strong>mappedBy</strong> 속성을 사용하면 됩니다. 다대일 양방향 매핑 처럼 외래키가 있는 곳이 연관관계의 주인이 됩니다.</p>
<p>주 테이블에 외래키가 있을 때 <strong>단점은 값이 없으면 외래키에 null 을 허용해야 한다는 점</strong>입니다.</p>
<br>



<h2 id="4-2-대상-테이블">4-2. 대상 테이블</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/27de6221-c212-4b4b-b465-cbb38e95dc23/image.png" alt=""></p>
<p>대상 테이블에 외래키가 있다는 의미는 예를 들어, Member 의 Locker 참조가 외래키를 관리하고 싶은데 외래키가 Locker 테이블에 있는 것을 의미하는데 <strong>대상 테이블 단방향의 경우는 JPA 에서 지원하지 않습니다.</strong> </p>
<p>대상 테이블 양방향은 지원이 가능하며, 일대일 주 테이블 외래키 양방향과 매핑 방법은 같은데 단순하게 생각해서 <strong>일대일 관계는 내 엔티티에 있는 외래키는 내가 직접 관리해야 한다고 생각하면 됩니다.</strong></p>
<hr>


<p>대상 테이블에 외래키가 있을 때 <strong>단점은 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시로딩 된다는 점</strong>입니다.</p>
<p>주 테이블에 외래키 단방향 그림을 보았을 때 JPA 가 Member 를 로딩할 때 LOCKER_ID 에 값이 있으면 프록시를 넣고, 없으면 null 을 집어넣으면 됩니다. 그래서 Member 만 쿼리하면 됩니다.</p>
<p>하지만 대상 테이블에 양방향 그림을 보면 <strong>Member 를 조회할 때 Locker 의 값이 있는지 없는지 알려면 Member 테이블만 조회해서는 알 수 없기 때문에 Locker 에 Member 의 ID 가 있는지 확인해야 합니다.</strong> </p>
<p>그래서 어차피 두 테이블 모두에서 조회를 해야 프록시를 넣을지, null 을 넣을지 알게 되기 때문에 <strong>일대일에서 대상 테이블에 외래키가 있으면 지연로딩으로 설정해도 즉시로딩이 됩니다.</strong> </p>
<p><br><br><br></p>
<h1 id="5-다대다">5. 다대다</h1>
<blockquote>
<p>관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없습니다. 그래서 <strong>연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야 합니다</strong>.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/hj_/post/81b644e8-1e3b-43d2-a60b-b94a3327a0b0/image.png" alt=""></p>
<p>예를 들어, 회원이 여러 개의 상품을 선택할 수 있고 하나의 상품은 여러 명의 회원들에게 선택될 수 있습니다. 이런 다대다 관계를 MEMBER_PRODUCT 는 중간 테이블을 만들어 일대다, 다대일로 풀어야 합니다.</p>
<p>하지만 <strong>객체는 컬렉션을 이용해서 객체 2개로 다대다 관계를 만들 수 있습니다.</strong> <code>@ManyToMany</code> 를 사용하고 <code>@JoinTable</code> 로 연결 테이블을 지정하면 됩니다.</p>
<pre><code class="language-java">// 하나의 엔티티에만 지정하면 단방향, 둘 다 지정하면 양방향
public class Member {
    ...
    @ManyToMany
    @JoinTable(name = &quot;MEMBER_PRODUCT&quot;)
    private List&lt;Product&gt; products = new ArrayList&lt;&gt;();
}
// -------------------------------------------------------
public class Product {
    ...
    @ManyToMany
    @JoinTable(mappedBy = &quot;products&quot;)
    private List&lt;Member&gt; members = new ArrayList&lt;&gt;();
}</code></pre>
<br>


<h4 id="-다대다-매핑의-한계-">[ 다대다 매핑의 한계 ]</h4>
<p>위의 그림에서 연결 테이블인 MEMBER_PRODUCT 에는 두 개의 외래키만 가지고 있으며 추가적인 정보를 넣는게 불가능한데, 실제로 연결 테이블은 연결만 하고 끝나지 않고 주문시간이나 수량과 같은 데이터가 들어갈 수 있습니다. </p>
<h4 id="-다대다-한계-극복-">[ 다대다 한계 극복 ]</h4>
<p><img src="https://velog.velcdn.com/images/hj_/post/9a00bcb5-cebf-4b12-a60a-7f84d0f497b6/image.png" alt=""></p>
<p>연결 테이블용 엔티티를 추가( 연결 테이블을 엔티티로 승격 )해서 위처럼 <code>@ManyToMany</code> 를 <code>@OneToMany</code> 와 <code>@ManyToOne</code> 으로 사용하면 됩니다.</p>
<p><br><br><br></p>
<h1 id="6-어노테이션-속성">6. 어노테이션 속성</h1>
<h4 id="-joincolumn-">[ @JoinColumn ]</h4>
<table>
<thead>
<tr>
<th>속성</th>
<th>설명</th>
<th>기본값</th>
</tr>
</thead>
<tbody><tr>
<td>name</td>
<td>매핑할 외래키 이름</td>
<td>필드명_참조 테이블의 기본키</td>
</tr>
<tr>
<td>referencedColumnName</td>
<td>외래키가 참조하는 대상 테이블의 컬럼명</td>
<td>참조하는 테이블의 기본키</td>
</tr>
<tr>
<td>foreignKey</td>
<td>외래키 제약조건을 직접 생성, 테이블 생성 시에만 사용</td>
<td></td>
</tr>
</tbody></table>
<h4 id="-manytoone-">[ @ManyToOne ]</h4>
<table>
<thead>
<tr>
<th>속성</th>
<th>설명</th>
<th>기본값</th>
</tr>
</thead>
<tbody><tr>
<td>optional</td>
<td>false 로 설정하면 연관된 엔티티가 항상 있어야 한다</td>
<td>true</td>
</tr>
<tr>
<td>fetch</td>
<td>글로벌 패치 전략을 설정</td>
<td>FetchType.EAGER</td>
</tr>
<tr>
<td>cascade</td>
<td>영속성 전이 기능을 사용한다</td>
<td></td>
</tr>
<tr>
<td>targetEntity</td>
<td>연관된 엔티티의 타입 정보를 설정</td>
<td></td>
</tr>
</tbody></table>
<h4 id="-onetomany-">[ @OneToMany ]</h4>
<table>
<thead>
<tr>
<th>속성</th>
<th>설명</th>
<th>기본값</th>
</tr>
</thead>
<tbody><tr>
<td>mappedBy</td>
<td>연관관계의 주인 필드를 선택</td>
<td></td>
</tr>
<tr>
<td>fetch</td>
<td>글로벌 패치 전략을 설정</td>
<td>FetchType.LAZY</td>
</tr>
<tr>
<td>cascade</td>
<td>영속성 전이 기능을 사용한다</td>
<td></td>
</tr>
<tr>
<td>targetEntity</td>
<td>연관된 텐티티의 타입 정보를 설정</td>
<td></td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA 기본편] 4. 연관관계 매핑 기초]]></title>
            <link>https://velog.io/@hj_/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-4.-%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/@hj_/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-4.-%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>Thu, 22 Feb 2024 08:30:35 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한 님의 <a href="https://www.inflearn.com/course/ORM-JPA-Basic/dashboard">자바 ORM 표준 JPA 프로그래밍 - 기본편</a> 강의를 보고 작성한 내용입니다.</p>
</blockquote>
<br>


<h1 id="1-연관관계가-필요한-이유">1. 연관관계가 필요한 이유</h1>
<p>객체는 참조로 연관관계를 표현하고, 테이블은 외래키로 연관관계를 표현합니다. 그래서 <strong>객체의 참조와 테이블의 외래키를 매핑</strong>하는 방법을 알아야 합니다.</p>
<p>예를 들어 아래와 같은 관계가 있다고 가정해보겠습니다.</p>
<blockquote>
<p>회원과 팀이 있다
회원은 하나의 팀에만 소속될 수 있다
하나의 팀에는 여러 명의 회원이 존재한다
➜ 회원과 팀은 다대일 관계이다</p>
</blockquote>
<p>해당 관계에서 <strong>객체를 테이블에 맞추어 모델링</strong>하면 아래와 같고 이는 <strong>연관관계가 없는 객체</strong>가 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/8c420ec6-d953-4d05-83c0-618887b1be38/image.png" alt=""></p>
<pre><code class="language-java">@Entity
public class Member {
  @Id @GeneratedValue
  private Long id;
  private String name;
  @Column(name = &quot;TEAM_ID&quot;)
  private Long teamId;
  ...
}
// ------------------------
@Entity
public class Team {
  @Id @GeneratedValue
  private Long id;
  private String name;
  ...
}</code></pre>
<p>테이블에 맞추어 모델링을 했기 때문에 <strong>Member 가 Team 에 대한 참조를 가진 것이 아닌 Team 의 ID 를 FK 로 가지게 됩니다.</strong></p>
<hr>

<p>만약 회원의 팀을 저장하고 조회하려면 서로 연관관계가 없기 때문에 아래처럼 해야 합니다.</p>
<pre><code class="language-java">// 저장
Team team = new Team();
team.setName(&quot;teamA&quot;); 
em.persist(team);

Member member = new Member();
member.setName(&quot;memberA&quot;);
member.setTeamId(team.getId());
em.persist(member);
// ------------------------------------------------------
// 조회
Member findMember = em.find(Member.class, member.getId());

Long findTeamId = findMember.getId();
Team findTeam = em.find(Team.class, findTeamId);</code></pre>
<p>연관관계가 없기 때문에 member 를 저장할 때 Team 의 참조가 아닌 ID 를 사용합니다. 또한 회원의 팀을 조회할 때도 member 에서 Team 의 id 를 조회하고, team 의 id 로 다시 Team 을 조회해야 합니다.</p>
<p>이렇게 객체를 테이블에 맞추어 데이터 중심으로 모델링하면 협력 관계를 만들 수 없습니다.</p>
<p><br><br><br></p>
<h1 id="2-단방향-연관관계">2. 단방향 연관관계</h1>
<p>위에서 보았던 예제를 <strong>객체지향적으로 모델링</strong>하면 아래와 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/a9f3b724-2655-4c50-ae8e-fb687bf74367/image.png" alt=""></p>
<pre><code class="language-java">@Entity
public class Member {
  @Id @GeneratedValue
  private Long id;
  private String name;
  @ManyToOne
  @JoinColumn(name = &quot;TEAM_ID&quot;)
  private Team team;
  ...
}
// ------------------------
@Entity
public class Team {
  @Id @GeneratedValue
  private Long id;
  private String name;
  ...
}</code></pre>
<p>Member 를 보면 Team 의 ID 가 아닌 Team 의 참조를 그대로 들고 있습니다. 참조를 사용하면 아래 두 가지를 명시해주어야 합니다.</p>
<blockquote>
<ol>
<li><p>JPA 에게 현재 엔티티와 참조 엔티티가 <strong>어떤 관계</strong>인지</p>
</li>
<li><p>현재 엔티티에서 <strong>참조가 어떤 FK 와 매핑</strong>되는지</p>
</li>
</ol>
</blockquote>
<p>1번의 경우 회원과 팀은 다대일 관계이기 때문에 <code>@ManyToOne</code> 을 사용합니다. </p>
<p>2번의 경우 Member 가 가진 <strong>Team 의 참조와 테이블 관점에서 보았을 때 Member 가 가진 FK 를 매핑</strong>해주어야 하기 때문에 <code>@JoinColumn</code> 을 사용해서 이를 지정해줍니다.</p>
<hr>


<p>위처럼 모델링 했을 때 저장과 조회를 하는 코드는 아래처럼 변하게 됩니다.</p>
<pre><code class="language-java">// 저장
Team team = new Team();
team.setName(&quot;teamA&quot;);  // 단방향 연관관계 설정, 참조 저장
em.persist(team);

Member member = new Member();
member.setName(&quot;memberA&quot;);
member.setTeam(team);
em.persist(member);
// ------------------------------------------------------
// 조회
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();   // 참조를 사용해서 연관관계 조회 ( 객체 그래프 탐색 )</code></pre>
<p>저장할 때는 Team 의 참조를 넣어주면 되고, 조회할 때는 Member 에서 바로 Team 을 꺼내면 됩니다. 변경할 때도 <code>setTeam()</code> 에 Team 에 대한 참조만 넣어주면 됩니다.</p>
<p><br><br><br></p>
<h1 id="3-양방향-연관관계">3. 양방향 연관관계</h1>
<p>위에서 단방향 관계로 설정했기 때문에 Member ➜ Team 은 가능하지만 Team ➜ Member 는 불가능합니다. 이를 양방향 연관관계로 변경하면 아래와 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/hj_/post/444dc6c2-9a0f-4e06-8886-9b9b5aeb1ff9/image.png" alt=""></p>
<p><strong>테이블 연관관계를 보면 이전과 동일</strong>합니다. 왜냐하면 회원이 속한 팀을 알고 싶을 때도 TEAM_ID 로 조인하면 되고, 팀에 속한 회원을 알고 싶을 때도 TEAM_ID 로 조인하면 되기 때문입니다. </p>
<p>즉, <strong>테이블의 연관관계는 외래키 하나에 양방향이 다 있다는 것</strong>이고, 테이블에서는 외래키 하나 만으로 양쪽의 데이터를 가져올 수 있게 됩니다.</p>
<p>하지만 객체 연관관계에서 <strong>Team 을 보면 List Member 가 추가</strong>된 것을 볼 수 있는데 이렇게 해야 Team 에서 Member 로 갈 수 있기 때문에 추가된 것입니다.</p>
<p>이것이 객체 참조와 테이블의 외래키의 가장 큰 차이점인데 <strong>참조를 통한 객체 연관관계는 단방향, 외래키를 통한 테이블 연관관계는 양방향</strong>입니다.</p>
<hr>


<pre><code class="language-java">@Entity
public class Member {
  @Id
  @Column(name = &quot;MEMBER_ID&quot;)
  private String id;
  private String username;

  @ManyToOne
  @JoinColumn(name=&quot;TEAM_ID&quot;)
  private Team team;
  ...
}
// ------------------------
@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;();
  ...
}</code></pre>
<p>Team 에 입장에서 일대다 관계이기 때문에 <code>@OneToMany</code> 를 사용하고 이를 List 로 묶습니다. 이때 미리 초기화 시켜야 <code>add()</code> 를 호출했을 때 NullPointerException 이 발생하지 않습니다.</p>
<p><code>@OneToMany</code> 를 보면 <strong>mappedBy</strong> 라는 속성이 사용된 것을 볼 수 있는데, <strong>team 이라는 변수명에 연결되어 있음을 표시함과 동시에 연관관계의 주인이 아님을 명시</strong>합니다.</p>
<p>이렇게 하면 Member ➜ Team, Team ➜ Member 가 둘 다 가능한 양방향 연관관계가 완성됩니다.</p>
<p><br><br><br></p>
<h1 id="4-양방향-연관관계의-주인">4. 양방향 연관관계의 주인</h1>
<h2 id="4-1-객체와-테이블이-관계를-맺는-차이">4-1. 객체와 테이블이 관계를 맺는 차이</h2>
<p>객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단뱡향 관계 2개입니다. 그래서 <strong>객체를 양방향으로 참조하려면 단방향 연관관계를 2개</strong> 만들어야 합니다.</p>
<p>하지만 테이블은 외래키 하나로 두 테이블의 연관관계를 관리하고, <strong>외래키 하나로 양방향 연관관계</strong>를 가져 양쪽으로 조인할 수 있습니다.</p>
<p>결국 엔티티와 테이블의 차이점은 <strong>엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 테이블에서는 외래키 하나</strong>라는 점입니다.</p>
<p>2번에서 객체에서 연관관계를 표현하기 위해 <strong>참조에 FK 를 매핑</strong>해주었습니다. 단방향일 때는 참조가 1개였지만, 양방향일 때는 참조가 2개입니다. 그래서 <strong>어떤 참조에 외래키를 매핑해야 할 지 문제가 발생</strong>하는데 이때 등장하는 개념이 <strong>연관관계의 주인</strong>입니다.</p>
<br>



<h2 id="4-2-연관관계의-주인">4-2. 연관관계의 주인</h2>
<p><img src="https://velog.velcdn.com/images/hj_/post/1b418366-729b-4062-bd43-96041217c0f6/image.png" alt=""></p>
<p>정리하자면 위의 그림처럼 양방향 관계일 때는 Member 에 있는 Team 참조에 FK 를 매핑할 것인지, Team 에 있는 Member 참조에 FK 를 매핑할 것인지를 결정해야 합니다. </p>
<p><img src="https://velog.velcdn.com/images/hj_/post/4cc01a5f-880c-4d76-9315-2d46f9b97340/image.png" alt=""></p>
<p><strong>연관관계의 주인을 정할 때는 외래키가 있는 곳을 주인으로 정하면 됩니다.</strong> 위의 예시에서는 Member.team 이 연관관계의 주인이 됩니다.</p>
<p>그래서 연관관계의 주인이 아닌 Team 에 <strong>mappedBy</strong> 가 붙었으며, Member 의 team 필드와 매핑되어 있다고 표시합니다.</p>
<h4 id="-양방향-매핑-규칙-">[ 양방향 매핑 규칙 ]</h4>
<blockquote>
<ol>
<li><p>객체의 두 관계 중 하나를 연관관계의 주인으로 지정합니다.</p>
</li>
<li><p><strong>연관관계의 주인만이 외래키를 관리합니다. ( 등록, 수정이 가능합니다 )</strong></p>
</li>
<li><p><strong>주인이 아닌 쪽은 읽기만 가능합니다.</strong></p>
</li>
<li><p>주인은 mappedBy 속성을 사용하지 않습니다.</p>
</li>
<li><p><strong>주인이 아니면 mappedBy 속성으로 주인을 지정합니다.</strong></p>
</li>
</ol>
</blockquote>
<br>



<h2 id="4-3-양방향-매핑-시-주의점">4-3. 양방향 매핑 시 주의점</h2>
<h3 id="1-연관관계-주인에-값을-입력하지-않음">1. 연관관계 주인에 값을 입력하지 않음</h3>
<h4 id="-잘못된-코드-">[ 잘못된 코드 ]</h4>
<pre><code class="language-java">Team team = new Team();
team.setName(&quot;teamA&quot;);
em.persist(team);

Member member = new Member();
member.setUsername(&quot;memebrA&quot;);

team.getMembers().add(member);  // 연관관계의 주인이 아닌 값만 설정
em.persist(member);</code></pre>
<p>위의 코드를 실행하고 Member 와 Team 을 조회하면 아래와 같습니다. Member 테이블의 TEAM_ID 를 보면 NULL 인 것을 알 수 있습니다.</p>
<table>
<thead>
<tr>
<th>MEMBER_ID</th>
<th>USERNAME</th>
<th>TEAM_ID</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>member1</td>
<td>null</td>
</tr>
</tbody></table>
<br>


<table>
<thead>
<tr>
<th>TEAM_ID</th>
<th>NAME</th>
</tr>
</thead>
<tbody><tr>
<td>2</td>
<td>TeamA</td>
</tr>
</tbody></table>
<p>Member 를 보면 Team 참조가 연관관계의 주인이고, Team 의 List Member 는 mappedBy 로 설정되어 읽기 전용입니다. 그래서 <strong>JPA 는 Insert 나 Update 를 할 때 mappedBy 가 붙은 부분은 확인하지 않습니다.</strong></p>
<p>하지만 위의 코드에서는 연관관계의 주인인 Member.team 에 값을 세팅하지 않았기 때문에 MEMBER 의 TEAM_ID 값이 null 이 됩니다. <strong>올바르게 하려면 양방향 매핑 시, 연관관계의 주인에 값을 입력하는 것이 맞습니다.</strong></p>
<h4 id="-올바른-코드-">[ 올바른 코드 ]</h4>
<pre><code class="language-java">Team team = new Team();
team.setName(&quot;teamA&quot;);
em.persist(team);

Member member = new Member();
member.setUsername(&quot;memebrA&quot;);

member.setTeam(team); // 연관관계 주인
em.persist(member);</code></pre>
<br>


<h3 id="2-순수-객체-관계를-고려해서-항상-양쪽에-값을-세팅해야-한다">2. 순수 객체 관계를 고려해서 항상 양쪽에 값을 세팅해야 한다</h3>
<p>순수한 객체 관계를 고려하면 <strong>연관관계의 주인에도 세팅을 해주고, 주인이 아닌 쪽에도 값을 세팅해주는 것이 맞는 방법</strong>입니다. 아래 예시를 보겠습니다.</p>
<pre><code class="language-java">Team team = new Team();
team.setName(&quot;teamA&quot;);
em.persist(team);

Member member = new Member();
member.setUsername(&quot;memebrA&quot;);
member.setTeam(team); // 연관관계 주인
em.persist(member);

Team findTeam = em.find(Team.class, team.getId());
List&lt;Member&gt; members = findTeam.getMembers();

for(Member m : members) {
  System.out.println(&quot;m = &quot; + m.getName());
}

tx.commit();</code></pre>
<p>위의 코드를 실행시키면 아무것도 출력되지 않습니다. 왜냐하면 Member 와 Team 이 지연로딩이 설정되어 있어, <strong>조회를 하려면 DB 에 select 쿼리가 나가야 하는데 1차 캐시에 있는 값을 가져왔기 때문</strong>입니다.</p>
<p><code>persist()</code> 로 인해 Team 이 영속성 컨텍스트에 존재하게 되고,  <code>find()</code> 를  실행할 때 1차 캐시에서 가지고 오게 됩니다. 그래서 DB에 쿼리가 나가지 않아 가져온 Team 의 List 에는 값이 존재하지 않게 됩니다. 그래서 아래처럼 양쪽 모두에 값을 세팅하는 것이 올바른 방법입니다.</p>
<pre><code class="language-java">Team team = new Team();
team.setName(&quot;teamA&quot;);

Member member = new Member();
member.setUsername(&quot;memebrA&quot;);

member.setTeam(team); // 연관관계 주인
team.getMembers().add(member);  // 주인이 아닌 관계
em.persist(team);
em.persist(member);</code></pre>
<hr>


<h4 id="영한님이-추천하는-방식">영한님이 추천하는 방식</h4>
<pre><code class="language-java">public class Member {
  ...
  public void setTeam(Team team) {
    this.team = team;
    team.getMember().add(this);
  }
}</code></pre>
<p>연관관계의 주인에 값을 세팅하는 것과, 연관관계의 주인이 아닌 곳에 값을 세팅하는 코드 두 개를 작성해야 하는데 이를 연관관계 편의 메서드를 생성해서 하나로 묶는 방식을 추천하셨습니다.</p>
<br>


<h2 id="4-4-양방향-매핑-정리">4-4. 양방향 매핑 정리</h2>
<p>단방향 매핑만으로도 이미 연관관계 매핑은 완료된 것입니다. 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색)기능이 추가된 것 뿐입니다. </p>
<p>그래서 단방향 매핑을 잘 하고, 양방향은 필요할 때 추가해도 테이블에 영향을 주지 않기 때문에 상관없습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA 기본편] 3. 엔티티 매핑]]></title>
            <link>https://velog.io/@hj_/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-3.-%EC%97%94%ED%8B%B0%ED%8B%B0-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@hj_/JPA-%EA%B8%B0%EB%B3%B8%ED%8E%B8-3.-%EC%97%94%ED%8B%B0%ED%8B%B0-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Thu, 22 Feb 2024 01:12:53 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>김영한 님의 <a href="https://www.inflearn.com/course/ORM-JPA-Basic/dashboard">자바 ORM 표준 JPA 프로그래밍 - 기본편</a> 강의를 보고 작성한 내용입니다.</p>
</blockquote>
<br>



<h1 id="1-엔티티-매핑">1. 엔티티 매핑</h1>
<p>엔티티 매핑에 필요한 어노테이션들은 아래와 같습니다.</p>
<blockquote>
<ol>
<li><p>객체와 테이블 매핑 : @Entity, @Table</p>
</li>
<li><p>필드와 컬럼 매핑 : @Column</p>
</li>
<li><p>기본 키 매핑 : @Id</p>
</li>
<li><p>연관관계 매핑 : @ManyToOne, @JoinColumn</p>
</li>
</ol>
</blockquote>
<p><br><br><br></p>
<h1 id="2-객체와-테이블-매핑">2. 객체와 테이블 매핑</h1>
<h2 id="2-1-entity">2-1. @Entity</h2>
<p><code>@Entity</code> 가 붙은 클래스는 JPA 가 관리하게 되며, 엔티티라고 합니다. JPA 를 사용해서 테이블과 매핑할 클래스는 <code>@Entity</code> 가 필수적으로 붙어야 합니다.</p>
<p>JPA 는 객체를 프록싱하는 등의 기능이 있기 때문에 <strong>파라미터가 없는 public 이나 protected 기본 생성자가 필수로 필요</strong>합니다. </p>
<p>final 클래스, inner 클래스, interface, enum 에는 사용할 수 없고, DB 에 저장하고 싶은 필드에 final 키워드를 사용하면 안됩니다.</p>
<table>
<thead>
<tr>
<th>속성</th>
<th>기능</th>
<th>기본값</th>
</tr>
</thead>
<tbody><tr>
<td>name</td>
<td>JPA 에서 사용할 엔티티 이름을 지정합니다. JPA 가 내부적으로 사용하는 이름이며 같은 클래스 이름이 없으면 가급적 기본값을 사용합니다. 만약 다른 패키지에 같은 클래스 이름이 있다면 name 속성을 지정해주어야 합니다.</td>
<td>클래스 이름을 그대로 사용</td>
</tr>
</tbody></table>
<br>


<h2 id="2-2-table">2-2. @Table</h2>
<p><code>@Table</code> 은 엔티티와 매핑할 테이블 지정합니다.</p>
<table>
<thead>
<tr>
<th>속성</th>
<th>기능</th>
<th>기본값</th>
</tr>
</thead>
<tbody><tr>
<td>name</td>
<td>매핑할 테이블 이름</td>
<td>엔티티 이름을 사용</td>
</tr>
<tr>
<td>catalog</td>
<td>DB catalog 매핑</td>
<td></td>
</tr>
<tr>
<td>schema</td>
<td>DB schema 매핑</td>
<td></td>
</tr>
<tr>
<td>uniqueConstraints( DDL )</td>
<td>DDL 생성 시에 유니크 제약 조건 생성</td>
<td></td>
</tr>
</tbody></table>
<h4 id="-사용-예시-">[ 사용 예시 ]</h4>
<pre><code class="language-java">@Table(uniqueConstraints = 
    {@UniqueConstraint( 
        name = &quot;NAME_AGE_UNIQUE&quot;,
        columnNames = {&quot;NAME&quot;, &quot;AGE&quot;} )
    }
) </code></pre>
<p><br><br><br></p>
<h1 id="3-데이터베이스-스키마-자동-생성">3. 데이터베이스 스키마 자동 생성</h1>
<p>JPA 는 매핑 정보만 보면 어떤 테이블인지, 어떤 쿼리를 만들어야 하는지 다 알 수 있습니다. 그래서 JPA 는 <strong>객체를 만들고 매핑을 하면 애플리케이션 로딩 시점에 DB 테이블을 생성하는 기능을 지원</strong>해줍니다. </p>
<p>이떄 JPA 는 <strong>데이터베이스 방언을 활용해서 DB 에 맞는 적절한 DDL 을 생성</strong>합니다. 예를 들어, <code>hibernate.dialect</code> 를 Oracle 로 설정하면 String 을 varchar2 로 생성하고, MySQL 로 지정하면 varchar 로 생성합니다.</p>
<p>이렇게 생성된 DDL 은 꼭 개발에서만 사용해야 하는데, 운영에서 create, create-drop, update 를 사용하면 안됩니다.</p>
<h4 id="-hibernatehbm2ddlauto-">[ hibernate.hbm2ddl.auto ]</h4>
<table>
<thead>
<tr>
<th>옵션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>create</td>
<td>기존 테이블 삭제 후 다시 생성 (DROP + CREATE)</td>
</tr>
<tr>
<td>create-drop</td>
<td>create와 같지만 종료시점에 테이블 DROP (DROP + CREATE + DROP)</td>
</tr>
<tr>
<td>update</td>
<td>DB 테이블과 엔티티를 비교해서 변경분만 반영(DROP 하지 않는다)</td>
</tr>
<tr>
<td>validate</td>
<td>엔티티와 테이블이 정상 매핑되었는지만 확인해서 차이가 존재하면 오류 메세지가 출력되며 애플리케이션이 실행되지 않습니다</td>
</tr>
<tr>
<td>none</td>
<td>사용하지 않음. none 이라는 옵션은 없지만 관례 상 none 이라고 적고, 매칭되는 옵션이 없기 때문에 실행되지 않습니다</td>
</tr>
</tbody></table>
<p><br><br><br></p>
<h1 id="4-필드와-컬럼-매핑">4. 필드와 컬럼 매핑</h1>
<p>컬럼 매핑에 사용되는 어노테이션들은 아래와 같습니다.</p>
<blockquote>
<ol>
<li><p>컬럼 매핑 : @Column</p>
</li>
<li><p>날짜 타입 매핑 : @Temporal ( 스프링에서 LocalDateTime 타입을 사용하면 자동으로 날짜 타입이 됩니다 )</p>
</li>
<li><p>enum 타입 매핑 : @Enumrated</p>
</li>
<li><p>BLOB, CLOB 매핑 : @Lob</p>
</li>
<li><p>특정 필드를 컬럼에 매핑하지 않음( 매핑 무시 ) : @Transient</p>
</li>
</ol>
</blockquote>
<br>


<h2 id="4-1-컬럼-매핑">4-1. 컬럼 매핑</h2>
<ul>
<li><p><strong>name</strong> : 필드와 매핑할 테이블의 컬럼 이름 ( 기본값 : 객체의 필드 이름 )</p>
</li>
<li><p><strong>length</strong> : 문자 길이 제약 조건으로 String 타입에만 사용합니다.</p>
</li>
<li><p><strong>nullable</strong> : null 값의 허용 여부를 설정합니다. false 로 설정하면 DDL 생성 시에 not null 제약 조건이 붙습니다. ( 기본값 : true )</p>
</li>
<li><p><strong>insertable / updatable</strong> : 컬럼 값을 수정했을 때, DB 에 저장, 혹은 수정할 것인지에 대한 여부를 지정합니다. ( 기본값 : true )</p>
</li>
<li><p><strong>unique</strong></p>
<ul>
<li><p>@Table의 uniqueConstraints와 같지만 한 컬럼에 간단히 유니크 제약조건을 걸 때 사용합니다. </p>
</li>
<li><p>복잡적으로 사용할 수 없고 하나의 컬럼에만 사용할 수 있습니다. </p>
</li>
<li><p>제약조건의 이름이 알아볼 수 없게 저장되기 때문에 잘 사용하지 않습니다. ( <code>@Table</code> 에서 지정하면 이름 지정과 복잡 지정이 가능 )</p>
</li>
</ul>
</li>
<li><p><strong>columnDefinition</strong></p>
<ul>
<li><p>데이터베이스 컬럼 정보를 직접 줄 수 있습니다.  </p>
</li>
<li><p>ex&gt; columnDefinition = &quot;varchar(100) default &#39;EMPTY&#39;&quot;</p>
</li>
</ul>
</li>
<li><p><strong>percision / scale</strong></p>
<ul>
<li><p>BigDecimal, BigInteger 타입에서 사용합니다. (double, float 에는 적용되지 않습니다)</p>
</li>
<li><p>percision 은 소수점을 포함한 전체 자리수를 나타냅니다</p>
</li>
<li><p>scale 은 소수의 자리수를 나타냅니다.</p>
</li>
</ul>
</li>
</ul>
<br>


<h2 id="4-2-타입에-따른-매핑">4-2. 타입에 따른 매핑</h2>
<h4 id="-날짜-타입-">[ 날짜 타입 ]</h4>
<p><code>@Temporal</code> 은 Calendar 와 같은 날짜 타입을 매핑할 때 사용합니다. 
만약 <strong>LocalDate, LocalDateTime 을 사용하면 어노테이션 자체를 생략</strong>할 수 있습니다.
속성은 <strong>value</strong> 가 있습니다.</p>
<table>
<thead>
<tr>
<th>속성</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>TemporalType.DATE</td>
<td>날짜, 데이터베이스 date 타입과 매핑</td>
</tr>
<tr>
<td>TemporalType.TIME</td>
<td>시간, 데이터베이스 time 타입과 매핑</td>
</tr>
<tr>
<td>TemporalType.TIMESTAMP</td>
<td>날짜와 시간, 데이터베이스 timestamp 타입과 매핑</td>
</tr>
</tbody></table>
<br>


<h4 id="-enum-타입-">[ Enum 타입 ]</h4>
<p>자바 Enum 타입을 매핑할 때 <code>@Enumrated</code> 를 사용합니다. 속성은 <strong>value</strong> 가 있습니다.</p>
<table>
<thead>
<tr>
<th>속성</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>EnumType.ORDINAL</td>
<td>enum 순서를 0부터 시작해서 데이터베이스에 저장( 기본값 )</td>
</tr>
<tr>
<td>EnumType.STRING</td>
<td>enum 이름을 데이터베이스에 저장( 사용 권장 )</td>
</tr>
</tbody></table>
<br>


<h4 id="-큰-타입-">[ 큰 타입 ]</h4>
<p>varchar 를 넘어가는 큰 컨텐츠를 넣고 싶은 경우에 <code>@Lob</code>를 사용합니다. 속성은 존재하지 않습니다. 매핑하는 필드 타입이 <strong>String, char[] 와 같은 문자이면 CLOB</strong> 매핑, 나머지는 BLOB 를 매핑합니다.</p>
<br>


<h4 id="-임시-타입-">[ 임시 타입 ]</h4>
<p>DB 랑 관계없이 메모리에서 계산하고 싶은 필드에 <code>@Transient</code> 를 사용합니다. 해당 어노테이션을 붙이면 필드가 매핑되지 않아 DB 에 저장되지 않습니다. </p>
<p><br><br><br></p>
<h1 id="5-기본-키-매핑">5. 기본 키 매핑</h1>
<h2 id="5-1-기본-키-매핑-어노테이션">5-1. 기본 키 매핑 어노테이션</h2>
<p>JPA가 제공하는 데이터베이스 기본 키 매핑 어노테이션은 아래와 같습니다.</p>
<ul>
<li><p>직접 할당 : 기본 키를 애플리케이션에서 직접 할당할 때 <code>@Id</code> 를 사용합니다.</p>
</li>
<li><p>자동 생성 : 데이터베이스가 값을 자동으로 할당해줄 때는 <code>@GeneratedValue</code> 를 사용합니다.</p>
<ul>
<li><p><strong>IDENTITY</strong> : 데이터베이스에 위임 ( MySQL )</p>
</li>
<li><p><strong>SEQUENCE</strong> : 데이터베이스 시퀀스 오브젝트 사용, <code>@SequenceGenerator</code> 지정 가능 ( Oracle )</p>
</li>
<li><p><strong>TABLE</strong> : 키 생성용 테이블 사용, 모든 DB 에서 사용하며 <code>@TableGenerator</code> 지정 가능</p>
</li>
<li><p><strong>AUTO</strong> : 데이터베이스 방언에 따라 자동으로 지정하며, 기본값입니다.</p>
</li>
</ul>
</li>
</ul>
<br>


<h2 id="5-2-identity-전략">5-2. IDENTITY 전략</h2>
<p>기본키 생성을 데이터베이스에 위임하는 전략입니다. 주로 MySQL, PostgreSQL, MS SQL Server 에서 사용합니다. MySQL 에서는 <strong>AUTO_INCREMENT</strong> 가 있는데 <strong>null 로 집어넣으면 DB가 알아서 순차적으로 올라가도록 값을 세팅</strong>해줍니다.</p>
<p>JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL 실행하는데  <strong>IDENTITY 는 데이터베이스에 INSERT SQL을 실행한 이후에 ID 값을 알 수 있습니다.</strong> </p>
<p>엔티티가 영속 상태가 되려면( 1차 캐시에 저장되려면 ) 식별자가 필요하기 때문에 <strong>IDENTITY 전략은 <code>em.persist()</code> 시점에 즉시 INSERT SQL 실행하고 DB에서 식별자를 조회</strong>합니다.</p>
<hr>


<h4 id="-sequence-사용-">[ SEQUENCE 사용 ]</h4>
<pre><code class="language-sql">========= persist member1 이전 호출
Hibernate: 
    select 
        next value for MEMBER_SEQ
========= persist member1 이후 호출
========= transaction commit
Hibernate: 
    insert into
        Member (name, id) 
    values
        (?, ?)</code></pre>
<p>SEQUENCE 를 사용하면 <code>persist()</code> 를 호출해도 Insert 쿼리가 수행되지 않고, <strong>트랜잭션이 커밋될 때 INSERT 쿼리가 수행</strong>되는 것을 확인할 수 있습니다.</p>
<h4 id="-identity-사용-">[ IDENTITY 사용 ]</h4>
<pre><code class="language-sql">========= persist member1 이전 호출
Hibernate: 
    insert into
        Member (name, id) 
    values
        (?, default)
========= persist member1 이후 호출
========= transaction commit</code></pre>
<p>하지만 IDENTITY 는 트랜잭션이 커밋되지 않아도 <strong><code>persist()</code> 를 호출하면 바로 INSERT 쿼리가 수행</strong>되는 것을 확인할 수 있습니다.</p>
<br>


<h2 id="5-3-sequence-전략">5-3. SEQUENCE 전략</h2>
<p>데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트입니다. 주로 Oracle, PostgreSQL, H2 에서 사용합니다.</p>
<blockquote>
<p>create sequence Member_SEQ start with 1 increment by 50</p>
</blockquote>
<p>웹 애플리케이션을 실행시키면 위와 같은 로그가 출력되는데 create sequence 를 통해 시퀀스를 만들어내는 것을 확인할 수 있습니다.</p>
<hr>



<h4 id="-sequencegenerator-">[ @SequenceGenerator ]</h4>
<pre><code class="language-java">@Entity
@SequenceGenerator(
        name = &quot;MEMBER_SEQ_GENERATOR&quot;,
        sequenceName = &quot;MEMBER_SEQ&quot;,
        initialValue = 1,
        allocationSize = 1
)
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE,
                    generator = &quot;MEMBER_SEQ_GENERATOR&quot;)
    private Long id;
    ...
}</code></pre>
<p>테이블마다 다른 시퀀스를 사용하고 싶은 경우, <code>@SequenceGenerator</code> 를 통해서 시퀀스를 생성하고, <code>@GeneratedValue</code> 에 generator 속성에 매핑해서 사용할 수 있습니다.</p>
<table>
<thead>
<tr>
<th>속성</th>
<th>기능</th>
<th>기본값</th>
</tr>
</thead>
<tbody><tr>
<td>name</td>
<td>식별자 생성기 이름으로써 필수값입니다</td>
<td></td>
</tr>
<tr>
<td>sequenceName</td>
<td>DB 에 등록되어 있는 시퀀스 이름</td>
<td></td>
</tr>
<tr>
<td>initValue</td>
<td>DDL 생성 시에만 사용되며, 시퀀스 DDL 을 처음 생성할때 처음 1 시작하는 수를 지정합니다</td>
<td>1</td>
</tr>
<tr>
<td>allocationSize</td>
<td>시퀀스 한 번 호출에 증가하는 수</td>
<td>50</td>
</tr>
</tbody></table>
<p>참고로 위처럼 지정하고 실행하면 <strong>allocationSize</strong> 에 의해 로그가 아래처럼 변하게 됩니다.</p>
<blockquote>
<p>create sequence MEMBER_SEQ start with 1 increment by 1</p>
</blockquote>
<hr>


<h4 id="-동작-확인-">[ 동작 확인 ]</h4>
<pre><code class="language-java">Member member = new Member();
member.setName(&quot;HelloA&quot;);
System.out.println(&quot;========= persist member1&quot;);
em.persist(member);
System.out.println(&quot;========= member1 id = &quot; + member.getId());

Member member2 = new Member();
member2.setName(&quot;HelloB&quot;);
System.out.println(&quot;========= persist member2&quot;);
em.persist(member2);
System.out.println(&quot;========= member2 id = &quot; + member2.getId());

System.out.println(&quot;========= transaction commit&quot;);
tx.commit();</code></pre>
<pre><code class="language-sql">========= persist member1
Hibernate: 
    select
        next value for MEMBER_SEQ
========= member1 id = 1

========= persist member2
Hibernate: 
    select
        next value for MEMBER_SEQ
========= member1 id = 2

========= transaction commit
Hibernate: 
    insert into Member (name, id) values (?, ?)
Hibernate: 
    insert into Member (name, id) values (?, ?)</code></pre>
<p>위의 로그를 보면 <strong><code>persist()</code> 를 호출하면 데이터베이스 시퀀스를 사용해서 식별자를 먼저 조회</strong>하는 것을 확인할 수 있습니다. </p>
<p>그리고 <strong>조회한 식별자를 엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장</strong>하기 때문에 <code>persist()</code> 호출 후에 id 값을 확인해보면 정상적으로 출력되는 것을 확인할 수 있습니다.</p>
<p>Insert 쿼리는 트랜잭션 커밋이 호출되었을 때 실행되게 됩니다.</p>
<br>



<h2 id="5-4-table-전략">5-4. TABLE 전략</h2>
<p>TABLE 전략은 <strong>키 생성 전용 테이블</strong>을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략입니다. 모든 데이터베이스에 적용 가능하다는 장점이 있지만 락이 걸릴 수도 있는 등 성능이 좋지 않습니다. </p>
<pre><code class="language-java">@TableGenerator(
        name = &quot;MEMBER_SEQ_GENERATOR&quot;,
        table = &quot;MY_SEQUENCES&quot;,
        pkColumnValue = &quot;MEMBER_SEQ&quot;, allocationSize = 1)
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.TABLE,
                    generator = &quot;MEMBER_SEQ_GENERATOR&quot;)
    private Long id;
    ...
}</code></pre>
<p>시퀀스와 동일하게 어노테이션으로 generator 를 지정하고, <code>@GeneratedValue</code> 에 매핑하는 방식을 사용합니다.</p>
<pre><code class="language-sql">Hibernate: 
    create table MY_SEQUENCES (
        next_val bigint,
        sequence_name varchar(255) not null,
        primary key (sequence_name)
    )
Hibernate: 
    insert into MY_SEQUENCES(sequence_name, next_val) values (&#39;MEMBER_SEQ&#39;, 0)</code></pre>
<p>애플리케이션을 실행하면 JPA 가 테이블을 생성해주면서 0 값을 넣어주게 됩니다. 
그 뒤에 데이터를 INSERT 하게 되면 <strong>1부터 시작되는 PK</strong> 를 갖게 됩니다.</p>
<table>
<thead>
<tr>
<th>속성</th>
<th>기능</th>
<th>기본값</th>
</tr>
</thead>
<tbody><tr>
<td>name</td>
<td>식별자 생성기 이름으로써 필수값입니다</td>
<td></td>
</tr>
<tr>
<td>table</td>
<td>키 생성 테이블명</td>
<td></td>
</tr>
<tr>
<td>pkColumnName</td>
<td>시퀀스 컬럼명</td>
<td>sequence_name</td>
</tr>
<tr>
<td>valueColumnName</td>
<td>시퀀스 값 컬럼명</td>
<td>next_val</td>
</tr>
<tr>
<td>pkColumnValue</td>
<td>키로 사용할 값 이름</td>
<td>엔티티 이름</td>
</tr>
<tr>
<td>initValue</td>
<td>초기값, 마지막으로 생성된 값이 기준입니다.</td>
<td>0</td>
</tr>
<tr>
<td>allocationSize</td>
<td>시퀀스 한 번 호출에 증가하는 수</td>
<td>50</td>
</tr>
</tbody></table>
<br>



<h2 id="5-5-auto-전략">5-5. AUTO 전략</h2>
<p>AUTO 전략이 기본값입니다. 데이터베이스 방언에 맞춰서 자동으로 생성되는 전략입니다. 위의 3개 중에 선택되어 사용됩니다.</p>
<p>Oracle 의 경우에는 <strong>SEQUENCE</strong> 전략을 사용하는데 DB 가 자동으로 숫자값을 생성해줍니다.</p>
<br>


<h2 id="5-6-allocationsize">5-6. allocationSize</h2>
<pre><code class="language-sql">========= persist member1
Hibernate: 
    select
        next value for MEMBER_SEQ

========= persist member2
Hibernate: 
    select
        next value for MEMBER_SEQ

========= transaction commit
Hibernate: 
    insert into Member (name, id) values (?, ?)
Hibernate: 
    insert into Member (name, id) values (?, ?)</code></pre>
<p>SEQUENCE 를 보면 위와 같은 로그가 있습니다. SEQUENCE 전략은 <code>persist()</code> 를 호출하면 데이터베이스 시퀀스를 조회해서 영속성 컨텍스트에 저장합니다. </p>
<p>여기서 데이터베이스 <strong>시퀀스를 조회하기 위해서 데이터베이스와 통신</strong>을 해야하고, <strong>트랜잭션이 커밋될 때도 데이터베이스와 통신</strong>을 해야 하기 때문에 <strong>2번의 통신</strong>이 이루어져야 합니다.
이러한 것을 방지하고 성능을 높이기 위해 JPA 는 <strong>allocationSize</strong> 라는 속성을 제공합니다.</p>
<p>현재 시퀀스가 1이고, allocationSize 가 50이면 시퀀스를 조회할 때 DB의 시퀀스를 51로 높이게 됩니다. 그리고 메모리에서 1씩 사용하게 되면서 데이터베이스에 시퀀스 조회 쿼리가 날라가지 않게 됩니다.</p>
<p><strong>시퀀스를 모두 소진하기 전에 어플리케이션이 종료될 경우, 나머지 시퀀스는 사라지며 다시 사용할 수 없는 상태</strong>가 됩니다. 만약 1-50번까지의 시퀀스 중 10번까지 사용하고 11-50번은 사용하지 않은 상태로 어플리케이션을 종료하고 다시 시작하면, 51번부터 시작하게 됩니다.</p>
]]></description>
        </item>
    </channel>
</rss>