<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>i-no.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 15 Jul 2024 02:59:42 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>i-no.log</title>
            <url>https://images.velog.io/images/i-no/profile/1b2c47a7-a21b-4627-8ac1-6942cc865ab9/social.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. i-no.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/i-no" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[코드 스타일 적용 자동화]]></title>
            <link>https://velog.io/@i-no/%EC%BD%94%EB%93%9C-%EC%8A%A4%ED%83%80%EC%9D%BC-%EC%A0%81%EC%9A%A9-%EC%9E%90%EB%8F%99%ED%99%94</link>
            <guid>https://velog.io/@i-no/%EC%BD%94%EB%93%9C-%EC%8A%A4%ED%83%80%EC%9D%BC-%EC%A0%81%EC%9A%A9-%EC%9E%90%EB%8F%99%ED%99%94</guid>
            <pubDate>Mon, 15 Jul 2024 02:59:42 GMT</pubDate>
            <description><![CDATA[<p>인천의 한 일요일, 지인과 함께 인증 서비스 개발을 하기로 하였다.
프로젝트 세팅 중에 코드 스타일 적용 방식에 대하여 이야기 하였다.</p>
<h3 id="pre-commit">pre-commit</h3>
<p>지인분의 회사에서는 pre-commit을 활용하여 commit 이전에 코드 스타일이 자동으로 적용되도록 하고 있다고 한다.
이를 활용하면 commit시에 pre-commit git hook을 통해서 코드 스타일에 위배되는 코드를 자동으로 수정해준다.
결국 commit에 코드 스타일에 위배되는 내용이 들어가는 것을 방지할 수 있다.</p>
<p>이번 프로젝트에 pre-commit hook을 사용하여 코드 스타일 적용을 자동화 하기로 하였다.</p>
<h3 id="java-style-checker">Java style checker</h3>
<p>Java 의 코딩 스타일 적용에 사용할 플러그인을 조사하였다.
pre-commit git hook에 등록하여 사용하는 것은 주로 2가지로 보인다.</p>
<ul>
<li><a href="https://github.com/checkstyle/checkstyle">CheckStyle</a></li>
<li><a href="https://github.com/diffplug/spotless">Spotless</a></li>
</ul>
<p>CheckStyle은 코드 스타일 체크는 가능하지만 코드 스타일 적용 기능은 제공하지 않는다.
반면 Spotless 는 코드 스타일을 체크하고 적용 기능까지 제공한다.</p>
<p>따라서 Spotless 를 사용하기로 하였다.</p>
<h3 id="플러그인-추가">플러그인 추가</h3>
<pre><code>plugins {
    id &quot;com.diffplug.spotless&quot; version &quot;6.25.0&quot;
}

spotless {
    java {
        googleJavaFormat(&#39;1.21.0&#39;)
    }
}</code></pre><p>build.gradle 에서 spotless 플러그인을 추가해주고
사용할 스타일과 버전을 적용해준다.</p>
<h3 id="git-hook-설정">git hook 설정</h3>
<pre><code>#!/bin/sh

# Apply google style using Spotless
./gradlew spotlessApply

# Format staged Java files using google-java-format
java -jar tools/google-java-format-1.21.0-all-deps.jar -i $(git diff --cached --name-only --diff-filter=ACM -- &#39;*.java&#39; | xargs)

# Add formatted files back to the index
git add $(git diff --cached --name-only --diff-filter=ACM -- &#39;*.java&#39; | xargs)
</code></pre><p>.git/hooks/pre-commit 파일을 생성하고 실행할 내용을 설정해준다.</p>
<p>여기서는 google-java-format-1.21.0-all-deps.jar 파일을 실행시켜서 사용되지 않은 import 까지도 제거하도록 해주었다.</p>
<h3 id="git-hook-설정-스크립트-작성">git hook 설정 스크립트 작성</h3>
<pre><code>#!/bin/sh

# Copy the pre-commit sample to the .git/hooks directory
cp hooks/pre-commit.sample .git/hooks/pre-commit

# Make the pre-commit hook executable
chmod +x .git/hooks/pre-commit
</code></pre><p>.git 하위의 파일들은 git에 올라가지 않는다.</p>
<p>해당 프로젝트를 사용하는 인원 모두가 git hook을 적용할 수 있도록 git hook을 적용하는 스크립트를 추가해주었다.</p>
<h3 id="readme에-git-hook-설정-스크립트-실행-내용-추가">readme에 git hook 설정 스크립트 실행 내용 추가</h3>
<p>마지막으로 readme 에 git hook 설정 스크립트를 실행해야한다는 내용을 추가하여 모두가 동일한 코드 스타일을 공유할 수 있도록 하였다.</p>
<p><a href="https://github.com/ttokcheong/debate_summary/pull/3">https://github.com/ttokcheong/debate_summary/pull/3</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로덕션 서비스에서 무중단 테이블 분리]]></title>
            <link>https://velog.io/@i-no/%ED%94%84%EB%A1%9C%EB%8D%95%EC%85%98-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%97%90%EC%84%9C-%EB%AC%B4%EC%A4%91%EB%8B%A8-%ED%85%8C%EC%9D%B4%EB%B8%94-%EB%B6%84%EB%A6%AC</link>
            <guid>https://velog.io/@i-no/%ED%94%84%EB%A1%9C%EB%8D%95%EC%85%98-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%97%90%EC%84%9C-%EB%AC%B4%EC%A4%91%EB%8B%A8-%ED%85%8C%EC%9D%B4%EB%B8%94-%EB%B6%84%EB%A6%AC</guid>
            <pubDate>Tue, 25 Jun 2024 09:01:54 GMT</pubDate>
            <description><![CDATA[<p>서울의 한 월요일.
장바구니 테이블을 분리하는 작업을 담당하게 되었습니다.</p>
<h2 id="장바구니-테이블">장바구니 테이블</h2>
<p><code>장바구니 테이블</code>은 고객이 주문할 상품을 담아두는 테이블입니다.
<code>장바구니 테이블</code>에는 <code>샘플 상품</code>과 <code>일반 상품</code>이 함께 담겨있습니다.
<code>샘플 상품</code>에 대한 정책 변경으로 <code>장바구니 테이블</code>에서 <code>샘플 상품</code>과 <code>일반 상품</code>을 분리하는 작업을 진행하게 되었습니다.</p>
<h2 id="테이블-분리-작업">테이블 분리 작업</h2>
<ul>
<li><code>샘플 장바구니 테이블</code>을 생성</li>
<li><code>장바구니 테이블</code>의 <code>샘플 상품 데이터</code>를 <code>샘플 장바구니 테이블</code>로 이동</li>
<li><code>장바구니 서비스 로직</code>을 <code>장바구니 로직</code>과 <code>샘플 장바구니 로직</code>으로 분리</li>
</ul>
<p>크게 보면 위 3가지 작업이 필요합니다.
서비스의 중단 없이 위 작업을 진행하려면 작업을 좀 더 작게 쪼개어 진행해야 합니다.</p>
<h2 id="무중단-마이그레이션">무중단 마이그레이션</h2>
<p>변경된 로직을 배포하면 즉시 변경된 로직으로 동작합니다.
테이블의 데이터를 다른 테이블로 이동시키는 작업은 시간이 소요되는 작업입니다.</p>
<p>따라서 <code>샘플 장바구니 테이블</code>을 사용하는 <code>샘플 장바구니 로직</code>이 적용되었지만, <code>샘플 장바구니 테이블</code>의 데이터는 없는 상황이 발생 가능합니다.</p>
<p>고객이 비어있는 장바구니를 보고 당황하는 일이 발생하지 않도록 주의하여 배포해야 합니다.</p>
<h2 id="배포-시나리오">배포 시나리오</h2>
<ol>
<li><code>샘플 장바구니 테이블</code> 생성</li>
<li><code>샘플 상품 데이터</code>에 <code>삽입 삭제 업데이트</code> 시 <code>장바구니 테이블</code>과 <code>샘플 장바구니 테이블</code> 모두 <code>삽입 삭제 업데이트</code> 하는 로직 코드 배포</li>
<li>로직 배포 이전에 <code>장바구니 테이블</code>에 있는 <code>샘플 상품 데이터</code>를 <code>샘플 장바구니 테이블</code>에 복제하는 스크립트 실행</li>
<li><code>장바구니 로직</code>과 <code>샘플 장바구니 로직</code> 분리 코드 배포</li>
<li><code>장바구니 테이블</code>에 있는 <code>샘플 상품 데이터</code> 삭제 스크립트 실행</li>
</ol>
<p>1, 2번 작업을 통해서 새로운 데이터가 새로운 테이블에도 저장되도록 하였습니다.
이후 3번 작업으로 기존 데이터가 새로운 테이블에 저장되도록 하여 데이터 이전 작업을 마쳤습니다.</p>
<h2 id="결과">결과</h2>
<p>간단한 데이터 마이그레이션 작업은 여러 번 해보았지만, 테이블을 분리하는 규모의 작업은 처음이라 조심스러웠습니다.
배포 시나리오를 먼저 작성하고 팀원들에게 공유하여 예상되는 문제가 있을지 확인하고, staging 서버에서 QA를 통해 확인하여 진행하였습니다.</p>
<p>그 결과 서비스 중단 없이 안정적으로 테이블 분리 작업을 완료했습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[모호한 컬럼명 리팩토링에서 느낀 테스트 코드의 중요성]]></title>
            <link>https://velog.io/@i-no/%EB%AA%A8%ED%98%B8%ED%95%9C-%EC%BB%AC%EB%9F%BC%EB%AA%85-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%EC%97%90%EC%84%9C-%EB%8A%90%EB%82%80-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1</link>
            <guid>https://velog.io/@i-no/%EB%AA%A8%ED%98%B8%ED%95%9C-%EC%BB%AC%EB%9F%BC%EB%AA%85-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%EC%97%90%EC%84%9C-%EB%8A%90%EB%82%80-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1</guid>
            <pubDate>Tue, 25 Jun 2024 05:38:11 GMT</pubDate>
            <description><![CDATA[<p>서울의 한 월요일.
내가 작성한 코드가 예상과 다르게 동작했습니다.
모델 인스턴스가 리턴될 것으로 예상했는데 string 이 리턴되었습니다.</p>
<h2 id="문제-발견">문제 발견</h2>
<pre><code class="language-ruby">customer.country</code></pre>
<p>customer와 country는 연관관계가 있는 모델입니다.
customer에 countryId 컬럼이 있고, 그래서 위 코드는 country 모델 인스턴스를 리턴합니다.</p>
<pre><code class="language-ruby">shipment.country</code></pre>
<p>shipment는 모델 인스턴스입니다.
shipment에는 country라는 컬럼이 있고, 이 컬럼에는 KR, JP 등 국가 코드를 저장하고 있습니다.
그래서 위 코드는 string 타입의 국가 코드를 리턴합니다.</p>
<pre><code class="language-ruby">modelInstance.country</code></pre>
<p>8개의 테이블에 country라는 이름의 컬럼이 있고, 국가 코드를 저장하고 있습니다.
또 다수의 테이블이 countryId 컬럼이 있고 country 테이블과 연관관계를 가지고 있습니다.</p>
<p>따라서 위의 코드처럼 모델 인스턴스의 country를 호출할 때, 모델링을 확인하지 않으면 어떤 값이 리턴될지 알 수 없습니다.
<code>.country</code>를 사용할 때마다 모델링을 확인해야 하는 리소스가 추가로 필요해집니다.</p>
<h2 id="원인">원인</h2>
<p>테이블명과 컬럼명이 일치해서 모호함이 생기는 것이 원인입니다.</p>
<p>이 문제를 깃이슈에 등록하고, 주간 백엔드 회의에서 언급하여 country 컬럼명을 countryCode로 명확하게 변경하여 해결하는 것으로 결론 내렸습니다.</p>
<h2 id="리팩토링-누락-문제">리팩토링 누락 문제</h2>
<p>테이블의 컬럼명을 변경하고,
해당 컬럼이 사용되는 곳을 모두 찾아서 변경된 컬럼명을 사용하도록 수정해 줘야 합니다.
뿐만 아니라 해당 컬럼값을 받아서 처리하는 곳도 변수명을 countryCode로 명확하게 변경해 줘야 합니다.</p>
<p>일부 수정이 누락된 곳이 있다면 버그가 발생될 것입니다.
해당 컬럼은 매우 다양한 곳에서 당장은 버그가 안 보이더라도, 나중에 어딘가에서 발견될 가능성이 컸습니다.
따라서 누락된 곳이 없다는 것을 보장해 줄 방법이 필요했습니다.</p>
<h2 id="테스트-코드-사용">테스트 코드 사용</h2>
<p>테스트 코드를 작성하여 모두 통과하는 것을 확인하여 수정이 누락된 부분이 없음을 보장할 수 있습니다.</p>
<ol>
<li>리팩토링 이전에 테스트 코드를 확인하여 기존에 잘 동작하는 것을 확인</li>
<li>리팩토링 이후 테스트 코드 실행</li>
<li>테스트 코드가 실패하는 부분을 확인하여 누락된 부분 수정</li>
<li>테스트 코드가 모두 통과하는 것을 확인하여 신뢰성 있는 코드 배포!</li>
</ol>
<p>위의 순서로 리팩토링을 진행하기로 결정했습니다.</p>
<h2 id="테스트-코드-개선">테스트 코드 개선</h2>
<p>기존에 작성되어 있는 테스트 케이스는 2314개였습니다.
그런데 일부 유지 보수가 누락되어 620개의 케이스가 실패하고 있었습니다.
일단 이 코드를 먼저 개선하여 테스트 코드의 신뢰도를 높여주었습니다.</p>
<p>테스트 코드가 잘 통과하는 것을 확인하고 리팩토링을 진행하였고, 테스트 코드를 다시 실행시켰습니다.
리팩토링을 매우 꼼꼼하게 진행을 했다고 생각했지만, 테스트 코드 실행 결과 누락된 부분이 발견되었습니다.
테스트 코드가 실패한 부분을 확인하여 누락된 부분을 수정하는 작업을 테스트 코드가 모두 통과할 때까지 반복하였습니다.</p>
<h2 id="결과">결과</h2>
<p>기존의 테스트 코드의 실패하는 케이스를 개선하여 테스트 코드를 사용하여 코드의 신뢰성을 검증할 수 있도록 했습니다.</p>
<p>이 테스트 코드를 사용해서 리팩토링이 누락된 부분을 발견하고 신뢰성 있는 코드를 배포할 수 있었습니다.</p>
<h2 id="소감">소감</h2>
<p>테스트 코드를 실행하기 전에는 리팩토링 누락에 대한 불안감이 매우 컸습니다.</p>
<p>리팩토링 이후에 테스트 코드를 통해서 리팩토링이 누락된 부분을 찾았을 때 정말 짜릿했습니다.</p>
<p>테스트 코드가 유지 보수에 중요하다고 익히 들어 알고 있었지만, 이렇게 직접 경험해 보니 진짜 너무 중요하다는 것을 제대로 이해할 수 있었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis가 RDBMS보다 빠른 이유]]></title>
            <link>https://velog.io/@i-no/%EB%A9%B4%EC%A0%91-%ED%95%99%EC%8A%B5%EB%B2%95-Redis%EA%B0%80-RDBMS%EB%B3%B4%EB%8B%A4-%EB%B9%A0%EB%A5%B8-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@i-no/%EB%A9%B4%EC%A0%91-%ED%95%99%EC%8A%B5%EB%B2%95-Redis%EA%B0%80-RDBMS%EB%B3%B4%EB%8B%A4-%EB%B9%A0%EB%A5%B8-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Tue, 18 Jun 2024 11:56:32 GMT</pubDate>
            <description><![CDATA[<p>서울의 한 수요일.
Redis가 RDBMS보다 빠른 이유에 대하여 질문을 받았습니다.
이에 대하여 면접 과정 혹은 그 이후에 학습한 내용을 정리해 보겠습니다.</p>
<h3 id="첫-번째-이유">첫 번째 이유</h3>
<p>Redis는 메모리에 데이터를 저장하기 때문에 디스크에 저장하는 RDBMS보다 빠르다.</p>
<p>이는 구글에 Redis가 빠른 이유를 검색하면 바로 나오는 내용입니다.
맞는 말입니다.
메모리에 데이터를 저장하면 디스크에 저장하는 것보다 빠를 수밖에 없죠.
저도 이전에 구글에 Redis가 빠른 이유를 검색하고, 위 내용을 보고 바로 납득하고 다른 이유를 크게 고민하지 않았습니다.</p>
<p>그러나 요즘은 RDBMS에서 버퍼 캐시, 쿼리 캐시 등에 메모리를 활용하는 경우가 많아졌습니다.
심지어 메모리 기반의 RDBMS도 존재합니다.
그럼 Redis가 RDBMS보다 빠른 이유는 다른 이유가 더 필요합니다.</p>
<h3 id="두-번째-이유">두 번째 이유</h3>
<p>Redis는 index를 생성하지 않기 때문에 RDB보다 insert, update, delete에서 빠르다.</p>
<p>머리를 열심히 굴려 떠올린 이유는 위의 내용이었습니다.
테이블에 설정된 index가 여러 개라면 read를 제외한 연산에 index에 대한 처리에 추가시간이 소요되기 때문입니다.
그렇다고 해서 index를 설정하지 않으면 read 시에 풀 스캔이 필요하여 느려지게 됩니다.</p>
<h3 id="세-번째-이유">세 번째 이유</h3>
<p>Redis는 싱글 스레드 기반이기 때문에 lock을 사용하지 않아서 경합이 없고, 컨텍스트 스위칭 비용이 들지 않아서 빠르다.</p>
<p>Redis 공식 문서에서 당당하게 자랑하고 있는 내용입니다.
멀티 스레드는 공유 자원에 동시에 접근하게 되면 문제가 발생할 수 있어 lock을 사용하여 동시 접근을 제한합니다.
그럼 lock 경합이 발생하여 성능이 저하됩니다.
또 컨텍스트 스위칭의 오버헤드로 CPU 캐시 효율성이 저하되어 성능이 저하됩니다.</p>
<h3 id="네-번째-이유">네 번째 이유</h3>
<p>RDBMS는 Repeatable Read 격리 수준에서 version 을 관리해야 하기 때문에 Redis보다 느립니다.</p>
<p>RDBMS는 Repeatable Read 이상의 격리 수준에서 각 트랜잭션이 독립적인 스냅샷을 볼 수 있도록 하여 데이터 일관성과 동시성을 보장해 줍니다.
대신 여러 version에 대한 관리가 필요하여 성능 저하가 발생합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[대용량 데이터 다운로드 기능 개선]]></title>
            <link>https://velog.io/@i-no/%EC%84%B1%EA%B3%B5%EC%9D%98-%EC%96%B4%EB%A8%B8%EB%8B%88-%EB%8C%80%EC%9A%A9%EB%9F%89-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-%EA%B8%B0%EB%8A%A5-%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@i-no/%EC%84%B1%EA%B3%B5%EC%9D%98-%EC%96%B4%EB%A8%B8%EB%8B%88-%EB%8C%80%EC%9A%A9%EB%9F%89-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-%EA%B8%B0%EB%8A%A5-%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Fri, 14 Jun 2024 12:18:01 GMT</pubDate>
            <description><![CDATA[<p>대용량 다운로드 기능 개선 작업에서 생각했던 해결 방법과 선택하지 않은 이유 정리</p>
<h3 id="문제">문제</h3>
<p>RDB에 저장된 데이터를 가공하여 CSV 형식으로 다운로드 받는 기능에서 Timeout이 발생
어드민 서버에서 사용하는 기능입니다.
사용 빈도가 매우 낮습니다.</p>
<hr>
<h3 id="해결-방법-1--timeout-늘리기">해결 방법 1 : Timeout 늘리기</h3>
<p>Timeout 설정 시간을 늘려서 당장 Timeout이 발생하지 않도록 하기</p>
<p><strong>장점</strong></p>
<ul>
<li>초간단하게 해결</li>
</ul>
<p><strong>기각 사유</strong></p>
<ul>
<li>시간이 지나 RDB에 데이터가 더 많아지면 다시 Timeout 발생 가능</li>
</ul>
<hr>
<h3 id="해결-방법-2--비동기-처리한-뒤-결과는-메일로-전송">해결 방법 2 : 비동기 처리한 뒤 결과는 메일로 전송</h3>
<p>비동기로 CSV 형식으로 파일을 생성한 뒤, 유저에게 메일에 첨부하여 결과 파일 전송하기</p>
<p><strong>장점</strong></p>
<ul>
<li>시간이 아무리 오래 걸려도 Timeout이 발생하지 않음</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>메일 첨부파일 용량 제한이 있음</li>
<li>보통 20~25MB</li>
<li>현재 파일 크기 10MB</li>
</ul>
<p><strong>기각 사유</strong></p>
<ul>
<li>시간이 지나 RDB에 데이터가 더 많아지면 용량 초과 가능성 있음</li>
</ul>
<hr>
<h3 id="해결-방법-3--비동기-처리한-뒤-s3에-저장하고-다운로드-페이지-제공">해결 방법 3 : 비동기 처리한 뒤 S3에 저장하고 다운로드 페이지 제공</h3>
<p>비동기로 CSV 형식으로 파일을 생성한 뒤, S3에 저장하고, 다운로드 페이지 추가하여 다운로드 가능하도록 구현, 처리 완료시 슬랙 알림 제공</p>
<p><strong>장점 == 채택 사유</strong></p>
<ul>
<li>시간이 아무리 오래 걸려도 Timeout이 발생하지 않음</li>
<li>파일 용량이 25MB를 초과해도 동작 가능함</li>
<li>다운로드 페이지 링크를 사용하여 결과 파일 쉽게 공유 가능</li>
<li>슬랙 알림을 통해 처리 완료 이후 즉시 알림</li>
</ul>
<hr>
<h3 id="3번-방법으로-구현-도중에-마주친-메모리-이슈">3번 방법으로 구현 도중에 마주친 메모리 이슈</h3>
<p><a href="https://velog.io/@i-no/Ruby-on-Rails-Memory-Bloat">https://velog.io/@i-no/Ruby-on-Rails-Memory-Bloat</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Blue Green 무중단 배포] docker + nginx + git actions 으로 간단 구현]]></title>
            <link>https://velog.io/@i-no/Blue-Green-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC-docker-nginx-git-action-%EC%9C%BC%EB%A1%9C-%EA%B0%84%EB%8B%A8-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@i-no/Blue-Green-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC-docker-nginx-git-action-%EC%9C%BC%EB%A1%9C-%EA%B0%84%EB%8B%A8-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Wed, 05 Jun 2024 02:30:29 GMT</pubDate>
            <description><![CDATA[<p>인천의 한 수요일.
사이드 프로젝트에서 배포할 때마다 20초 정도 접속 불가한 것에 대해 찝찝함이 느껴졌다.
센서에서 쏜 정보를 저장하는 서버인데, 하필 배포 타이밍에 중요한 정보를 전송할까봐 무중단 배포를 적용하기로 했다.</p>
<h2 id="blue-green-deployment">Blue Green Deployment</h2>
<p>기존에 떠있는 서버를 유지하고, 새로운 서버를 동시에 띄운 뒤에
새로운 서버가 정상적으로 뜬다면 새로운 서버를 사용해서 서비스하고
기존에 떠있던 서버는 내리는 방식</p>
<p>기존에 떠있는 서버가 Blue
새로운 서버가 Green
Green이 정상적으로 동작하는 것이 확인되면 Blue로 승격시키는 방식</p>
<h3 id="장점">장점</h3>
<p>다운타임 최소화</p>
<ul>
<li>새로운 서버가 준비된 뒤에 트래픽을 블루에서 그린으로 전환하기 때문에 다운타임이 거의 없다.</li>
</ul>
<p>빠른 롤백</p>
<ul>
<li>문제 발생 시, 트래픽을 다시 블루 환경으로 전환하여 즉시 이전 버전으로 돌아갈 수 있다.</li>
</ul>
<p>단순함</p>
<ul>
<li>배포 프로세스가 단순하여 오류 발생 가능성이 낮다.</li>
</ul>
<h3 id="단점">단점</h3>
<p>비용 증가</p>
<ul>
<li>동시에 blue, green 두 개의 서버가 떠야 하기 때문에 일시적으로 두 배의 리소스를 필요로 한다.</li>
</ul>
<h3 id="구현-방법">구현 방법</h3>
<p>다양한 방법이 있지만 최대한 간단하게 구현해 보는 것이 목적이다.
기존 CI/CD 로직을 조금 변경하여 간단하게 구현해 본다.
전체적인 프로세스는 아래와 같다.</p>
<ol>
<li>Docker image 를 빌드하여 DockerHub에 업로드</li>
<li>ssh-action 으로 서버에 Docker image pull &amp; up</li>
<li>Curl 로 서버 동작 확인 이후</li>
<li>nginx conf 파일에서 port 번호를 green port로 수정 &amp; restart</li>
<li>Blue Docker down</li>
<li>Green 을 Blue 로 변경</li>
</ol>
<h3 id="docker-image-를-빌드하여-dockerhub에-업로드">Docker image 를 빌드하여 DockerHub에 업로드</h3>
<pre><code>name: Backend CD
on:
  push:
    branches: [ master ]

jobs:
  deploy:
    runs-on: ubuntu-22.04
    steps:
      - name: 저장소 Checkout
        uses: actions/checkout@v3

      - name: 도커 이미지 빌드 # (2)
        run: docker build -t ${{ secrets.DOCKER_IMAGE }} .

      - name: Docker Hub 로그인 # (3)
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Docker Hub 퍼블리시 # (4)
        run: docker push ${{ secrets.DOCKER_IMAGE }}</code></pre><p>git workflows 으로 작성한 내용이다.
master branch에 머지 되면 실행되는 jobs</p>
<p>Repository secrets 으로 등록한 도커 이미지 파일 이름과 아이디 비밀번호를 사용하여 Docker Hub에 빌드한 이미지를 올린다.</p>
<h3 id="ssh-action-으로-서버에-docker-image-pull--up">ssh-action 으로 서버에 Docker image pull &amp; up</h3>
<pre><code>      - name: Set Dynamic Project Name
        run: echo &quot;PROJECT_NAME=deploy-${{ github.run_id }}&quot; &gt;&gt; $GITHUB_ENV

      - name: Deploy to EC2
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            docker pull ${{ secrets.DOCKER_IMAGE }}:latest 
            docker compose -p ${{ env.PROJECT_NAME }} -f ~/docker-compose.green.yml up -d</code></pre><p>github.run_id 는 git actions 가 실행되는 run_id 로 실행 시마다 변경된다.
이를 Docker project name으로 사용하여 Blue와 Green 을 구분한다.</p>
<p>아까 Dokcer Hub에 올린 이미지를 pull &amp; up 해주고</p>
<h3 id="curl-로-서버-동작-확인-이후">Curl 로 서버 동작 확인 이후</h3>
<pre><code>      - name: Test Green Environment
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            ~/test-green.sh</code></pre><p>서버 내부에 있는 테스트 스크립트를 실행 시킨다.</p>
<pre><code>#!/bin/bash

for i in {1..10}; do
  curl -f http://localhost:8081 &amp;&amp; break || sleep 10;
done</code></pre><p>위 내용의 스크립트이다.
10초에 한 번씩 최대 10번을 curl 를 통해 Green 서버가 준비되었는지 확인한다.</p>
<h3 id="nginx-conf-파일에서-port-번호를-green-port로-수정--restart">nginx conf 파일에서 port 번호를 green port로 수정 &amp; restart</h3>
<pre><code>      - name: Switch Traffic to Green Environment
        if: ${{ success() }}
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            sudo cp ~/nginx.green.conf /etc/nginx/sites-enabled/default
            sudo service nginx restart  </code></pre><p>테스트에 성공하고 나면 트래픽을 Green으로 이동시켜준다.
준비해둔 Green conf 파일을 nginx 설정 파일로 사용하여 nginx 를 재실행한다.</p>
<h3 id="blue-docker-down">Blue Docker down</h3>
<pre><code>      - name: Cleanup Old Blue Environment
        if: ${{ success() }}
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            ~/cleanup-blue.sh
            docker image prune -a -f 

      - name: Set cleanup-blue file
        if: ${{ success() }}
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            echo &quot;docker compose -p ${{ env.PROJECT_NAME }} -f ~/docker-compose.blue.yml down&quot; &gt; ~/cleanup-blue.sh</code></pre><p>트래픽을 Green으로 이동시킨 뒤에 Blue를 Down 시키는 스크립트를 실행한다.
이후 사용하지 않는 이미지를 제거하여 저장 공간을 확보한다.</p>
<pre><code>docker compose -p deploy-9363951795 -f ~/docker-compose.blue.yml down</code></pre><p>이런 식의 간단한 스크립트이다.
여기서 -p 옵션의 파라미터 deploy-9363951795 는 매번 변경된다.
Blue를 down 시킨 뒤에 이 파라미터는 해당 Green의 project name으로 변경되어 이제 Blue가 된 Green을 종료시키는 스크립트로 변경된다.</p>
<h3 id="green-을-blue-로-변경">Green 을 Blue 로 변경</h3>
<pre><code>      - name: Prepare New Blue Environment
        if: ${{ success() }}
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            cp ~/docker-compose.green.yml ~/docker-compose.new-blue.yml
            mv ~/docker-compose.blue.yml ~/docker-compose.green.yml
            mv ~/docker-compose.new-blue.yml ~/docker-compose.blue.yml
            cp ~/nginx.green.conf ~/nginx.new-blue.conf
            mv ~/nginx.blue.conf ~/nginx.green.conf
            mv ~/nginx.new-blue.conf ~/nginx.blue.conf
            cp ~/test-green.sh ~/test-new-blue.sh
            mv ~/test-blue.sh ~/test-green.sh
            mv ~/test-new-blue.sh ~/test-blue.sh</code></pre><p>현재 Green을 띄우기 위해 사용했던 compose 파일이나 설정 파일, 스크립트 파일들을 Blue 파일과 스왑 해준다.
이렇게 Green가 Blue로 된다.
이렇게 파일을 스왑 해주는 이유는 Blue와 Green은 다른 포트 번호로 실행되는데, Blue 와 Green 에 대하여 고정된 포트 번호를 사용한다면
Green을 Blue로 변경을 위해서 실행 중인 서버의 포트 번호를 변경시켜줘야 한다.
이는 매우 복잡해보여 Blue와 Green의 포트 번호를 스왑해주고 실행 중인 서버의 포트 번호는 그대로 두어 논리적으로 Blue로 변경해 주는 것이다.</p>
<h2 id="후기">후기</h2>
<p>기존 CI/CD 로직에서 간단하게 Blue Green Deployment를 적용했다는 점에서 만족한다.
설정 파일을 스왑 하는 방식으로 Green 을 Blue로 변경시키는 것이 조금 불안해 보이긴 한다.
그래도 간단한 무중단 배포 구현에서는 나쁘지 않아 보인다.</p>
<p>유저들이 많이 사용하는 중요한 서비스라면 ArgoCD, Jenkins 등을 사용하여 구현하는 것도 좋지만
개인 사이드 프로젝트에서는 이렇게 간단하게 구현하는 것도 괜찮을지도?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Ruby on Rails] Memory Bloat]]></title>
            <link>https://velog.io/@i-no/Ruby-on-Rails-Memory-Bloat</link>
            <guid>https://velog.io/@i-no/Ruby-on-Rails-Memory-Bloat</guid>
            <pubDate>Thu, 23 May 2024 01:57:54 GMT</pubDate>
            <description><![CDATA[<p>서울의 한 월요일.
내가 개발한 기능이 비동기 서버를 다운시켰다. (다행히 development 서버에서...)
이를 해결하는 과정에서 공부한 Ruby 에서 발생하는 memory bloat 와 해결 방법에 대해 공유해 봅니다.
누군가는 또 비슷한 일을 겪고 이 글을 본다면 도움이 되길 바라며...</p>
<h2 id="memory-bloat">Memory Bloat</h2>
<blockquote>
<p>Memory bloat refers to a situation where a program consumes more memory than is necessary to execute its intended tasks. It typically occurs due to poor memory optimization, excessive data caching, or the accumulation of redundant objects. As a result, the program’s memory usage increases, leading to performance degradation and potential slowdowns. Memory bloat is usually a gradual and incremental process, and its effects may become noticeable over time.</p>
</blockquote>
<p>메모리 팽창은 프로그램이 의도한 작업을 실행하는 데 필요한 것보다 더 많은 메모리를 사용하는 상황을 말합니다.
일반적으로 메모리 최적화 불량, 과도한 데이터 캐싱 또는 중복 객체의 축적으로 인해 발생합니다.
결과적으로 프로그램의 메모리 사용량이 증가하여 성능 저하 및 잠재적인 속도 저하로 이어집니다.
메모리 팽창은 일반적으로 점진적이고 점진적으로 진행되며 시간이 지남에 따라 그 영향이 눈에 띄게 나타날 수 있습니다.</p>
<h2 id="ruby-에서-메모리-할당-방법">Ruby 에서 메모리 할당 방법</h2>
<p>ruby 는 malloc arena 를 사용하여 쓰레드에 메모리를 할당해 줍니다.
malloc arena는 여러 개가 될 수 있는데요.</p>
<p>메모리를 할당해 주는 malloc arena가 많으면</p>
<ul>
<li>쓰레드가 동시에 메모리 할당 요청을 해도 여러 malloc arena가 빠르게 할당해 줄 수 있습니다.</li>
<li>대신 malloc arena는 각각 독립적인 메모리 공간을 관리하기 때문에 전체적인 메모리 사용량이 많아질 수 있습니다.</li>
</ul>
<h2 id="ruby에서-memory-bloat-발생-과정">Ruby에서 Memory Bloat 발생 과정</h2>
<ol>
<li>한 malloc arena가 쓰레드에 메모리를 할당해 줍니다.</li>
<li>한 malloc arena가 관리하는 메모리를 모두 할당해 줘서 메모리가 부족합니다.</li>
<li>한 malloc arena는 os로부터 메모리를 더 받아서 쓰레드에 메모리를 할당해 줍니다.</li>
<li>쓰레드의 작업이 끝나 malloc arena로 메모리를 반환했지만, malloc arena는 os에 메모리를 반환하지 않습니다.</li>
<li>다른 쓰레드가 다른 malloc arena에 메모리 할당 요청을 합니다.</li>
<li>다른 malloc arena가 관리하는 메모리를 모두 할당해 줘서 메모리가 부족합니다.</li>
<li>다른 malloc arena는 os로부터 메모리를 더 받아서 쓰레드에 메모리를 할당해 줍니다.</li>
<li>쓰레드의 작업이 끝나 malloc arena로 메모리를 반환했지만, malloc arena는 os에 메모리를 반환하지 않습니다.</li>
<li>또 다른 쓰레드가 또 다른 malloc arena에 메모리 할당 요청을...</li>
<li>각각의 malloc arena가 관리하는 메모리가 많아지면서 서버의 전체적인 메모리 사용량이 늘어납니다.</li>
<li>제 경우에는 쿠버네티스에서 설정한 서버 메모리 limit 을 초과하게 되어 out of memory killed 로 서버가 다운되었습니다.(정확히는 재실행됨)</li>
</ol>
<h2 id="malloc-arena">Malloc Arena</h2>
<p>malloc arena는 os로부터 가져온 메모리를 웬만하면 os로 반환하지 않습니다.
대신에 잘 가지고 있다가 쓰레드로부터 할당 요청이 오면 할당해 주죠</p>
<p>쓰레드가 malloc arena로 메모리를 반환할 때마다
malloc arena도 os로 메모리를 반환한다면 그때마다 system call 을 사용하는 비싼 비용이 발생하게 됩니다.
이후 또 쓰레드가 메모리 할당 요청을 하고 메모리가 부족하다면 다시 os로부터 할당받아야 하는 문제가 발생하기도 하겠죠?
이 때문에 os로 메모리를 잘 반환하지 않습니다.</p>
<h2 id="해결-방법">해결 방법</h2>
<ol>
<li>malloc arena max 값 줄이기</li>
<li>jemalloc 사용</li>
<li>쓰레드가 메모리 적당히 쓰게하기...</li>
</ol>
<h2 id="malloc-arena-max">Malloc Arena Max</h2>
<p>Ruby 에는 Malloc Arena Max 값을 설정할 수 있습니다.
Malloc Arena의 수가 적다면 독립적으로 메모리를 관리하는 수가 줄어서 전체적인 메모리 사용량을 줄 수 있습니다.
그러나 멀티쓰레드들이 메모리 할당을 위해 경합이 발생하여 처리 시간이 늘어날 수 있다는 trade off 가 있습니다.</p>
<p><a href="https://www.speedshop.co/2017/12/04/malloc-doubles-ruby-memory.html">https://www.speedshop.co/2017/12/04/malloc-doubles-ruby-memory.html</a></p>
<h2 id="jemalloc-사용">Jemalloc 사용</h2>
<p>Ruby의 기본 malloc 구현체는 glibc의 ptmalloc 입니다.
ptmalloc 은 메모리를 os에 잘 반환하지 않습니다.</p>
<p>다른 malloc 구현체로 jemalloc 가 있습니다.
jemalloc 은 메모리 반환에 보다 적극적이라고 하는데요.</p>
<p><a href="https://medium.com/motive-eng/we-solved-our-rails-memory-leaks-with-jemalloc-5c3711326456">https://medium.com/motive-eng/we-solved-our-rails-memory-leaks-with-jemalloc-5c3711326456</a></p>
<p>다만 malloc 구현체를 jemalloc 으로 변경 시에 기존의 로직에 영향이 갈 수 있으니 충분한 테스트가 동반되어야 할듯합니다.</p>
<h2 id="쓰레드가-메모리-적당히-쓰게하기">쓰레드가 메모리 적당히 쓰게하기...</h2>
<p>저의 경우에는 대용량 데이터에 대해 처리하는 기능이었습니다.
메모리를 500메가 정도 차지했고 동시에 여러 워커가 이 작업을 수행 시에 사용하는 메모리의 총합은 1.8기가까지 찍고 서버 따운! 되었습니다. (쿠버네티스에서 설정된 메모리 limit은 1기가)</p>
<p>저는 이 작업을 batch로 처리하여 메모리 사용량을 30메가 수준으로 줄였습니다.
active record로 db 데이터를 읽어올 때 find_in_batches 를 사용했습니다.</p>
<p>물론 find_in_batches 를 쓰는 것만으로 해결된 것은 아니고요
작업이 끝난 active record 인스턴스를 gc에서 수집해가지 않아서
gc.start(); gc.compact(); 를 직접 호출해 줬습니다.</p>
<p>gc가 실행되는 동안에 애플리케이션의 다른 작업들이 중단되기 때문에 매우 비싼 작업입니다.
다만 해당 기능이 사용되는 빈도가 매우 낮은 점, malloc_arena_max 나 jemallc으로 변경은 기존의 다른 로직에 전체적인 영향을 줄 수 있는 점을 고려하여 batch + gc 실행 방법으로 해결하였습니다.</p>
<h2 id="thanks-to">thanks to</h2>
<p>위 작업은 스와치온에 입사하고 처음으로 받은 테스크를 해결하는 과정에서 겪은 것입니다.
덕분에 ruby 메모리에 대하여 깊게 이해해 볼 수 있었습니다.</p>
<p>원단은? 스와치온!
<a href="https://swatchon.com">https://swatchon.com</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[bugfix] Caused by: java.sql.SQLIntegrityConstraintViolationException at ProblemRepositoryTest.java:48]]></title>
            <link>https://velog.io/@i-no/bugfix-Caused-by-java.sql.SQLIntegrityConstraintViolationException-at-ProblemRepositoryTest.java48</link>
            <guid>https://velog.io/@i-no/bugfix-Caused-by-java.sql.SQLIntegrityConstraintViolationException-at-ProblemRepositoryTest.java48</guid>
            <pubDate>Tue, 04 Oct 2022 13:37:50 GMT</pubDate>
            <description><![CDATA[<p>인천의 한 월요일
로컬에서는 모두 성공적이던 테스트가 서버에서는 실패했다.</p>
<p><img src="https://velog.velcdn.com/images/i-no/post/54520191-141f-4413-b6f1-3d9193c8c8ca/image.png" alt=""></p>
<p>많은 테스트중 2가지만 실패했다.
원인이 무엇일지 해당 에러를 구글링 해보았는데...
구글링 결과 데이터베이스 무결성 관련된 에러라고
제약조건을 위배 했을때 발생한다고...</p>
<h3 id="추측-1--problem-entity의-column에-unique-제약조건-추가-문제">추측 1 : problem entity의 column에 unique 제약조건 추가 문제</h3>
<p>이전에 다른 테스트는 서버에서도 성공적으로 실행이 되었다.
이전과 달라진 점은 많지만 그 중에서도 이전에는 제약조건이 누락이 되어있었다.
이번 풀리퀘스트에 제약조건 추가도 함께 진행하여 이것이 원인이 아닌가 추측했다.
그러나 unique 제약조건을 추가한 column은 unique하게 생성하여 테스트를 진행하고 있어 문제가 없어 보였다.</p>
<p>혹시 모르니 해당 제약 조건을 제거 하고 커밋을 날려봤지만 그래도 결과는 동일했다.</p>
<h3 id="추측-2--dialect-문제">추측 2 : dialect 문제</h3>
<p>로컬에서는 잘 알아먹는걸 dialect 차이로 잘못 알아먹어서 발생한 문제인가??
dialect는 로컬과 서버가 동일하게 설정되어 있어 그럴일은 없을거라 생각하지만 혹시 몰라서 test/resources/application.yml에 dialect를 수정했다.</p>
<h4 id="추가한-내용">추가한 내용</h4>
<pre><code class="language-yml">    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5Dialect</code></pre>
<p>그랬더니 테스트 성공...
<img src="https://velog.velcdn.com/images/i-no/post/c6c27c1b-93cf-4d65-a557-c4738ccf2e8a/image.png" alt=""></p>
<p>굉장히 허무했지만 예상 못했던 원인이라 오랜 시간 고민했던 에러고
구글링을 통해 찾을 수 없었던 원인이라 같은 상황을 겪는 누군가에게 도움이 될 수 있도록 블로그에 작성했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[mockito 테스트 하나씩 실행하면 성공하고 한번에 실행 하면 실패하는 경우]]></title>
            <link>https://velog.io/@i-no/mockito-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%98%EB%82%98%EC%94%A9-%EC%8B%A4%ED%96%89%ED%95%98%EB%A9%B4-%EC%84%B1%EA%B3%B5%ED%95%98%EA%B3%A0-%ED%95%9C%EB%B2%88%EC%97%90-%EC%8B%A4%ED%96%89-%ED%95%98%EB%A9%B4-%EC%8B%A4%ED%8C%A8%ED%95%98%EB%8A%94-%EA%B2%BD%EC%9A%B0</link>
            <guid>https://velog.io/@i-no/mockito-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%98%EB%82%98%EC%94%A9-%EC%8B%A4%ED%96%89%ED%95%98%EB%A9%B4-%EC%84%B1%EA%B3%B5%ED%95%98%EA%B3%A0-%ED%95%9C%EB%B2%88%EC%97%90-%EC%8B%A4%ED%96%89-%ED%95%98%EB%A9%B4-%EC%8B%A4%ED%8C%A8%ED%95%98%EB%8A%94-%EA%B2%BD%EC%9A%B0</guid>
            <pubDate>Mon, 11 Jul 2022 05:02:20 GMT</pubDate>
            <description><![CDATA[<p>최근에 프로젝트를 진행하며 2주동안 해결하지 못했던 문제가 있었습니다.</p>
<p>service 단위 테스트를 작성하는데 mockito 를 사용하여 repository를 mock 처리하고 service에 insertMock 하였습니다.</p>
<p>단위 테스트에서 테스트 메서드를 하나씩 작성할 때 마다 하나씩 실행시키며 잘되는 것을 확인하며 진행하였습니다.</p>
<p>service의 특정 메서드 테스트를 모두 작성한 뒤 한번에 실행시켰더니 일부 테스트가 실패했습니다.</p>
<p>테스트를 주석 처리하고 하나씩 실행시키면 다시 정상적으로 작동하였습니다.</p>
<p>실패할때는 모킹한 객체가 doReturn 으로 설정한대로 동작하지 않았습니다.</p>
<h3 id="의심-1--모킹된-객체가-주입되지-않았다">의심 1 : 모킹된 객체가 주입되지 않았다</h3>
<p>이는 간단하게 확인할 수 있었습니다.
디버깅을 통해 service에서 사용하는 repository를 확인해보니 모킹된 객체임을 확인할 수 있었습니다.</p>
<h3 id="의심-2--doreturn-이-제대로-설정되지-않았다">의심 2 : doReturn 이 제대로 설정되지 않았다.</h3>
<p>repository가 doReturn 한대로 동작하는지 확인하기 위해 테스트 코드에서 모킹된 repository를 검증하였고 정상적으로 동작함을 확인할 수 있었습니다.</p>
<h3 id="의심-3--doreturn-이-중첩되지-않는다">의심 3 : doReturn 이 중첩되지 않는다.</h3>
<p>repository에서 findById 메서드를 호출하는데 상황에 따라 비어있는 optional을 리턴하기도 하고 비어있지 않은 optional을 리턴하도록 하고 있습니다.
둘다 any()로 argument를 받고 있어서 상황에 따라 구분이 안되는 것이 아닌가 생각되어 any() 대신 eq(1L), eq(2L) 과 같은 식으로 구별할 수 있도록 수정하였으나 해결되지 않았습니다.</p>
<h3 id="의심-4--같은-인스턴스를-사용하여-테스트-코드의-독립성이-보장되지-않았다">의심 4 : 같은 인스턴스를 사용하여 테스트 코드의 독립성이 보장되지 않았다.</h3>
<p>테스트 코드에서 서비스와 레포지토리는 테스트 최상위 클래스에서 선언하여 해당 테스트에서 모두 공유하고 있었습니다.
때문에 각 인스턴스가 공유되어 독립성이 보장되지 않는 문제일 수 있다고 생각되어 각 테스트 메서드를 내장 클래스로 만들고 각각 인스턴스를 선언하여 사용하도록 수정하였더니 테스트를 모두 통과하였습니다.</p>
<p>그러나 각 메서드를 별도의 클래스로 감싸고 각각 인스턴스를 선언해서 사용하는 방식은 유지보수에 굉장히 문제가 많은 방식입니다.</p>
<p>더 좋은 방법을 찾기 위해 많은 예시 코드를 찾아보았지만 모두 별도의 클래스로 감싸지 않아도 잘 작동하였습니다.</p>
<h3 id="일단-처음부터-다시-해보자">일단 처음부터 다시 해보자</h3>
<p>다른 예시 코드와 차이가 없는데 제 코드만 문제가 발생하는 것이 억울하였습니다.
일단 작성한 테스트 코드를 두고 새로운 테스트 코드 파일을 생성하여 처음부터 작성해보았습니다.
다시 작성하였더니 신기하게도 인스턴스를 공유해도 문제가 없었습니다.</p>
<h3 id="진짜-원인--testinstancelifecycleper_class">진짜 원인 : @TestInstance(LifeCycle.PER_CLASS)</h3>
<p>제가 @BeforeAll을 사용하기 위해 @TestInstance(LifeCycle.PER_CLASS)를 사용했습니다.
때문에 라이프 사이클이 각 클래스 단위로 설정되어 인스턴스가 독립적이지 않고 공유하게 되었습니다.</p>
<p>해당 어노테이션을 삭제하고 beforeAll을 삭제하였더니 테스트가 모두 통과하였습니다.</p>
<p>해결하여 기쁘고 너무 간단한 원인이라 허탈했습니다.</p>
<h3 id="결론">결론</h3>
<p>문제가 해결이 안될때는 어노테이션도 잘 살피고
예시와 같은데 왜 안되지?? 할때는 처음부터 작성해보기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[독립 테스트 환경 구축]]></title>
            <link>https://velog.io/@i-no/%EB%8F%85%EB%A6%BD-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95</link>
            <guid>https://velog.io/@i-no/%EB%8F%85%EB%A6%BD-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95</guid>
            <pubDate>Sat, 28 May 2022 10:12:40 GMT</pubDate>
            <description><![CDATA[<h3 id="테스트-코드-작성중-이상한-점-발견">테스트 코드 작성중 이상한 점 발견</h3>
<p><img src="https://velog.velcdn.com/images/i-no/post/5b249a2c-646b-49c4-b6c8-9c0dc7fdebd8/image.png" alt=""></p>
<p>유저를 20명 넣고 findAll 을 한 뒤 출력을 해봤는데 user1~10이 2번씩 들어가있다.</p>
<h3 id="문제점">문제점</h3>
<p>유저 이름에 유니크 제약 조건을 걸지 않았다
-&gt; entity에 유니크 제약 조건이 없는거 처리해주기</p>
<p>로컬 데이터베이스에서 테스트를 진행하고 있었다.
-&gt; 로컬 데이터베이스의 데이터와 테스트 데이터가 혼합되어 기대하지 않은 결과가 발생함
-&gt; 독립 테스트 환경 구축해주기</p>
<h3 id="독립-테스트-환경-구축-방법">독립 테스트 환경 구축 방법</h3>
<p>찾아보니 크게 2가지 방법이 존재한다.</p>
<ol>
<li>h2로 테스트 데이터베이스를 구축하여 인메모리로 빠른 테스트 환경 구축</li>
</ol>
<p>빠른 속도가 장점이다.
실제 서비스에서 이용하는 db는 mysql인데 h2로 테스트를 진행하므로 실제 쿼리 수행과 차이가 있을수 있다고 한다.
직관적으로는 hibernate 가 잘 처리해줘서 실제 쿼리 수행과 차이가 없지 않을까 하는 생각이 든다.
스키마 설정을 따로 해줘야한다고 한다.</p>
<ol start="2">
<li>testContainer 를 사용하여 테스트시 스키마 생성하여 테스트 진행</li>
</ol>
<p>이 방법은 도커에 새로운 스키마를 생성하도록 하는 방법인데 상대적으로 속도가 느리다고 한다.
ci 처럼 서버에서 이용시 메모리 사용량에 주의해야 된다고 한다.
그런데 h2도 메모리 사용량에 주의해야 하지 않을까? 하는 생각이 들긴한다.</p>
<h3 id="결론">결론</h3>
<p>일단 h2 사용이 간편해 보인다.
하지만 실제 쿼리 수행과 차이가 있을 수 있다는 블로그 글 때문에 이게 사실인지 팩트체크가 필요해 보인다.
tdd를 수행하다가 테스트 코드가 자꾸 실패해서 막 다 뜯어 고쳤는데 알고보니 쿼리 수행 차이 때문이라면 눈물이 차올라서 고개를 살짝 들것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[entity setter 고민 결과 2]]></title>
            <link>https://velog.io/@i-no/entity-setter-%EA%B3%A0%EB%AF%BC-%EA%B2%B0%EA%B3%BC-2</link>
            <guid>https://velog.io/@i-no/entity-setter-%EA%B3%A0%EB%AF%BC-%EA%B2%B0%EA%B3%BC-2</guid>
            <pubDate>Thu, 26 May 2022 11:52:02 GMT</pubDate>
            <description><![CDATA[<p>지난 고민에서 내린 결론은 user와 team의 관계를 저장하는 userTeam을 서비스 계층에서 생성하고 플러시 하는 방식이다.</p>
<p>하지만 이 방법은 객체지향적이지 못하다는 생각이 들어 더 좋은 방법을 계속 고민했다.
성능도 좋으면서 객체지향적인 코드에 대한 고민을 계속한 끝에 내린 결론은</p>
<h3 id="지금-너무-객체지향적인-코드에-집착하고-있다는-것이다">지금 너무 객체지향적인 코드에 집착하고 있다는 것이다.</h3>
<p>객체지향적인 코드를 작성하는 것은 물론 중요하다.
객체지향적인 코드가 주는 장점은 너무나 많다.
그러나 객체지향적인 코드는 결국 목적이 아니라 도구이다.
객체지향은 더 효율적으로 일하기 위한 도구이고 다른 도구를 쓰더라도 중요한 것은 목적을 달성하는 것이다.</p>
<p>나는 지금 더 효율적인 코드를 찾았음에도 이 방식은 객체지향이라는 도구를 쓰지 않았다는 이유로 더 고민하고 있었다.</p>
<p>진짜 중요한 것은 어떤 도구를 사용하던 목적을 달성하는 것인데 말이다.</p>
<h3 id="그리고-user와-team의-객체-그래프-탐색에-집착하고-있었다">그리고 user와 team의 객체 그래프 탐색에 집착하고 있었다.</h3>
<p>책에서 객체 그래프 탐색이 원활하게 하는 것이 좋다고 하였다.
그래서 user와 team 의 멤버로 userTeams 를 추가하여 사용했다.
그리고 이 데이터 동기화를 위해 setter 메서드를 어떻게 작성할지 많이 고민했다.</p>
<p>그러나 user와 team 에서 userTeam을 접근하는 양방향 관계가 정말 필요한 것인가에 대해 고민해보지 않았다.</p>
<p>user와 team에서 getUserTeam을 하지 않는다면 양방향 관계가 필요하지 않다.
그럼 userTeams 를 멤버로 사용하지 않아도 되고 데이터 동기화를 위해 setter 메서드를 고민하지 않아도 된다.</p>
<h3 id="결론">결론</h3>
<p>가끔 방법에 집착하여 목적을 까먹는 경우가 있다.
항상 내가 하는 행위의 목적을 생각하며 그 목적을 위해 진짜 필요한 고민을 하자</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 애플리케이션과 영속성 관리]]></title>
            <link>https://velog.io/@i-no/%EC%9B%B9-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98%EA%B3%BC-%EC%98%81%EC%86%8D%EC%84%B1-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@i-no/%EC%9B%B9-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98%EA%B3%BC-%EC%98%81%EC%86%8D%EC%84%B1-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Mon, 23 May 2022 14:07:41 GMT</pubDate>
            <description><![CDATA[<p>자바 ORM 표준 JPA 프로그래밍
<img src="https://velog.velcdn.com/images/i-no/post/b8cb40a8-0710-4b43-be98-b3beacd3f8f1/image.png" alt="">
<a href="http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&amp;ejkGb=KOR&amp;barcode=9788960777330">http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&amp;ejkGb=KOR&amp;barcode=9788960777330</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[entity setter 고민 결과]]></title>
            <link>https://velog.io/@i-no/entity-setter-%EA%B3%A0%EB%AF%BC-%EA%B2%B0%EA%B3%BC</link>
            <guid>https://velog.io/@i-no/entity-setter-%EA%B3%A0%EB%AF%BC-%EA%B2%B0%EA%B3%BC</guid>
            <pubDate>Sun, 22 May 2022 14:18:22 GMT</pubDate>
            <description><![CDATA[<h4 id="유저에-팀-추가하는-로직은">유저에 팀 추가하는 로직은</h4>
<p>단순히 유저에 팀을 추가하고 끝나는 서비스라면</p>
<p>서비스 계층에서 단순히 UserTeam 인스턴스를 생성하고 트랜잭션을 커밋하는 방식으로 구현한다.
유저에 추가된 팀이 여러개라면 여러개의 UserTeam 인스턴스를 생성하고 트랜잭션을 커밋한다.
인스턴스 하나 생성할 때 마다 flush 할 필요는 없다고 생각된다.
이후에 진행될 로직이 추가적인 UserTeam 인스턴스를 생성하는 것이다.
flush 를 해줄 이유는 User나 Team 데이터를 사용할때 UserTeam 데이터와 일치하지 않을 수 있기 때문인데 이후 User 나 Team 데이터를 사용하지 않고 UserTeam 인스턴스만 추가로 생성하고 트랜잭션을 커밋하기 때문에 해당 로직을 모두 수행하고 트랜잭션 커밋해도 문제가 없다.</p>
<h4 id="유저의-팀을-삭제하는-로직은">유저의 팀을 삭제하는 로직은</h4>
<p>단순히 유저의 팀을 삭제하고 끝나는 서비스라면</p>
<p>마찬가지로 서비스 계층에서 UserTeam 인스턴스를 삭제해주면된다.
여러개의 팀을 삭제한다면 여러개의 UserTeam 인스턴스를 삭제한 뒤에 트랜잭션 커밋을 해주면 된다.
이유는 팀 추가 로직과 동일하다.</p>
<h4 id="단순히-유저의-팀을-추가-삭제만-하지-않는-서비스라면">단순히 유저의 팀을 추가 삭제만 하지 않는 서비스라면??</h4>
<p>유저의 팀을 추가 삭제할 때는 위의 서비스를 이용하도록 한다면 문제가 없을 것이다.</p>
<h3 id="이-과정에서-생겨난-고민-2">이 과정에서 생겨난 고민 2</h3>
<p>유저가 소속된 팀이 추가 제거 되는 것을 어떻게 감시할 것인가??</p>
<p>유저가 소속된 팀을 추가 제거시 우리 서버에서는 즉시 확인할 수가 없고 solved ac 에 API 요청을 해야만 확인 가능하다.</p>
<ol>
<li>유저 정보 갱신시 혹은 주기적으로 유저가 소속된 팀 리스트를 request body에서 받아서 데이터베이스의 데이터와 비교한다</li>
</ol>
<p>이 방식은 주기적 혹은 갱신 전까지 데이터가 실제와 불일치하는 문제가 있을 수 있다.
유저가 우리 서비스에서 그룹을 변경하는 것이 아니기 때문에 다른 방법은 없는거 같다.
회의할 때 안건으로 이야기 해봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[entity setter]]></title>
            <link>https://velog.io/@i-no/entity-setter</link>
            <guid>https://velog.io/@i-no/entity-setter</guid>
            <pubDate>Sun, 22 May 2022 00:10:09 GMT</pubDate>
            <description><![CDATA[<h3 id="entity-setter">entity setter</h3>
<p>entity에서 멤버 변수를 초기화 할 때 setter 메서드를 사용하는 것은 문제점이 있다.</p>
<ol>
<li><p>setter 메서드를 사용하면 한번 초기화 이후 값이 변경 되면 안되는 경우에 다시 setter 메서드를 사용하여 변경할 가능성이 존재한다.</p>
</li>
<li><p>setter 메서드의 메서드명에는 메서드의 의도가 담겨있지 않아서 어떤 용도로 사용해야하는지 알기 어렵다.</p>
</li>
</ol>
<h3 id="변경-가능성-문제">변경 가능성 문제</h3>
<p>따라서 한번 초기화 이후 값이 변경 되면 안되는 경우에는 setter 메서드 대신 생성자에서 초기화 해주는 방식으로 하고
대신 생성자에서 초기화 해주지 않으면 이후 값을 초기화 해줄 수 없기 때문에 기본 생성자로 public 접근을 막아두어야 한다.
JPA를 사용하려면 기본 생성자를 만들어 둬야하기 때문에 JPA에서 허용하는 protected 수준으로 기본 생성자를 정의해둔다.</p>
<h3 id="메서드명-문제">메서드명 문제</h3>
<p>setter 메서드 대신 의도에 맞게 멤버 변수의 값을 설정하는 메서드를 작성하면 된다.
예를 들어 유저가 문제를 풀어서 푼 문제 수와 유저의 점수 값을 변경해야하는 경우 이 두 멤버 변수 값을 변경해주는 user.solvingProblem() 메서드를 작성할 수 있다.
이렇게 작성하게 되면 메서드 명 만 보고도 메서드의 의도를 알 수 있어서 언제 해당 메서드를 사용해야할지 명확하게 알 수 있다.</p>
<h3 id="이-방식의-문제점">이 방식의 문제점</h3>
<p>메서드 명이 의도에 따라 다양하게 작성되어 어떤 메서드들이 있는지 entity를 열어 확인하고 구현도 확인해야 사용할 수 있다.</p>
<h3 id="unsovled-wa-에서-entity-메서드-작성">unsovled-wa 에서 entity 메서드 작성</h3>
<p><img src="https://velog.velcdn.com/images/i-no/post/fe7b7b09-5d33-45e4-90ba-42dc98ed43db/image.png" alt=""></p>
<p>여기서 지금 user의 entity 메서드를 작성중이다.</p>
<p>지금 고민중인 부분은 user가 소속된 team이 여러개일 수 있는데 이를 추가 제거하는 메서드를 어떻게 구현할지 고민중이다.</p>
<p>entity는 원활한 객체 그래프 탐색을 위해 서로 참조를 저장하고 있어 user에 team을 추가해주면 해당 team에도 user를 추가해줘야 하고 둘 사이의 user_team 에도 user와 team 를 추가해줘야한다.</p>
<p>이 부분은</p>
<pre><code class="language-java">public void setTeam(Team team){
        UserTeam userTeam = new UserTeam(this, team);
        this.userTeams.add(userTeam);
        team.getUserTeams().add(userTeam);
    }</code></pre>
<p>이렇게 구현하려하고 있다.</p>
<p>문제는 유저가 팀에서 탈퇴했을 때 삭제 로직을 고민 중이다.</p>
<ol>
<li>user의 userTeams를 for문으로 돌면서 userTeam.getTeam() == 탈퇴하려는 team 일때 해당 userTeam 을 pop하는 방법</li>
</ol>
<p>이 방법은 유저가 팀이 많을 경우 userTeam을 모두 탐색해야하는 되는 로직이 비효율적으로 보인다는 점</p>
<p>또 추가로 team에서도 team의 userTeams에서 해당 userTeam을 찾아서 삭제해줘야 한다는 점이 비효율적으로 보인다.</p>
<ol start="2">
<li>userTeam을 cascade를 이용해서 em.remove() 하는 방법</li>
</ol>
<p>이 경우에는 cascade를 이용해서 userTeam row를 삭제해도 user와 team의 userTeams에는 즉시 반영되지 않는 문제가 있다.</p>
<p>해결을 위해서 transaction을 flush 하는 방법도 있지만 remove할 때마다 flush를 하는 것이 효율적인지는 고민해봐야 할 것 같다.</p>
<p>좀 더 고민을 해봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[값 타입]]></title>
            <link>https://velog.io/@i-no/%EA%B0%92-%ED%83%80%EC%9E%85</link>
            <guid>https://velog.io/@i-no/%EA%B0%92-%ED%83%80%EC%9E%85</guid>
            <pubDate>Fri, 20 May 2022 13:47:19 GMT</pubDate>
            <description><![CDATA[<p>자바 ORM 표준 JPA 프로그래밍
<img src="https://velog.velcdn.com/images/i-no/post/b8cb40a8-0710-4b43-be98-b3beacd3f8f1/image.png" alt="">
<a href="http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&amp;ejkGb=KOR&amp;barcode=9788960777330">http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&amp;ejkGb=KOR&amp;barcode=9788960777330</a></p>
<h3 id="값-타입">값 타입</h3>
<p>JPA의 테이터 타입은 엔티티 타입과 값 타입으로 나눌 수 있다.</p>
<p>엔티티 타입은 식별자를 통해 속성이 바뀌얻 같은 엔티티 임을 알 수 있지만</p>
<p>값 타입은 식별자가 없고 속성만 있으므로 속성이 바뀌면 구별 불가능하다.</p>
<p>값 타입은 기본값 타입, 임베디드 타입, 컬렉션 값 타입이 있다.</p>
<h3 id="기본값-타입">기본값 타입</h3>
<p>Member 엔티티의 멤버 변수 String name, int age 등이 기본 값 타입이다.
name, age 속성은 식별자 값도 없다.
생명주기 또한 Member 엔티티에 의존한다.</p>
<p>값 타입은 공유하면 안된다.
한 member 의 name을 변경했을 때 다른 member 의 name이 변경되면 안되기 때문이다.</p>
<p>클래스가 아닌 값 타입은 대입 연산자로 복사를 해도 값만 복사된다.</p>
<h3 id="임베디드-타입">임베디드 타입</h3>
<p>새로운 값 타입을 직접 정의해서 사용하는 것을 JPA에서는 임베디드 타입이라고 한다.
예를들어 주소를 저장할때 String city, String street, String zipcode 이렇게 따로 관리하던 걸 이 세가지 속성을 가지는 Address 타입을 만들어 사용할 수 있다.</p>
<p>정의하려는 타입을 클래스로 구현하고 @Embeddable을 붙여주면 된다.
사용할 때는 타입 왼쪽에 @Embedded 을 붙여주면 된다.</p>
<p>임베디드 타입은 기본 생성자가 필수다.</p>
<p>임베디드 타입 또한 값 타입이므로 엔티티의 생명주기에 의존한다.
이 때문에 엔티티와 임베디드 타입의 관계를 UML로 표현하면 컴포지션 관계가 된다.</p>
<p>하이버네이트는 임베디드 타입을 컴포넌트라 한다.</p>
<p>임베디드 타입은 엔티티의 값일 뿐이므로 기존 속성을 임베디드 타입으로 바꾸어도 테이블은 변하지 않는다.</p>
<h4 id="임베디드-타입과-연관관계">임베디드 타입과 연관관계</h4>
<p>임베디드 타입의 속성이 연관관계를 가지는 경우 해당 속성에 어노테이션으로 연관관계를 설정해주면 된다.</p>
<h4 id="임베디드-연관관계-재정의">임베디드 연관관계 재정의</h4>
<p>임베디드 타입에 정의한 매핑정보를 재정의하려면 엔티티에서 임베디드 타입에 @AttributeOverride를 사용하면 된다.</p>
<h4 id="임베디드-타입-null">임베디드 타입 null</h4>
<p>임베디드 타입이 Null이면 매핑한 컬럼 값은 모두 null이 된다.</p>
<h3 id="값-타입과-불변-객체">값 타입과 불변 객체</h3>
<p>임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.
따라서 대입 연산사를 사용하면 같은 인스턴스를 가리키게 되기 때문에 문제가 발생할 수 있다.</p>
<p>따라서 clone 같은 메서드를 작성하여 속성의 값만 복사할 수 있도록 해주어야한다.</p>
<p>또한 임베디드 타입의 클래스에 setter 메서드를 제거하여 만약 공유 참조를 해도 값을 변경하지 못하게 하여 부작용의 발생을 막을 수 있다.</p>
<h4 id="불변-객체">불변 객체</h4>
<p>이처럼 객체를 불변하게 만들어 값을 수정할 수 없게 부작용을 차단한 것을 불변 객체라 한다.
값 타입은 가능하면 불변 객체로 설계해야 한다.</p>
<p>Integer, String 은 자바가 제공하는 불변 객체이다.</p>
<h3 id="값-타입의-비교">값 타입의 비교</h3>
<p>임베디드 타입의 경우에는 객체이므로 equals 메서드를 재정의하여 동등성을 비교 할 수 있다.
equals를 재정의 할 때는 hashCode 메서드도 재정의 하는 것이 안전하다.
그래야 해시를 사용하는 컬렉션이 정상 작동할 수 있다.</p>
<p>자바 ID의 자동 재정의 기능을 이용하면 편하다.</p>
<h3 id="값-타입-컬렉션">값 타입 컬렉션</h3>
<p>값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection 과 @CollectionTable 어노텡션을 사용하면 된다.</p>
<p>관계형 데이터베이스의 테이블은 컬럼안에 컬렉션을 포함할 수 없다.
@CollectionTable 어노테이션을 사용하면 테이블이 추가되고 매핑된다.</p>
<p>값 타입 컬렉션은 영속성 전이와 고아 객체 제거 기능을 필수도 가진다.</p>
<p>값 타입 컬렉션 조회시 페치 전략은 LAYZ 가 기본 값이다.</p>
<h3 id="값-타입-컬렉션의-제약사항">값 타입 컬렉션의 제약사항</h3>
<p>특정 엔티티 하나에 소속된 값 타입은 값이 변경되어도 자신이 소속된 엔티티를 데이터베이스에서 찾고 값을 변경하면 된다.</p>
<p>값 타입 컬렉션은 별도의 테이블에 보관 되기 때문에 수정 되었을때 원본 데이터를 찾기 어려워 연관된 데이터를 모두 삭제하고 수정된 컬렉션 전체를 다시 저장한다.</p>
<p>따라서 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신 일대다 관계를 고려해야 한다.</p>
<p>또한 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야한다.
따라서 컬럼에 null을 입력할 수 없고, 같은 값을 중복해서 저장할 수 없다.</p>
<p>이를 해결하기 위해서는 일대다 관계로 설정하면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프록시와 연관관계 관리]]></title>
            <link>https://velog.io/@i-no/%ED%94%84%EB%A1%9D%EC%8B%9C%EC%99%80-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@i-no/%ED%94%84%EB%A1%9D%EC%8B%9C%EC%99%80-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Thu, 19 May 2022 14:03:30 GMT</pubDate>
            <description><![CDATA[<p>자바 ORM 표준 JPA 프로그래밍
<img src="https://velog.velcdn.com/images/i-no/post/b8cb40a8-0710-4b43-be98-b3beacd3f8f1/image.png" alt="">
<a href="http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&amp;ejkGb=KOR&amp;barcode=9788960777330">http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&amp;ejkGb=KOR&amp;barcode=9788960777330</a></p>
<h3 id="프록시">프록시</h3>
<p>객체는 데이터베이스에 저장되어 있기 때문에 객체 그래프로 연관된 객체들을 탐색할 때 마음껏 탐색하기 어렵다.
프록시를 사용하여 지연로딩을 이용하면 이 문제를 해결 할 수 있다.</p>
<p>엔티티를 조회할 때 연관된 엔티티들을 사용하지 않는 경우도 있기 때문에 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 지연로딩을 사용한다.</p>
<p>실제 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체를 프록시 객체라고한다.</p>
<p>프록시 클래스는 실제 클래스를 상속 받아서 만들어지므로 실제 클래스와 겉 모양이 같다.</p>
<h3 id="프록시-기초">프록시 기초</h3>
<p>em.find로 엔티티를 직접 조회하면 데이터베이스를 조회하게 된다.</p>
<p>이를 실제 사용하는 시점까지 조회를 미루고 싶으면 em.getReference 메소드를 사용하면 된다.
이 메소드는 데이터베이스 접근은 위임한 프록시 객체를 반환한다.</p>
<h3 id="프록시-객체의-초기화">프록시 객체의 초기화</h3>
<p>프록시 객체는 member.getName() 처럼 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.
이것이 프록시 객체의 초기화이다.</p>
<p>member.getNmae()을 호출하면</p>
<ol>
<li>영속성 컨텍스트에서 실제 데이터를 조회한다.</li>
<li>없으면 영속성 컨텍스트에 실제 엔티티 생성을 요청한다.</li>
<li>그럼 영속성 컨텍스트가 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다.</li>
<li>프록시 객체는 생성된 실제 엔티티 객체의 참조를 멤버변수로 보관한다.</li>
<li>프록시 객체가 실제 엔티티 객체의 getName를 호출해서 결과를 반환한다.</li>
</ol>
<h3 id="프록시의-특징">프록시의 특징</h3>
<p>프록시 객체는 처음 사용할 때 한번만 초기화된다.</p>
<p>초기화 이후 프록시 객체가 실제 엔티티에 접근할 수 있다.</p>
<p>프록시 객체는 원본을 상속받은 객체라서 타입 체크할 때 주의해야한다.</p>
<p>영속성 컨텍스트에 찾으려는 엔티티가 이미 있으면 em.getReference를 호출해도 실제 엔티티를 반환한다.</p>
<p>초기화는 영속성 컨텍스트의 도움을 받아야 가능하다.
준영속 상태에서 초기화하면 예외가 발생한다.</p>
<h3 id="프록시와-식별자">프록시와 식별자</h3>
<p>엔티티를 프록시로 조회할 때 식별자 값을 프록시 객체가 보관한다.
따라서 이 식별자 값을 조회하는 메소드를 호출해도 프록시를 초기화 하지 않는다. (엔티티 접근 방식을 프로퍼티로 설정한 경우에만)</p>
<p>프록시는 연관관계를 설정할 때 유용하게 사용할 수 있다.
연관관계를 설정할 때는 식별자 값만 사용하기 때문이다.</p>
<h3 id="프록시-확인">프록시 확인</h3>
<p>PersistenceUnitUtil.isLoaded(Object entity) 메소드로 프록시 인스턴스의 초기화 여부를 확인할 수 있다.</p>
<h3 id="즉시-로딩">즉시 로딩</h3>
<p>엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
이때 연관된 테이블도 조회해야 하기 때문에 쿼리가 여러번 실행될 것 같지만 JPA 구현체는 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용한다.</p>
<p>이때 조인은 left outer join으로 실행된다.
외래 키가 null을 허용하고 있는지 여부를 알수 없기 때문이다.</p>
<p>외래 키가 null허용하고 있지 않다면 @JoinColumn에 nullable = false 를 설정해서 JPA에게 알려주면 inner join으로 실행되어 성능과 최적화에서 더 유리하다.</p>
<h3 id="즉시-로딩-지연-로딩">즉시 로딩, 지연 로딩</h3>
<p>필요할 때마다 SQL을 실행해서 연관된 엔티티를 지연 로딩하는 것도 최적화 관점에서 항상 좋은 것은 아니다.
연관된 엔티티를 같이 사용한다면 즉시 로딩으로 한번에 조회하는 것이 효율적이다.</p>
<h3 id="프록시와-컬렉션-래퍼">프록시와 컬렉션 래퍼</h3>
<p>하이버네이트는 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로 원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경한다.
이것을 컬렉션 래퍼라 한다.</p>
<p>엔티티를 지연 로딩하면 엔티티의 컬렉션은 컬렉션 래퍼가 지연 로딩 처리해준다.
컬렉션 래퍼가 컬렉션에 대한 프록시 역할을 해준다.</p>
<p>member.getOrders()를 호출해도 컬렉션은 초기화 되지 않고
member.getOrders().get(0) 처럼 컬렉션에서 실제 데이터를 조회할 때 데이터 베이스를 조회해서 초기화한다.</p>
<h3 id="jpa-기본-페치-전략">JPA 기본 페치 전략</h3>
<p>@ManyToOne, @OneToOne 은 즉시 로딩
@OneToMany, @ManyToMany 는 지연 로딩이 기본 값이다.</p>
<p>즉 연관된 엔티티가 하나면 즉시 로딩이, 컬렉션이면 지연 로딩이 기본 값이다.</p>
<p>컬렉션을 로딩하는 것은 비용이 많이 들고 너무 많은 데이터를 로딩할 수 있기 때문이다.</p>
<p>그러나 추천하는 방법은 모두 지연 로딩을 사용하는 것이다.
이후 꼭 필요한 곳에만 즉시 로딩을 사용하도록 최적화 하면 된다.
이때 SQL을 직접 사용하면 유연한 최적화가 어렵다.</p>
<h3 id="컬렉션에-즉시-로딩-사용시-주의점">컬렉션에 즉시 로딩 사용시 주의점</h3>
<h4 id="컬렉션을-하나-이상-즉시-로딩하는-것은-권하지-않는다">컬렉션을 하나 이상 즉시 로딩하는 것은 권하지 않는다.</h4>
<p>컬렉션과 조인한다는 것은 일대다 조인이다.
이때 결과 데이터는 다의 수만큼 증가하게 된다.
서로 다른 컬렉션을 2개 이상 조인하게 되면 다*다 로 엄청 많은 데이터가 반환될 수 있다.
JPA는 이렇게 조회된 결과를 메모리에서 필터링해서 반환한다.</p>
<h4 id="컬렉션-즉시-로딩은-항상-outer-join을-사용한다">컬렉션 즉시 로딩은 항상 outer join을 사용한다.</h4>
<p>팀과 회원의 경우에 회원 테이블의 외래 키에 not null 제약조건을 걸어 내부 조인을 사용해도 된다.
그러나 팀에서 회원으로 조회할 때는 회원이 없는 팀의 경우 inner join을 하게되면 조회되지 않는다.
따라서 일대다를 즉시 로딩할 때는 outer join을 사용해야한다.</p>
<h3 id="영속성-전이-cascade">영속성 전이 (CASCADE)</h3>
<p>특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만드는 기능이다.</p>
<h3 id="영속성-전이로-저장">영속성 전이로 저장</h3>
<p>@OneToMany(cascade = CascadeType.PERSIST)
로 설정하면 해당 엔티티를 저장할 때 이렇게 설정한 연관된 엔티티도 영속화해서 저장한다.</p>
<h3 id="영속성-전이로-삭제">영속성 전이로 삭제</h3>
<p>@OneToMany(cascade = CascadeType.REMOVE)
로 설정하면 해당 엔티티를 삭제할 때 이렇게 설정한 연관된 엔티티도 함께 삭제한다.</p>
<h3 id="고아-객체">고아 객체</h3>
<p>부모 엔티티와 연관관계가 끊어진 자식 엔티티를 고아 객체라고 한다.
JPA는 고아 객체 제거 기능을 제공한다.
@OneToMany(orphanRemoval = true)
로 설정하면 고아 객체 제거 기능이 활성화 된다.</p>
<p>이 기능은 참조하는 곳이 하나일 때만 사용해야한다.</p>
<h3 id="영속성-전이--고아-객체">영속성 전이 + 고아 객체</h3>
<p>CascadeType.All + orphanRemoval = true 를 동시에 사용하면 엔티티 스스로 생명주기를 관리한다는 뜻이다.</p>
<p>자식을 저장하려면 부모에 등록하면 되고
자식을 삭제하려면 부모에서 제거하면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[QueryDSL]]></title>
            <link>https://velog.io/@i-no/QueryDSL</link>
            <guid>https://velog.io/@i-no/QueryDSL</guid>
            <pubDate>Wed, 18 May 2022 14:15:41 GMT</pubDate>
            <description><![CDATA[<p>자바 ORM 표준 JPA 프로그래밍
<img src="https://velog.velcdn.com/images/i-no/post/b8cb40a8-0710-4b43-be98-b3beacd3f8f1/image.png" alt="">
<a href="http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&amp;ejkGb=KOR&amp;barcode=9788960777330">http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&amp;ejkGb=KOR&amp;barcode=9788960777330</a></p>
<h3 id="querydsl">QueryDSL</h3>
<p>JPA Creiteria는 너무 복잡하여 어떤 JPQL이 생성될지 파악하기 어렵다.
QueryDSL는 쿼리를 코드로 작성하면서 쉽고 간결하다.</p>
<h3 id="jpaquery-객체-생성">JPAQuery 객체 생성</h3>
<p>QueryDSL을 사용하려면 JPAQuery 객체를 생성해야한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[객체지향 쿼리]]></title>
            <link>https://velog.io/@i-no/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%BF%BC%EB%A6%AC</link>
            <guid>https://velog.io/@i-no/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%BF%BC%EB%A6%AC</guid>
            <pubDate>Tue, 17 May 2022 08:04:42 GMT</pubDate>
            <description><![CDATA[<p>자바 ORM 표준 JPA 프로그래밍
<img src="https://velog.velcdn.com/images/i-no/post/b8cb40a8-0710-4b43-be98-b3beacd3f8f1/image.png" alt="">
<a href="http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&amp;ejkGb=KOR&amp;barcode=9788960777330">http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&amp;ejkGb=KOR&amp;barcode=9788960777330</a></p>
<h3 id="객체지향-쿼리">객체지향 쿼리</h3>
<p>식별자로 조회와 객체 그래프 탐색 만으로는 복잡한 조회가 어렵다.</p>
<p>JPQL과 같은 객체지향 쿼리를 사용하면 복잡한 조회도 가능하다.</p>
<h3 id="jpql">JPQL</h3>
<p>엔티티 객체를 조회하는 객체지향 쿼리다.
SQL을 추상화하여 특정 데이터베이스에 의존하지 않기 때문에 다른 데이터베이스로 바꿔도 방언 설정만 수정하면 JPQL 수정없이 사용가능하다.</p>
<p>엔티티 직접 조회, 묵시적 조인, 다형성 지원으로 SQL보다 간결하다.</p>
<p>em.createQuery(JPQL 쿼리, 엔티티 클래스 타입).getResultList();
로 쿼리를 실행하고 결과를 받을 수 있다.</p>
<h3 id="criteria-쿼리">Criteria 쿼리</h3>
<p>JPQL을 생성하는 빌더 클래스이다.
문자가 아닌 프로그래밍 코드로 JPQL을 작성할 수 있다.</p>
<p>JPQL은 오타가 있어도 컴파일을 성공하고 애플리케이션 서버에 배포되어 런타임에 오류가 발생한다.
Criteria는 코드로 JPQL을 작성하므로 컴파일 시점에 오류를 발견할 수 있다.
또 IDE를 사용하면 코드 자동완성을 지원한다.
동적 쿼리를 작성하기도 편하다.</p>
<p>m.get(&quot;username&quot;) 에서 메타모델을 사용하면 &quot;username&quot;을 문자열이 아닌 코드로 m.get(Member_.username) 이렇게 작성 가능하다.</p>
<p>Criteria가 가진 장점이 많지만 너무 복잡하고 장황하여 사용하기 불편하고 가독성이 떨어진다.</p>
<h3 id="querydsl">QueryDSL</h3>
<p>JPQL 빌더 역할을 한다.
코드 기반이면서 단순하고 사용하기 쉽다.
작성한 코드도 JPQL과 비슷해서 한눈에 들어온다.</p>
<p>JPA 표준은 아니고 오픈소스 프로젝트이다.</p>
<p>어노테이션 프로세서를 사용해서 쿼리 전용 클래스를 만들어줘야한다.</p>
<h3 id="네이티브-sql">네이티브 SQL</h3>
<p>SQL을 직접 사용할 수 있는 기능이다.</p>
<p>특정 데이터베이스만 사용하는 기능이나 JPAL이 지원하지 않는 기능을 사용해야할 때 사용한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 데이터 JPA]]></title>
            <link>https://velog.io/@i-no/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA</link>
            <guid>https://velog.io/@i-no/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA</guid>
            <pubDate>Sun, 15 May 2022 20:59:09 GMT</pubDate>
            <description><![CDATA[<p>자바 ORM 표준 JPA 프로그래밍
<img src="https://velog.velcdn.com/images/i-no/post/b8cb40a8-0710-4b43-be98-b3beacd3f8f1/image.png" alt="">
<a href="http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&amp;ejkGb=KOR&amp;barcode=9788960777330">http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&amp;ejkGb=KOR&amp;barcode=9788960777330</a></p>
<h3 id="genericdao">GenericDAO</h3>
<p>데이터 접근 계층은 CRUD 코드를 반복해서 해야한다.
이때 리포지토리들이 하는 일이 비슷해서 중복된 코드가 생긴다.
이를 해결하기 위해 제네릭과 상속을 사용해서 공통 부분을 처리하는 부모 클래스를 만든다.
이를 GenericDAO라고 한다.</p>
<p>하지만 이 방법은 부모 클래스에 종속되고 구현 클래스 상속이 가지는 단점을 가진다.</p>
<h3 id="스프링-데이터-jpa">스프링 데이터 JPA</h3>
<p>스프링 데이터 JPA는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트다.</p>
<p>CRUD를 처리하기 위한 공통 인터페이스를 제공한다.</p>
<p>리포지토리를 개발할 때 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 스프링 빈으로 등록한다.</p>
<p>findByUserName 같은 메서드도 스프링 데이터 JPA가 메서드 이름을 분석해서 JPQL을 실행한다.</p>
<h3 id="스프링-데이터-프로젝트">스프링 데이터 프로젝트</h3>
<p>스프링 데이터 프로젝트는 다양한 데이터 저장소에 대한 접근을 추상화해서 반복적인 데이터 접근 코드를 줄여준다.</p>
<p>스프링 데이터 JPA는 그 중 하나로 JPA에 특화된 기능을 제공한다.</p>
<h3 id="스프링-데이터-jpa-설정">스프링 데이터 JPA 설정</h3>
<p>책 541P 참고</p>
<p>리포지토리를 검색할 베이스 패키지 설정
라이브러리 설정 등</p>
<h3 id="공통-인터페이스-기능">공통 인터페이스 기능</h3>
<p>스프링 데이터 JPA는 CRUD를 공통으로 처리하는 JpaRepository 인터페이스를 제공한다.</p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {}</code></pre>
<p>이렇게 &lt;&gt; 에 엔티티 클래스와 식별자 타입을 지정해주면 된다.</p>
<p>JpaRepository는 스프링 데이터의 Repository를 상속하는 CrudRepository를 상속하는 PagingAndSortingRepository를 상속한다.</p>
<h4 id="주요-메서드">주요 메서드</h4>
<p>save() : 새로운 엔티티는 저장하고 이미 있는 엔티티는 수정한다.
엔티티에 식별자 값이 null이면 새로운 엔티티로 판단하여 persist를 호출하고 그렇지 않으면 merge를 호출한다.
필요하다면 스프링 데이터 JPA의 기능을 확장하여 판단 전략을 변경할 수 있다.</p>
<p>delete() : 엔티티를 삭제한다. remove를 호출한다.</p>
<p>findOne() : 엔티티 하나를 조회한다. find를 호출한다.</p>
<p>getOne() : 엔티티를 프록시로 조회한다. getReference를 호출한다. </p>
<p>findAll() : 모든 엔티티를 조회한다. 정렬이나 페이징 조건을 파라미터로 제공할 수 있다.</p>
<h3 id="쿼리-메서드-기능">쿼리 메서드 기능</h3>
<p>메서드 이름만으로 쿼리를 생성하는 기능이다.
인터페이스에 메서드만 선언하면 해당 메서드 이름으로 적절한 JPQL 쿼리를 생성해서 실행한다.</p>
<p>스프링 데이터 JPA가 제공하는 쿼리 메서드 기능</p>
<ol>
<li>메서드 이름으로 쿼리 생성</li>
<li>메서드 이름으로 JPA NamedQuery 호출</li>
<li>@Query 어노테이션을 사용해서 리포지토리 인터페이스에 쿼리 직접 정의</li>
</ol>
<p>이 기능들을 활용하면 인터페이스만으로 필요한 대부분의 쿼리 기능을 개발할 수 있다.</p>
<h3 id="메소드-이름으로-쿼리-생성">메소드 이름으로 쿼리 생성</h3>
<p><a href="https://docs.spring.io/spring-data/jpa/docs/2.3.9.RELEASE/reference/html/#jpa.repositories">https://docs.spring.io/spring-data/jpa/docs/2.3.9.RELEASE/reference/html/#jpa.repositories</a></p>
<p><img src="https://velog.velcdn.com/images/i-no/post/e7543a25-b6f4-4387-8a9a-02c1b25178b9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/i-no/post/d446dbb4-b0bb-464b-ac66-a35f8a514ef6/image.png" alt=""></p>
<p>정해진 규칙을 사용해서 메소드 이름으로 쿼리를 생성할 수 있다.</p>
<p>엔티티의 필드명이 변경되면 인터페이스에 정의한 메소드 이름도 변경해줘야한다.</p>
<h3 id="jpa-namedquery">JPA NamedQuery</h3>
<p>쿼리에 이름을 부여해서 사용하는 방법이다.</p>
<p>스프링 데이터 JPA를 사용하지 않으면 어노테이션이나 XML에 쿼리를 정의하고 Named 쿼리를 JPA에서 직접호출 하려면 리포지토리에서 createNamedQuery를 사용해서 정의해줘야한다.</p>
<p>스프링 데이터 JPA를 사용하면 메소드 이름만으로 Named 쿼리를 호출할 수 있다.
만약 실행할 Named 쿼리가 없으면 메소드 이름으로 쿼리 생성 전략을 사용한다.(설정해서 기본 전략 변경가능)</p>
<h3 id="query-리포지토리-메소드에-쿼리-정의">@Query, 리포지토리 메소드에 쿼리 정의</h3>
<p>실행할 메서드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리 느낌이다.
JPA Named 쿼리와 마찬가지로 애플리케이션 실행시점에 문법 오류를 발견할 수 있다.</p>
<p>메서드에 @Query 어노테이션으로 실행할 쿼리를 직접 작성한다.
nativeQuery = true 로 설정하면 네이티브 SQL을 사용할 수 있다.
이때 네이티브 SQL는 위치 기반 파라미터를 0부터 시작하고 JPQL은 1 부터 시작하므로 주의해야한다.</p>
<h3 id="파라미터-바인딩">파라미터 바인딩</h3>
<p>스프링 데이터 JPA는 위치 기반 파라미터 바인딩과 이름 기반 파라미터 바인딩을 모두 지원한다.
기본 값은 위치 기반이다.
코드 가독성과 유지보수를 위해 이름 기반을 사용하는 것이 좋다.</p>
<h3 id="벌크성-수정-쿼리">벌크성 수정 쿼리</h3>
<p>스프링 데이터 JPA에서 벌크성 수정, 삭제 쿼리는 Modifying 어노테이션을 사용하면 된다.</p>
<p>벌크성 쿼리 실행 이후 영속성 컨텍스트를 초기화 하고 싶으면 clearAutomatically = true 옵션을 주면 된다.</p>
<h3 id="반환-타입">반환 타입</h3>
<p>스프링 데이터 JPA는 결과가 한건 이상이면 컬렉션 인터페이스를 단건이면 반환 타입을 지정한다.</p>
<p>조회 결과가 없으면 컬렉션은 빈 컬렉션을 단건은 null을 반환한다.
단건을 기대하고 반환 타입을 지정했는데 결과가 2건 이상 조회되면 예외가 발생한다.</p>
<p>단건으로 지정한 메서드를 호출하면 스프링 데이터 JPA는 내부에서 getStringResult() 메서드를 호출한다.
조회 결과가 없으면 예외가 발생하는데 스프링 데이터 JPA는 이 예외를 무시하고 null을 반환한다.</p>
<h3 id="페이징과-정렬">페이징과 정렬</h3>
<p>파라미터에 Pageable을 사용하면 반환 타입으로 List나 Page를 사용할 수 있다.
Page를 사용하면 스프링 데이터 JPA는 페이징 기능을 제공하기 위해 검색된 전체 데이터 건수를 조회하는 count 쿼리를 추가로 호출한다.</p>
<pre><code class="language-java">PageRequest pageRequest =
    new PageRequest(원하는 페이지, 페이지 당 데이터 수, new Sort(정렬 방식, 정렬 기준));

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

List&lt;Member&gt; members = result.getContent(); // 조회된 데이터
int totalPages = result.getTotalPages(); // 전체 페이지 수
boolean hasNextPage = result.hasNextPage(); // 다음 페이지 존재 여부</code></pre>
<p>이외에도 page 인터페이스는 전체 데이터 수 등등의 메서드를 제공한다.</p>
<h3 id="힌트">힌트</h3>
<p>JPA 쿼리 힌트를 사용하려면 QueryHints 어노테이션을 사용하면 된다.
SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트다.</p>
<h3 id="lock">Lock</h3>
<p>쿼리 식 락을 걸려면 Lock 어노테이션을 사용하면 된다.</p>
<h3 id="명세">명세</h3>
<h3 id="사용자-정의-리포지토리-구현">사용자 정의 리포지토리 구현</h3>
<p>스프링 데이터 JPA는 필요한 메소드만 구현할 수 있는 방법을 제공한다.</p>
<p>직접 구현할 메서드를 위한 사용자 정의 인터페이스를 작성해야한다.
이때 인터페이스 이름은 상관없다.
이후 사용자 정의 인터페이스를 구현한 클래스를 작성해야한다.
이때 구현 클래스 이름은 리포지토리 인터페이스 이름 + Impl 로 작성해야 스프링 데이터 JPA가 사용자 정의 구현 클래스로 인식한다.</p>
<p>이후 리포지토리 인터페이스에서 사용자 정의 인터페이스와 JpaRepository를 상속 받으면 된다.</p>
<h3 id="도메인-클래스-컨버터-기능">도메인 클래스 컨버터 기능</h3>
<p>HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩 해준다.</p>
<p>그러나 이 기능을 통해 넘어온 엔티티를 컨트롤러에서 직접 수정해도 실제 데이터 베이스에는 방영되지 않는다.
OSIV를 사용하지 않으면 조회한 엔티티가 준영속 상태이므로 변경 감지기능이 동작하지 않는다.
수정한 내용을 반영하기 위해서 merge 해줘야한다.
OSIV를 사용하면 조회한 엔티티는 영속 상태이지만 OSIV 특성상 컨트롤러와 뷰에서는 영속성 컨텍스트를 플러시하지 않기때문에 수정한 내용을 데이터베이스에 반영하고 싶으면 트랜잭션을 시작하는 서비스 계층을 호출해야한다.</p>
<h3 id="페이징과-정렬-기능">페이징과 정렬 기능</h3>
<h3 id="스프링-데이터-jpa가-사용하는-구현체">스프링 데이터 JPA가 사용하는 구현체</h3>
<p>스프링 데이터 JPA가 제공하는 공통 인터페이스는 SimpleJpaRepository 클래스가 구현한다.</p>
<p>@Repository
@Transactional(readOnlt = true)
가 적용되어있다.</p>
<h3 id="querydslpredicateexecutor">QueryDslPredicateExecutor</h3>
<p>리포지토리 인터페이스에서 QueryDslPredicateExecutor를 상속받으면 QueryDSL을 사용할 수 있다.</p>
<p>QueryDslPredicateExecutor는 편리하게 QueryDSL을 사용할 수 있지만 join, fetch를 사용할 수 없다는 한계가 있다.</p>
<p>QueryDSL에서 제공하는 다양한 기능을 사용하려면 JPAQuery를 직접 사용하거나 스프링 데이터 JPA가 제공하는 QueryDslRepositorySupport를 사용해야한다.</p>
<h3 id="querydslrepositorysupport">QueryDslRepositorySupport</h3>
<p>QueryDslRepositorySupport을 상속 받으면 조금 더 편리하게 QueryDSL을 사용할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[고급 매핑]]></title>
            <link>https://velog.io/@i-no/%EA%B3%A0%EA%B8%89-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@i-no/%EA%B3%A0%EA%B8%89-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Sun, 15 May 2022 10:54:42 GMT</pubDate>
            <description><![CDATA[<p>자바 ORM 표준 JPA 프로그래밍
<img src="https://velog.velcdn.com/images/i-no/post/b8cb40a8-0710-4b43-be98-b3beacd3f8f1/image.png" alt="">
<a href="http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&amp;ejkGb=KOR&amp;barcode=9788960777330">http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&amp;ejkGb=KOR&amp;barcode=9788960777330</a></p>
<h3 id="상속-관계-매핑">상속 관계 매핑</h3>
<p>ORM에서 이야기하는 상속 관계 매핑은 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것이다.</p>
<p>매핑 방법 3가지</p>
<ol>
<li>조인 전략</li>
</ol>
<p>각각의 테이블로 변환하는 방법이다.
테이블은 타입 개념이 없으므로 타입을 구분하는 컬럼으로 DTYPE 컬럼을 추가해줘야한다.</p>
<p>부모 클래스에 @Inheritance(strategy = InheritanceType.JOINED) 를 붙여서 부모 클래스이며 조인 전략으로 매핑할 것이라고 명시한다.</p>
<p>DTYPE 컬럼에는 @DiscriminatorColumn(name = &quot;DTYPE&quot;)를 붙여서 부모 클래스의 구분 컬럼임을 명시해줘야 한다.
기본 값이 DTYPE 이므로 컬럼명이 DTYPE이면 뒤 괄호는 생략 가능하다.</p>
<p>자식 클래스에 @DiscriminatorValue(&quot;M&quot;) 을 붙여서 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다.</p>
<p>자식 테이블의 기본 키 이름의 기본 값은 부모 테이블의 기본 키 이름이다.
변경하려면 @PrimaryKeyJoinColumn(name = &quot;movie_id&quot;)을 사용하여 변경가능하다.</p>
<h4 id="장점">장점</h4>
<p>테이블이 정규화 된다.
외래 키 참조 무결성 제약조건을 활용할 수 있다.???
저장공간을 효율적으로 사용한다.</p>
<h4 id="단점">단점</h4>
<p>조회할 때 조인이 많이 사용되어 성능 저하될 수 있다.
조회 쿼리가 복잡해진다.
데이터를 등록할 때 INSERT SQL을 두번 실행한다.</p>
<p>JPA 표준 명세는 구분 컬럼을 사용하도록 한다.
하이버네이트를 포함한 몇몇 구현체는 구분 컬럼 없이도 동작하긴 한다.</p>
<ol start="2">
<li>단일 테이블 전략</li>
</ol>
<p>테이블을 하나만 생성하고 자식 테이블의 컬럼을 모두 부모 테이블에 생성하고 사용하지 않는 컬럼은 null을 넣어두는 방법이다.
DTYPE을 구분 컬럼으로 사용하여 어떤 자식 데이터가 저장되었는지 구분 한다.
테이블은 하나만 생성하지만 클래스는 당연히 자식 클래스는 생성해줘야한다.</p>
<p>부모 클래스에 @Inheritance(strategy = InheritanceType.SINGLE_TABLE) 을 붙여서 전략을 명시해줄 수 있다.</p>
<p>DTYPE 컬럼에는 @DiscriminatorColumn(name = &quot;DTYPE&quot;)를 붙여서 부모 클래스의 구분 컬럼임을 명시해줘야 한다.</p>
<p>자식 클래스에 @DiscriminatorValue 를 설정해줘야한다.
안해주면 클래스 이름이 기본 값으로 들어간다.</p>
<h4 id="장점-1">장점</h4>
<p>조인이 필요없어 조회 성능이 빠르다.
조회 쿼리가 단순하다.</p>
<h4 id="단점-1">단점</h4>
<p>자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야한다.
테이블이 커져서 오히려 조회 성능이 느려질 수 있다.</p>
<ol start="3">
<li>구현 클래스마다 테이블 전략</li>
</ol>
<p>자식 엔티티마다 테이블을 만들고 거기에 부모 클래스의 값까지 넣는 방식이다.
구분 컬럼을 사용하지 않는다.
데이터베이스 설계자와 ORM 전문가 둘다 추천하지 않는 전략이다.</p>
<p>부모 클래스에 @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) 을 붙여서 전략을 명시해줄 수 있다.</p>
<h4 id="장점-2">장점</h4>
<p>서브 타입을 구분해서 처리할 때 효과적이다.
not null 제약조건을 사용할 수 있다.</p>
<h4 id="단점-2">단점</h4>
<p>여러 자식 테이블을 함께 조회할 때 성능이 느리다(SQL에 UNION을 사용해야한다.)
자식 테이블을 통합해서 쿼리하기 어렵다.</p>
<h3 id="mappedsuperclass">@MappedSuperclass</h3>
<p>부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속 받는 자식 클래스에게 매핑 정보만 제공하고 싶을때 사용한다.</p>
<p>BaseEntity 만들때 사용한다.
BaseEntity는 직접 생성해서 사용할 일이 없으므로 추상 클래스로 만드는 것이 좋다.</p>
<p>상속받은 매핑 정보를 재정의하려면 @AttributeOverride 를 사용하면 된다.</p>
<h3 id="복합-키와-식별-관계-매핑">복합 키와 식별 관계 매핑</h3>
<h3 id="식별-관계">식별 관계</h3>
<p>부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키로 사용하는 관계이다.</p>
<h3 id="비식별-관계">비식별 관계</h3>
<p>부모 테이블의 기본 키를 자식 테이블에서 외래 키로만 사용하는 관계이다.</p>
<h4 id="필수적-비식별-관계">필수적 비식별 관계</h4>
<p>외래 키에 null을 허용하지 않는다.
연관관계를 필수적으로 맺어야 한다.</p>
<h4 id="선택적-비식별-관계">선택적 비식별 관계</h4>
<p>외래 키에 null을 허용한다.
연관관계를 선택적으로 맺을 수 있다.</p>
<h3 id="복합-키">복합 키</h3>
<p>JPA에서 식별자를 둘 이상 사용하려면 별도의 식별자 클래스를 만들어야 한다.</p>
<p>JPA는 영속성 컨텍스트에 보관한 엔티티를 구분하기 위해 식별자에 equals와 hashCode를 사용해서 동등성 비교를 한다.</p>
<p>식별자 필드가 2개 이상이면 별도의 식별자 클래스를 만들고 그곳에 equals와 hashCode를 오버라이드하여 구현해야한다.</p>
<h4 id="idclassparentidclass">@IdClass(ParentId.class)</h4>
<p>식별자 필드가 2개 이상인 클래스에 식별자 클래스를 설정해준다.</p>
<p>이때 식별자 클래스는
엔티티 클래스와 식별자 속성명이 같아야한다.
Serializable 인터페이스를 구현해야한다.
equals, hashCode를 구현해야 한다.
기본 생성자가 있어야한다.
public 클래스어야 한다.</p>
<p>사용할 때 엔티티를 생성하여 저장하면 자동으로 식별자 클래스를 생성해준다.</p>
<h4 id="embeddedid">@EmbeddedId</h4>
<p>좀 더 객체지향적으로 식별자 클래스를 설정하는 방법이다.</p>
<p>이때 식별자 클래스는
@Embeddable 어노테이션을 붙여주어야 한다.
Serializable 인터페이스를 구현해야한다.
equals, hashCode를 구현해야 한다.
기본 생성자가 있어야한다.
public 클래스어야 한다.</p>
<p>사용할 때 식별자 클래스를 직접 생성하고 이를 이용하여 엔티티를 생성해야한다.</p>
<h4 id="이-방식이-좀-더-객체지향적이고-중복도-없어서-좋지만-특정-상황에-jpql이-조금-더-길어질-수-있다-어떤-상황">이 방식이 좀 더 객체지향적이고 중복도 없어서 좋지만 특정 상황에 JPQL이 조금 더 길어질 수 있다.????? 어떤 상황??</h4>
<h3 id="복합-키를-사용한-식별관계-구현">복합 키를 사용한 식별관계 구현</h3>
<p>ManyToOne 관계에서 식별관계로 외래 키를 가지는 엔티티는 식별자 클래스를 작성해야한다.</p>
<p>그러나 이를 비식별 관계로 구현하면 복합 키 없이 구현가능하여 식별자 클래스를 작성하지 않아도 된다.</p>
<p>OneToOne 식별관계의 경우에는 복합키로 구성하지 않아도 되어 식별자 클래스를 작성하지 않아도 된다.</p>
<h3 id="식별-관계-vs-비식별-관계">식별 관계 vs 비식별 관계</h3>
<p>식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어난다.
때문에 조인할 때 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.
ManyToOne이면 복합 기본 키를 만들어야해서 식별자 클래스를 작성해야한다.
비즈니스에 의미있는 자연 키 컬럼의 경우에는 식별 관계를 사용하기도 한다.
비즈니스 요구사항은 변경될 수 있으므로 이런 키 컬럼이 자식에 손자까지 전파되면 변경하기 힘들다.
따라서 식별 관계는 유연하지 못하다.
하지만 부모의 기본 키를 자식이 가지고 있어 조인 없이 검색이 가능하기도 하다.</p>
<p>비식별관계는 객체지향적 관점에서 선호된다.
ManyToOne일때 복합 기본 키를 사용하지 않아도 되어 식별자 클래스를 작성할 필요가 없다.
기본 키를 주로 대리 키를 사용하여 키 생성이 편하다.
대리 키는 비즈니스와 관련이 없어 상대적으로 유연하다.</p>
<p>선택적 비식별관계는 null을 허용하므로 조인할 때 외부 조인을 사용해야한다.
필수적 비식별관계는 내부 조인만 사용해도 되므로 더 좋다.</p>
<h3 id="조인-테이블">조인 테이블</h3>
<p>데이터베이스 테이블의 연관관계를 설계하는 방법</p>
<ol>
<li>조인 컬럼 사용</li>
<li>조인 테이블 사용</li>
</ol>
<h4 id="조인-컬럼">조인 컬럼</h4>
<p>외래 키를 사용하는 방법이다.
두 테이블의 로우 사이에 관계가 없는 경우 외래 키에 null을 사용해야하므로 null을 허용해야할 수 있다.</p>
<h4 id="조인-테이블-1">조인 테이블</h4>
<p>별도의 테이블에서 외래 키를 저장하여 관계를 맺는 방법이다.</p>
<h4 id="일대일-조인-테이블">일대일 조인 테이블</h4>
<p>조인 테이블의 외래 키 컬럼에 각각 유니크 제약조건을 걸어야한다.</p>
<h4 id="일대다-조인-테이블">일대다 조인 테이블</h4>
<p>일대다 중 다와 관련된 컬럼을 기본 키로 사용하여 유니크 제약조건을 걸어야한다.</p>
<h3 id="다대다-조인-테이블">다대다 조인 테이블</h3>
<p>조인 테이블의 두 컬럼을 합해서 하나의 복합 유니크 제약조건을 걸어야 한다.
조인 테이블에 컬럼을 추가하면 @JoinTable 전략을 사용할 수 없고 별도의 엔티티를 작성해어 조인 테이블과 매핑해야한다.</p>
<h3 id="엔티티-하나에-여러-테이블-매핑">엔티티 하나에 여러 테이블 매핑</h3>
<p>@SecondaryTable 을 하용하여 한 엔티티에 여러 테이블을 매핑할 수 있지만 이 경우 항상 두 테이블을 조회하므로 최적화하기 어렵다.
따라서 테이블당 엔티티를 각각 만들어서 일대일 매핑하는 것을 권장한다.</p>
]]></description>
        </item>
    </channel>
</rss>