<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.5">Jekyll</generator><link href="https://isntyet.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://isntyet.github.io/" rel="alternate" type="text/html" /><updated>2024-05-16T08:42:05+00:00</updated><id>https://isntyet.github.io/feed.xml</id><title type="html">해보고나면 별거아니다</title><subtitle>조재의 개발 노트입니다.</subtitle><author><name>Jo JaeYoung</name></author><entry><title type="html">Netflix DGS로 GraphQL 해보기</title><link href="https://isntyet.github.io/java/Netflix-DGS%EB%A1%9C-GraphQL-%ED%95%B4%EB%B3%B4%EA%B8%B0/" rel="alternate" type="text/html" title="Netflix DGS로 GraphQL 해보기" /><published>2023-05-14T07:30:30+00:00</published><updated>2023-05-14T07:30:30+00:00</updated><id>https://isntyet.github.io/java/Netflix-DGS%EB%A1%9C-GraphQL-%ED%95%B4%EB%B3%B4%EA%B8%B0</id><content type="html" xml:base="https://isntyet.github.io/java/Netflix-DGS%EB%A1%9C-GraphQL-%ED%95%B4%EB%B3%B4%EA%B8%B0/"><![CDATA[<p>DGS는 “Domain Graph Service”의 약어이고, GraphQL 기반 마이크로서비스 아키텍처에서 사용되는 자바 기반 프레임워크인데  회사에서 DGS Federation(여러 개의 GraphQL 서비스를 하나의 GraphQL 엔드포인트로 노출시키는 방식)을 하기 위해 각 MSA 서비스에서 GraphQL 구현을 해야하는 상황이 생겨서 해보게 되었다.</p>

<hr />

<h2 id="의존성-추가하기">의존성 추가하기</h2>

<p>문서를 보면 Spring Boot 3.0 이상에서는 최신버전의 DGS를 사용하면되는데, 나같은 경우는 Spring Boot 2.7을 사용중이라 DGS 5.5.x 를 사용해야 했다.  DGS 6.x 을 사용하려면 Spring Boot 3.0이상을 쓰면 되겠다.</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">plugins</span> <span class="o">{</span>
	<span class="n">id</span> <span class="s1">'com.netflix.dgs.codegen'</span> <span class="n">version</span> <span class="s1">'5.6.0'</span>
<span class="o">}</span>

<span class="n">repositories</span> <span class="o">{</span>
    <span class="n">mavenCentral</span><span class="o">()</span>
<span class="o">}</span>

<span class="n">dependencies</span> <span class="o">{</span>
	<span class="n">implementation</span><span class="o">(</span><span class="n">platform</span><span class="o">(</span><span class="s2">"com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:5.5.1"</span><span class="o">))</span>
  <span class="n">implementation</span><span class="o">(</span><span class="s2">"com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter"</span><span class="o">)</span>
	<span class="n">implementation</span><span class="o">(</span><span class="s2">"com.netflix.graphql.dgs:graphql-dgs-extended-scalars"</span><span class="o">)</span> <span class="c1">// graphql 타입이랑 java type을 맞추기 위해 필요함</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="idea-plugin-추가하기">IDEA Plugin 추가하기</h2>

<p>조금 더 편하게 DGS 개발을 하기 위해 Intellij IDEA를 사용중이라면 아래 두가지 plugin을 설치해주자</p>

<ul>
  <li><a href="https://plugins.jetbrains.com/plugin/8097-graphql">GraphQL</a></li>
  <li><a href="https://plugins.jetbrains.com/plugin/17852-dgs">DGS</a></li>
</ul>

<hr />

<h2 id="schema-추가하기">schema 추가하기</h2>

<p>이제 스키마 파일을 추가해서 스키마들을 정의할건데 <strong><code class="language-plaintext highlighter-rouge">src/main/resources/schema</code></strong> 경로에 .graphqls 확장자로 파일을 생성하고 schema를 정의해주자.</p>

<ul>
  <li>src/main/resources/schema/human.graphql
    <div class="language-graphql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="k">type</span><span class="w"> </span><span class="n">Human</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="n">idx</span><span class="p">:</span><span class="w"> </span><span class="nb">Int</span><span class="p">!</span><span class="w">
      </span><span class="n">name</span><span class="p">:</span><span class="w"> </span><span class="nb">String</span><span class="p">!</span><span class="w">
      </span><span class="n">money</span><span class="p">:</span><span class="w"> </span><span class="nb">Int</span><span class="w">
  </span><span class="p">}</span><span class="w">

  </span><span class="k">type</span><span class="w"> </span><span class="n">Query</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="n">getHumansByName</span><span class="p">(</span><span class="n">name</span><span class="p">:</span><span class="w"> </span><span class="nb">String</span><span class="p">):</span><span class="w"> </span><span class="p">[</span><span class="n">Human</span><span class="p">]</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span></code></pre></div>    </div>

    <p>DGS에서는 .graphql 확장자나 .graphqls 확장자가 크게 차이가 없다고 하니 알아서 선택해서 쓰면 된다.
나는 기존에 Human이라는 Domain이 존재해서 그것을 그대로 type화 하였다.</p>
  </li>
</ul>

<hr />

<h2 id="datafetcher-만들기">DataFetcher 만들기</h2>

<p>위에 의존성 추가에서 plugins에 <code class="language-plaintext highlighter-rouge">com.netflix.dgs.codegen</code> 를 추가했다면 <code class="language-plaintext highlighter-rouge">gradle dgs graphql codegen</code> 항목에 generateJava가 있는 것을 확인 할 수 있다.</p>

<p><img src="/assets/images/Netflix DGS로 GraphQL 해보기/0.png" alt="Netflix DGS로 GraphQL 해보기 0" /></p>

<p>generateJava를 실행하면 build/generated 에 예시 DataFetcher를 제공해주는데</p>

<p><img src="/assets/images/Netflix DGS로 GraphQL 해보기/1.png" alt="Netflix DGS로 GraphQL 해보기 1" /></p>

<p>이렇게 schema파일을 참고하여 codegen된 샘플 코드를 복사해서 작업을 진행해주면 된다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@DgsComponent</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">HumanResolver</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">HumanService</span> <span class="n">humanService</span><span class="o">;</span>

    <span class="nd">@DgsData</span><span class="o">(</span>
            <span class="n">parentType</span> <span class="o">=</span> <span class="s">"Query"</span><span class="o">,</span>
            <span class="n">field</span> <span class="o">=</span> <span class="s">"getHumansByName"</span>
    <span class="o">)</span>
    <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Human</span><span class="o">&gt;</span> <span class="nf">getHumansByName</span><span class="o">(</span><span class="nd">@InputArgument</span> <span class="nc">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">this</span><span class="o">.</span><span class="na">humanService</span><span class="o">.</span><span class="na">getHumansByName</span><span class="o">(</span><span class="n">name</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>sample code에 있는 <strong>DgsDataFetchingEnvironment</strong>를 쓰면 머리아파지니 @InputArgument를 이용해서 파라미터를 구현해주자.</p>

<p>그리고 <code class="language-plaintext highlighter-rouge">@DgsData</code> 는 getGetHumansByName 처럼 method이름을 그대로 쓸 예정이면 <code class="language-plaintext highlighter-rouge">@DgsQuery</code> 로 대체 가능하다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.isntyet.java.practice.human.controller</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">com.isntyet.java.practice.human.application.HumanService</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.isntyet.java.practice.human.domain.Human</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.netflix.graphql.dgs.DgsComponent</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.netflix.graphql.dgs.DgsQuery</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.netflix.graphql.dgs.InputArgument</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">lombok.RequiredArgsConstructor</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">java.util.List</span><span class="o">;</span>

<span class="nd">@DgsComponent</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">HumanResolver</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">HumanService</span> <span class="n">humanService</span><span class="o">;</span>

    <span class="nd">@DgsQuery</span>
    <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Human</span><span class="o">&gt;</span> <span class="nf">getHumansByName</span><span class="o">(</span><span class="nd">@InputArgument</span> <span class="nc">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">this</span><span class="o">.</span><span class="na">humanService</span><span class="o">.</span><span class="na">getHumansByName</span><span class="o">(</span><span class="n">name</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="query-실행-해보기">Query 실행 해보기</h2>

<p>이제 sample 코드 작성이 끝났으니 실행을 해보자.</p>

<p>혹시 실행을 했는데 아래와 같은 에러가 뜬다면</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>An attempt was made to call a method that does not exist. The attempt was made from the following location:
	com.apollographql.federation.graphqljava.Federation.ensureFederationDirectiveDefinitionsExist<span class="o">(</span>Federation.java:194<span class="o">)</span>
The following method did not exist:
	<span class="s1">'graphql.schema.idl.RuntimeWiring graphql.schema.idl.RuntimeWiring.transform(java.util.function.Consumer)'</span>
</code></pre></div></div>

<p>의존성 문제 때문에 그런것이니 gradle.properties에 아래를 추가해보자</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">graphql</span><span class="o">-</span><span class="n">java</span><span class="o">.</span><span class="na">version</span><span class="o">=</span><span class="mf">19.2</span>
</code></pre></div></div>

<p>실행이 되었다면 <a href="http://localhost:8080/graphiql">http://localhost:8080/graphiql</a> 여기로 접속하면 graphql 테스트를 할 수 있는 화면이 뜰 것이다.</p>

<p><img src="/assets/images/Netflix DGS로 GraphQL 해보기/2.png" alt="Netflix DGS로 GraphQL 해보기 2" /></p>

<p>이곳에서 아까 작성했던 getGetHumansByName query를 실행해보자.</p>

<div class="language-graphql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="n">getHumansByName</span><span class="p">(</span><span class="n">name</span><span class="p">:</span><span class="w"> </span><span class="s2">"jojo"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">idx</span><span class="w">
    </span><span class="n">name</span><span class="w">
    </span><span class="n">money</span><span class="w">    
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><img src="/assets/images/Netflix DGS로 GraphQL 해보기/3.png" alt="Netflix DGS로 GraphQL 해보기 3" /></p>

<hr />

<h2 id="mutation-해보기">Mutation 해보기</h2>

<p>한김에 mutation도 해보자.
Human 생성을 해볼건데 기존에 Human을 생성하기 위해서 요구되는 포맷은 아래와 같은데</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">CreateHumanRequest</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">String</span> <span class="n">name</span><span class="o">;</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Integer</span> <span class="n">money</span><span class="o">;</span>

    <span class="nd">@JsonFormat</span><span class="o">(</span><span class="n">pattern</span> <span class="o">=</span> <span class="s">"yyyy-MM-dd"</span><span class="o">)</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">LocalDate</span> <span class="n">birth</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p>해당 dto에 해당하는 input type을 schema에 만들어 주면 된다.</p>

<div class="language-graphql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">input</span><span class="w"> </span><span class="n">CreateHumanInput</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">name</span><span class="p">:</span><span class="w"> </span><span class="nb">String</span><span class="p">!</span><span class="w">
    </span><span class="n">money</span><span class="p">:</span><span class="w"> </span><span class="nb">Int</span><span class="w">
    </span><span class="n">birth</span><span class="p">:</span><span class="w"> </span><span class="n">Date</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="k">type</span><span class="w"> </span><span class="n">Mutation</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">createHuman</span><span class="p">(</span><span class="n">input</span><span class="p">:</span><span class="w"> </span><span class="n">CreateHumanInput</span><span class="p">):</span><span class="w"> </span><span class="n">Human</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>저기서 birth 필드의 <code class="language-plaintext highlighter-rouge">Date</code> type은 그냥 쓰면 graphql에 없는 타입이라 에러가 날텐데
위에서 의존성 추가했던 graphql-dgs-extended-scalars 를 이용하여 Long이나 DateTime 등 java와 mapping되는 여러 type들을 확장할 수 있다.  아래처럼 scalar를 추가해주자.</p>

<div class="language-graphql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">scalar</span><span class="w"> </span><span class="n">Date</span><span class="w">

</span><span class="k">input</span><span class="w"> </span><span class="n">CreateHumanInput</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">name</span><span class="p">:</span><span class="w"> </span><span class="nb">String</span><span class="p">!</span><span class="w">
    </span><span class="n">money</span><span class="p">:</span><span class="w"> </span><span class="nb">Int</span><span class="w">
    </span><span class="n">birth</span><span class="p">:</span><span class="w"> </span><span class="n">Date</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="k">type</span><span class="w"> </span><span class="n">Mutation</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">createHuman</span><span class="p">(</span><span class="n">input</span><span class="p">:</span><span class="w"> </span><span class="n">CreateHumanInput</span><span class="p">):</span><span class="w"> </span><span class="n">Human</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>추가한 mutation에 대한 DataFetcher를 똑같이 추가해주자.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@DgsComponent</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">HumanResolver</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">HumanService</span> <span class="n">humanService</span><span class="o">;</span>

    <span class="nd">@DgsQuery</span>
    <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Human</span><span class="o">&gt;</span> <span class="nf">getHumansByName</span><span class="o">(</span><span class="nd">@InputArgument</span> <span class="nc">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">this</span><span class="o">.</span><span class="na">humanService</span><span class="o">.</span><span class="na">getHumansByName</span><span class="o">(</span><span class="n">name</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="c1">// 여기</span>
    <span class="nd">@DgsMutation</span>
    <span class="kd">public</span> <span class="nc">Human</span> <span class="nf">createHuman</span><span class="o">(</span><span class="nd">@InputArgument</span> <span class="nc">CreateHumanRequest</span> <span class="n">input</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">this</span><span class="o">.</span><span class="na">humanService</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">input</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>이제 다시 앱을 실행하고 mutation 을 작성하고 실행해보면…</p>

<div class="language-graphql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">mutation</span><span class="w"> </span><span class="n">createHuman</span><span class="p">(</span><span class="nv">$input</span><span class="p">:</span><span class="w"> </span><span class="n">CreateHumanInput</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">createHuman</span><span class="p">(</span><span class="n">input</span><span class="p">:</span><span class="w"> </span><span class="nv">$input</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">idx</span><span class="w">
    </span><span class="n">name</span><span class="w">
    </span><span class="n">money</span><span class="w">    
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"input"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"jojojo"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"money"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
    </span><span class="nl">"birth"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1991-02-26"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><img src="/assets/images/Netflix DGS로 GraphQL 해보기/4.png" alt="Netflix DGS로 GraphQL 해보기 4" /></p>

<p>다음과 같이 에러가 난다.  input data를 CreateHumanRequest로 변환하다가 실패한 것이다.  추측하기에 아마 reflection 하여 생성하는데 내가 만든 CreateHumanRequest DTO에 기본 생성자가 없어서 인듯하니 바꿔주자.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Getter</span>
<span class="nd">@Builder</span>
<span class="nd">@AllArgsConstructor</span>
<span class="nd">@NoArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CreateHumanRequest</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">name</span><span class="o">;</span>

    <span class="kd">private</span> <span class="nc">Integer</span> <span class="n">money</span><span class="o">;</span>

    <span class="nd">@JsonFormat</span><span class="o">(</span><span class="n">pattern</span> <span class="o">=</span> <span class="s">"yyyy-MM-dd"</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">LocalDate</span> <span class="n">birth</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p>이제 다시 실행해보면 정상적으로 생성되는 것을 확인 할 수 있다.</p>

<p><img src="/assets/images/Netflix DGS로 GraphQL 해보기/5.png" alt="Netflix DGS로 GraphQL 해보기 5" /></p>

<hr />

<h2 id="custom-scalars">Custom Scalars</h2>

<p>graphql type에서는 java의 LinkedHashSet 같은 타입이 없기 때문에 그럴때는 어떻게 해야하는지 알아보자.
위에서 LocalDate를 사용한 것과 비슷한 맥락인데 <a href="https://github.com/graphql-java/graphql-java-extended-scalars">이곳</a>을 확인해보면 기본적으로 BigDecimal, Long, Url 등과 같은 타입은 만들어진게 있어서 그대로 사용하면 되지만 LinkedHashSet 같은 타입은 따로 구현을 해줘야 한다.</p>

<p><a href="https://netflix.github.io/dgs/scalars/">여기</a>
를 보면 Scalar를 @DgsScalar를 이용해서 커스텀하게 만들 수 있는데 한번 구현해보자.</p>

<ul>
  <li>config/graphql/scalar/LinkedHashSetScalar.java
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@DgsScalar</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"LinkedHashSet"</span><span class="o">)</span>
  <span class="kd">public</span> <span class="kd">class</span> <span class="nc">LinkedHashSetScalar</span> <span class="kd">implements</span> <span class="nc">Coercing</span><span class="o">&lt;</span><span class="nc">LinkedHashSet</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;,</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;&gt;</span> <span class="o">{</span>

      <span class="nd">@Override</span>
      <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="nf">serialize</span><span class="o">(</span><span class="nc">Object</span> <span class="n">value</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">CoercingSerializeException</span> <span class="o">{</span>
          <span class="k">if</span> <span class="o">(</span><span class="n">value</span> <span class="k">instanceof</span> <span class="nc">LinkedHashSet</span><span class="o">)</span> <span class="o">{</span>
              <span class="nc">LinkedHashSet</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">set</span> <span class="o">=</span> <span class="o">(</span><span class="nc">LinkedHashSet</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;)</span> <span class="n">value</span><span class="o">;</span>
              <span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">list</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;();</span>
              <span class="k">for</span> <span class="o">(</span><span class="nc">String</span> <span class="n">element</span> <span class="o">:</span> <span class="n">set</span><span class="o">)</span> <span class="o">{</span>
                  <span class="n">list</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">element</span><span class="o">);</span>
              <span class="o">}</span>
              <span class="k">return</span> <span class="n">list</span><span class="o">;</span>
          <span class="o">}</span>
          <span class="k">return</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;();</span>
      <span class="o">}</span>

      <span class="nd">@Override</span>
      <span class="kd">public</span> <span class="nc">LinkedHashSet</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="nf">parseValue</span><span class="o">(</span><span class="nc">Object</span> <span class="n">input</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">CoercingParseValueException</span> <span class="o">{</span>
          <span class="k">if</span> <span class="o">(</span><span class="n">input</span> <span class="k">instanceof</span> <span class="nc">List</span><span class="o">)</span> <span class="o">{</span>
              <span class="nc">List</span><span class="o">&lt;?&gt;</span> <span class="n">inputList</span> <span class="o">=</span> <span class="o">(</span><span class="nc">List</span><span class="o">&lt;?&gt;)</span> <span class="n">input</span><span class="o">;</span>
              <span class="nc">LinkedHashSet</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">result</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LinkedHashSet</span><span class="o">&lt;&gt;();</span>
              <span class="k">for</span> <span class="o">(</span><span class="nc">Object</span> <span class="n">element</span> <span class="o">:</span> <span class="n">inputList</span><span class="o">)</span> <span class="o">{</span>
                  <span class="k">if</span> <span class="o">(</span><span class="n">element</span> <span class="k">instanceof</span> <span class="nc">String</span><span class="o">)</span> <span class="o">{</span>
                      <span class="n">result</span><span class="o">.</span><span class="na">add</span><span class="o">((</span><span class="nc">String</span><span class="o">)</span> <span class="n">element</span><span class="o">);</span>
                  <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
                      <span class="k">throw</span> <span class="k">new</span> <span class="nf">CoercingParseValueException</span><span class="o">(</span><span class="s">"Invalid input value: "</span> <span class="o">+</span> <span class="n">element</span><span class="o">);</span>
                  <span class="o">}</span>
              <span class="o">}</span>
              <span class="k">return</span> <span class="n">result</span><span class="o">;</span>
          <span class="o">}</span>
          <span class="k">throw</span> <span class="k">new</span> <span class="nf">CoercingParseValueException</span><span class="o">(</span><span class="s">"Invalid input value: "</span> <span class="o">+</span> <span class="n">input</span><span class="o">);</span>
      <span class="o">}</span>

      <span class="nd">@Override</span>
      <span class="kd">public</span> <span class="nc">LinkedHashSet</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="nf">parseLiteral</span><span class="o">(</span><span class="nc">Object</span> <span class="n">input</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">CoercingParseLiteralException</span> <span class="o">{</span>
          <span class="k">if</span> <span class="o">(</span><span class="n">input</span> <span class="k">instanceof</span> <span class="nc">List</span><span class="o">)</span> <span class="o">{</span>
              <span class="nc">List</span><span class="o">&lt;?&gt;</span> <span class="n">inputList</span> <span class="o">=</span> <span class="o">(</span><span class="nc">List</span><span class="o">&lt;?&gt;)</span> <span class="n">input</span><span class="o">;</span>
              <span class="nc">LinkedHashSet</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">result</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LinkedHashSet</span><span class="o">&lt;&gt;();</span>
              <span class="k">for</span> <span class="o">(</span><span class="nc">Object</span> <span class="n">element</span> <span class="o">:</span> <span class="n">inputList</span><span class="o">)</span> <span class="o">{</span>
                  <span class="k">if</span> <span class="o">(</span><span class="n">element</span> <span class="k">instanceof</span> <span class="nc">String</span><span class="o">)</span> <span class="o">{</span>
                      <span class="n">result</span><span class="o">.</span><span class="na">add</span><span class="o">((</span><span class="nc">String</span><span class="o">)</span> <span class="n">element</span><span class="o">);</span>
                  <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
                      <span class="k">throw</span> <span class="k">new</span> <span class="nf">CoercingParseValueException</span><span class="o">(</span><span class="s">"Invalid input value: "</span> <span class="o">+</span> <span class="n">element</span><span class="o">);</span>
                  <span class="o">}</span>
              <span class="o">}</span>
              <span class="k">return</span> <span class="n">result</span><span class="o">;</span>
          <span class="o">}</span>
          <span class="k">throw</span> <span class="k">new</span> <span class="nf">CoercingParseValueException</span><span class="o">(</span><span class="s">"Invalid input value: "</span> <span class="o">+</span> <span class="n">input</span><span class="o">);</span>
      <span class="o">}</span>
  <span class="o">}</span>
</code></pre></div>    </div>

    <p>Coercing 을 implement해서 serialize(내보낼때)와 parseValue,parseLiteral(들어올때)를 구현해주면 된다.  (위 코드는 chatGPT을 이용해서 대충 돌아가게만 일단 만들어서 참고만 하자)</p>
  </li>
  <li>resoures/schema/human.graphql
    <div class="language-graphql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="k">scalar</span><span class="w"> </span><span class="n">Date</span><span class="w">
  </span><span class="k">scalar</span><span class="w"> </span><span class="n">LinkedHashSet</span><span class="w">

  </span><span class="k">type</span><span class="w"> </span><span class="n">Human</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="err">"사람</span><span class="w"> </span><span class="n">ID</span><span class="err">"</span><span class="w">
      </span><span class="n">idx</span><span class="p">:</span><span class="w"> </span><span class="nb">Int</span><span class="p">!</span><span class="w">
      </span><span class="err">"사람</span><span class="w"> </span><span class="err">이름"</span><span class="w">
      </span><span class="n">name</span><span class="p">:</span><span class="w"> </span><span class="nb">String</span><span class="p">!</span><span class="w">
      </span><span class="err">"가진</span><span class="w"> </span><span class="err">돈"</span><span class="w">
      </span><span class="n">money</span><span class="p">:</span><span class="w"> </span><span class="nb">Int</span><span class="w">
      </span><span class="err">"태그"</span><span class="w">
      </span><span class="n">tags</span><span class="p">:</span><span class="w"> </span><span class="n">LinkedHashSet</span><span class="w">
  </span><span class="p">}</span><span class="w">

  </span><span class="k">input</span><span class="w"> </span><span class="n">CreateHumanInput</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="n">name</span><span class="p">:</span><span class="w"> </span><span class="nb">String</span><span class="p">!</span><span class="w">
      </span><span class="n">money</span><span class="p">:</span><span class="w"> </span><span class="nb">Int</span><span class="w">
      </span><span class="n">birth</span><span class="p">:</span><span class="w"> </span><span class="n">Date</span><span class="w">
      </span><span class="n">tags</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="nb">String</span><span class="p">!]</span><span class="w">
  </span><span class="p">}</span><span class="w">

  </span><span class="k">type</span><span class="w"> </span><span class="n">Query</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="n">getHumansByName</span><span class="p">(</span><span class="n">name</span><span class="p">:</span><span class="w"> </span><span class="nb">String</span><span class="p">):</span><span class="w"> </span><span class="p">[</span><span class="n">Human</span><span class="p">]</span><span class="w">
  </span><span class="p">}</span><span class="w">

  </span><span class="k">type</span><span class="w"> </span><span class="n">Mutation</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="n">createHuman</span><span class="p">(</span><span class="n">input</span><span class="p">:</span><span class="w"> </span><span class="n">CreateHumanInput</span><span class="p">):</span><span class="w"> </span><span class="n">Human</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span></code></pre></div>    </div>

    <p><code class="language-plaintext highlighter-rouge">scalar LinkedHashSet</code> 를 선언해주고 테스트할 필드인 tags를 추가해주자. (Human.java엔 이미 tags가 LinkedHashSet 타입으로 추가 되어 있다)</p>
  </li>
  <li>build.gradle
    <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">generateJava</span> <span class="o">{</span>
      <span class="n">typeMapping</span> <span class="o">=</span> <span class="o">[</span>
              <span class="s2">"LinkedHashSet"</span><span class="o">:</span> <span class="s2">"java.util.LinkedHashSet"</span>
      <span class="o">]</span>
  <span class="o">}</span>
</code></pre></div>    </div>

    <p>codegen plugin의 정상적인 동작을 위해서 추가한 type에 대한 mapping을 위와같이 진행해주자.</p>
  </li>
</ul>

<p>이제 query와 mutation을 실행해보면</p>

<ul>
  <li>createHuman
    <div class="language-graphql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="k">mutation</span><span class="w"> </span><span class="n">createHuman</span><span class="p">(</span><span class="nv">$input</span><span class="p">:</span><span class="w"> </span><span class="n">CreateHumanInput</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">createHuman</span><span class="p">(</span><span class="n">input</span><span class="p">:</span><span class="w"> </span><span class="nv">$input</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="n">idx</span><span class="w">
      </span><span class="n">name</span><span class="w">
      </span><span class="n">money</span><span class="w">
      </span><span class="n">tags</span><span class="w">    
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span></code></pre></div>    </div>

    <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"input"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"jojojo"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"money"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
      </span><span class="nl">"birth"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1991-02-26"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"tags"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"backend"</span><span class="p">,</span><span class="w"> </span><span class="s2">"happy"</span><span class="p">]</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span></code></pre></div>    </div>

    <p><img src="/assets/images/Netflix DGS로 GraphQL 해보기/6.png" alt="Netflix DGS로 GraphQL 해보기 6" /></p>
  </li>
  <li>getHuman
    <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="o">{</span>
    <span class="n">getHumansByName</span><span class="o">(</span><span class="nl">name:</span> <span class="s2">"jojojo"</span><span class="o">)</span> <span class="o">{</span>
      <span class="n">idx</span>
      <span class="n">name</span>
      <span class="n">money</span>
      <span class="n">tags</span>
    <span class="o">}</span>
  <span class="o">}</span>
</code></pre></div>    </div>

    <p><img src="/assets/images/Netflix DGS로 GraphQL 해보기/7.png" alt="Netflix DGS로 GraphQL 해보기 7" /></p>
  </li>
</ul>

<p>위와같이 tags필드가 정상적으로 생성도 되고 조회도 되는 것을 확인 할 수 있다.</p>

<hr />

<h2 id="참조">참조</h2>

<ul>
  <li>DGS docs - <a href="https://netflix.github.io/dgs/getting-started/">https://netflix.github.io/dgs/getting-started/</a></li>
  <li>Example repo - <a href="https://github.com/isntyet/java-practice/commit/69384a2d3eedc743775ead19220e9c247470472c">https://github.com/isntyet/java-practice/commit/69384a2d3eedc743775ead19220e9c247470472c</a></li>
</ul>

<hr />

<h2 id="끝">끝.</h2>]]></content><author><name>Jo JaeYoung</name></author><category term="java" /><category term="java" /><category term="graphql" /><category term="dgs" /><category term="spring boot" /><summary type="html"><![CDATA[DGS는 “Domain Graph Service”의 약어이고, GraphQL 기반 마이크로서비스 아키텍처에서 사용되는 자바 기반 프레임워크인데 회사에서 DGS Federation(여러 개의 GraphQL 서비스를 하나의 GraphQL 엔드포인트로 노출시키는 방식)을 하기 위해 각 MSA 서비스에서 GraphQL 구현을 해야하는 상황이 생겨서 해보게 되었다.]]></summary></entry><entry><title type="html">fastexcel로 streaming download 해보기</title><link href="https://isntyet.github.io/java/fastexcel%EB%A1%9C-streaming-download-%ED%95%B4%EB%B3%B4%EA%B8%B0/" rel="alternate" type="text/html" title="fastexcel로 streaming download 해보기" /><published>2023-03-12T07:30:30+00:00</published><updated>2023-03-12T07:30:30+00:00</updated><id>https://isntyet.github.io/java/fastexcel%EB%A1%9C-streaming-download-%ED%95%B4%EB%B3%B4%EA%B8%B0</id><content type="html" xml:base="https://isntyet.github.io/java/fastexcel%EB%A1%9C-streaming-download-%ED%95%B4%EB%B3%B4%EA%B8%B0/"><![CDATA[<p>개발하고있는 서비스에 대량의 데이터를 엑셀로 다운로드해야하는 API가 필요하여 알아보던 중 아래와 같은 목표를 가지고 방법을 찾게되었다.</p>

<ul>
  <li>S3와 같은 별도의 저장소를 사용하지 않기</li>
  <li>다운로드시 임시 파일을 생성하지 않기</li>
  <li>엑셀 파일을 생성할 때 전체 데이터를 통째로 메모리에 올려서 엑셀 파일을 생성하지 않기</li>
  <li>다운로드 클릭시 즉시 다운로드가 시작되게 하기 (파일 다 만들고 다운X)</li>
</ul>

<p>기존에 사용하던 다운로드 방식은 비동기 다운로드 방식으로<br />
사용자가 다운로드 버튼을 누르면 s3에 파일을 비동기로 저장하게 되고<br />
다운로드 요청 목록을 확인 할 수 있는 화면으로 이동하여 s3 다운로드 링크를 이용하여 다운로드 하는 방식인데<br />
사용성이 매우 떨어졌다</p>

<p>위와 같은 이유 등등 때문에 방법을  알아보던중<br />
<strong><a href="https://jaimemin.tistory.com/2191?category=1084044">[SpringBoot + Fastexcel] 대용량 엑셀 생성 및 다운로드</a></strong> <br />
블로그에서 아주 잘 설명이 되어있고 내 목적에 딱 맞아서 따라해보기로 했다</p>

<hr />

<h3 id="구현">구현</h3>

<ul>
  <li>Sample Code (<a href="https://github.com/isntyet/java-practice/commit/8d5b845eccc279c622da8c6d5fe0064a9fb19826">github</a>)
    <ul>
      <li>dependency
        <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">implementation</span> <span class="s1">'org.dhatim:fastexcel:0.14.18'</span>
</code></pre></div>        </div>
      </li>
      <li>Controller
        <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="c1">// get 버전 (간단 버전)</span>
  <span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/download"</span><span class="o">)</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">downloadGet</span><span class="o">(</span><span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
      <span class="n">response</span><span class="o">.</span><span class="na">setContentType</span><span class="o">(</span><span class="s">"application/vnd.ms-excel"</span><span class="o">);</span>
      <span class="n">response</span><span class="o">.</span><span class="na">setCharacterEncoding</span><span class="o">(</span><span class="s">"utf-8"</span><span class="o">);</span>
      <span class="nc">String</span> <span class="n">fileNameUtf8</span> <span class="o">=</span> <span class="nc">URLEncoder</span><span class="o">.</span><span class="na">encode</span><span class="o">(</span><span class="s">"FAST_EXCEL"</span><span class="o">,</span> <span class="nc">StandardCharsets</span><span class="o">.</span><span class="na">UTF_8</span><span class="o">);</span>
      <span class="n">response</span><span class="o">.</span><span class="na">setHeader</span><span class="o">(</span><span class="s">"Content-Disposition"</span><span class="o">,</span> <span class="s">"attachment; filename="</span> <span class="o">+</span> <span class="n">fileNameUtf8</span> <span class="o">+</span> <span class="s">".xlsx"</span><span class="o">);</span>
      
      <span class="k">try</span> <span class="o">(</span><span class="nc">OutputStream</span> <span class="n">os</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="na">getOutputStream</span><span class="o">())</span> <span class="o">{</span>
          <span class="nc">Workbook</span> <span class="n">wb</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Workbook</span><span class="o">(</span><span class="n">os</span><span class="o">,</span> <span class="s">"PracticeApplication"</span><span class="o">,</span> <span class="s">"1.0"</span><span class="o">);</span>
      
          <span class="nc">Worksheet</span> <span class="n">ws</span> <span class="o">=</span> <span class="n">wb</span><span class="o">.</span><span class="na">newWorksheet</span><span class="o">(</span><span class="s">"home"</span><span class="o">);</span>
          <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">500000</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
              <span class="n">ws</span><span class="o">.</span><span class="na">value</span><span class="o">(</span><span class="n">i</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="s">"No."</span><span class="o">);</span>
              <span class="n">ws</span><span class="o">.</span><span class="na">value</span><span class="o">(</span><span class="n">i</span><span class="o">,</span> <span class="mi">1</span><span class="o">,</span> <span class="s">"첫번 째 칼럼 "</span> <span class="o">+</span> <span class="n">i</span><span class="o">);</span>
              <span class="n">ws</span><span class="o">.</span><span class="na">value</span><span class="o">(</span><span class="n">i</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="s">"두번 째 칼럼"</span><span class="o">);</span>
              <span class="n">ws</span><span class="o">.</span><span class="na">value</span><span class="o">(</span><span class="n">i</span><span class="o">,</span> <span class="mi">3</span><span class="o">,</span> <span class="n">i</span> <span class="o">+</span> <span class="s">"세번 째 칼럼"</span><span class="o">);</span>
      
              <span class="k">if</span> <span class="o">(</span><span class="n">i</span> <span class="o">%</span> <span class="mi">100</span> <span class="o">==</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span>
                  <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"flush 중 "</span> <span class="o">+</span> <span class="n">i</span><span class="o">);</span>
                  <span class="n">ws</span><span class="o">.</span><span class="na">flush</span><span class="o">();</span>
              <span class="o">}</span>
          <span class="o">}</span>
      
          <span class="n">ws</span><span class="o">.</span><span class="na">flush</span><span class="o">();</span>
          <span class="n">ws</span><span class="o">.</span><span class="na">finish</span><span class="o">();</span>
      
          <span class="n">wb</span><span class="o">.</span><span class="na">finish</span><span class="o">();</span>
      <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">IOException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
          <span class="n">log</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"[fastexcel] ERROR {}"</span><span class="o">,</span> <span class="n">e</span><span class="o">.</span><span class="na">getMessage</span><span class="o">());</span>
      <span class="o">}</span>
  <span class="o">}</span>
      
  <span class="c1">// post 버전</span>
  <span class="nd">@PostMapping</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"/download"</span><span class="o">)</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">downloadPost</span><span class="o">(</span>
          <span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span>
          <span class="nc">HomeFilter</span> <span class="n">filter</span>
  <span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
      <span class="n">response</span><span class="o">.</span><span class="na">setContentType</span><span class="o">(</span><span class="s">"application/vnd.ms-excel"</span><span class="o">);</span>
      <span class="n">response</span><span class="o">.</span><span class="na">setCharacterEncoding</span><span class="o">(</span><span class="s">"utf-8"</span><span class="o">);</span>
      <span class="nc">String</span> <span class="n">fileNameUtf8</span> <span class="o">=</span> <span class="nc">URLEncoder</span><span class="o">.</span><span class="na">encode</span><span class="o">(</span><span class="s">"FAST_EXCEL"</span><span class="o">,</span> <span class="nc">StandardCharsets</span><span class="o">.</span><span class="na">UTF_8</span><span class="o">);</span>
      <span class="n">response</span><span class="o">.</span><span class="na">setHeader</span><span class="o">(</span><span class="s">"Content-Disposition"</span><span class="o">,</span> <span class="s">"attachment; filename="</span> <span class="o">+</span> <span class="n">fileNameUtf8</span> <span class="o">+</span> <span class="s">".xlsx"</span><span class="o">);</span>
      
      <span class="k">try</span> <span class="o">(</span><span class="nc">OutputStream</span> <span class="n">os</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="na">getOutputStream</span><span class="o">())</span> <span class="o">{</span>
          <span class="k">this</span><span class="o">.</span><span class="na">homeService</span><span class="o">.</span><span class="na">excelDownload</span><span class="o">(</span><span class="n">os</span><span class="o">,</span> <span class="n">filter</span><span class="o">);</span>
      <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">IOException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
          <span class="n">log</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"error {}"</span><span class="o">,</span> <span class="n">e</span><span class="o">.</span><span class="na">getMessage</span><span class="o">());</span>
      <span class="o">}</span>
  <span class="o">}</span>
</code></pre></div>        </div>
      </li>
      <li>Service
        <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@Transactional</span><span class="o">(</span><span class="n">readOnly</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">excelDownload</span><span class="o">(</span><span class="nc">OutputStream</span> <span class="n">os</span><span class="o">,</span> <span class="nc">HomeFilter</span> <span class="n">filter</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
      <span class="kd">final</span> <span class="kt">int</span> <span class="no">FLUSH_SIZE</span> <span class="o">=</span> <span class="mi">100</span><span class="o">;</span>
      <span class="nc">Workbook</span> <span class="n">wb</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Workbook</span><span class="o">(</span><span class="n">os</span><span class="o">,</span> <span class="s">"PracticeApplication"</span><span class="o">,</span> <span class="s">"1.0"</span><span class="o">);</span>
      
      <span class="nc">Worksheet</span> <span class="n">ws</span> <span class="o">=</span> <span class="n">wb</span><span class="o">.</span><span class="na">newWorksheet</span><span class="o">(</span><span class="s">"home"</span><span class="o">);</span>
      <span class="n">ws</span><span class="o">.</span><span class="na">value</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="s">"No."</span><span class="o">);</span>
      <span class="n">ws</span><span class="o">.</span><span class="na">value</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="mi">1</span><span class="o">,</span> <span class="s">"이름"</span><span class="o">);</span>
      <span class="n">ws</span><span class="o">.</span><span class="na">value</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="s">"주소"</span><span class="o">);</span>
      <span class="n">ws</span><span class="o">.</span><span class="na">value</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="mi">3</span><span class="o">,</span> <span class="s">"가격"</span><span class="o">);</span>
      
      <span class="kt">int</span> <span class="n">row</span> <span class="o">=</span> <span class="mi">1</span><span class="o">;</span>
      <span class="kt">int</span> <span class="n">page</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
      
      <span class="k">while</span> <span class="o">(</span><span class="kc">true</span><span class="o">)</span> <span class="o">{</span>
          <span class="kt">var</span> <span class="n">homes</span> <span class="o">=</span> <span class="n">homeRepository</span><span class="o">.</span><span class="na">findAllHomesByFilterWithPage</span><span class="o">(</span>
                  <span class="n">filter</span><span class="o">,</span>
                  <span class="nc">PageRequest</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">page</span><span class="o">,</span> <span class="mi">10000</span><span class="o">)</span>
          <span class="o">);</span>
      
          <span class="k">if</span> <span class="o">(</span><span class="n">homes</span><span class="o">.</span><span class="na">getContent</span><span class="o">().</span><span class="na">size</span><span class="o">()</span> <span class="o">==</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span>
              <span class="k">break</span><span class="o">;</span>
          <span class="o">}</span>
      
          <span class="k">for</span> <span class="o">(</span><span class="nc">Home</span> <span class="n">home</span> <span class="o">:</span> <span class="n">homes</span><span class="o">)</span> <span class="o">{</span>
              <span class="n">ws</span><span class="o">.</span><span class="na">value</span><span class="o">(</span><span class="n">row</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">row</span><span class="o">);</span>
              <span class="n">ws</span><span class="o">.</span><span class="na">value</span><span class="o">(</span><span class="n">row</span><span class="o">,</span> <span class="mi">1</span><span class="o">,</span> <span class="n">home</span><span class="o">.</span><span class="na">getName</span><span class="o">());</span>
              <span class="n">ws</span><span class="o">.</span><span class="na">value</span><span class="o">(</span><span class="n">row</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="n">home</span><span class="o">.</span><span class="na">getAddress</span><span class="o">());</span>
              <span class="n">ws</span><span class="o">.</span><span class="na">value</span><span class="o">(</span><span class="n">row</span><span class="o">,</span> <span class="mi">3</span><span class="o">,</span> <span class="n">home</span><span class="o">.</span><span class="na">getPrice</span><span class="o">());</span>
      
              <span class="k">if</span> <span class="o">(++</span><span class="n">row</span> <span class="o">%</span> <span class="no">FLUSH_SIZE</span> <span class="o">==</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span>
                  <span class="n">ws</span><span class="o">.</span><span class="na">flush</span><span class="o">();</span>
              <span class="o">}</span>
          <span class="o">}</span>
      
          <span class="n">ws</span><span class="o">.</span><span class="na">flush</span><span class="o">();</span>
          <span class="n">ws</span><span class="o">.</span><span class="na">finish</span><span class="o">();</span>
      
          <span class="n">page</span><span class="o">++;</span>
      <span class="o">}</span>
      
      <span class="n">wb</span><span class="o">.</span><span class="na">finish</span><span class="o">();</span>
  <span class="o">}</span>
</code></pre></div>        </div>
      </li>
      <li>Test HTML
        <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="cp">&lt;!DOCTYPE html&gt;</span>
  <span class="nt">&lt;html</span> <span class="na">lang=</span><span class="s">"en"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;head&gt;</span>
      <span class="nt">&lt;meta</span> <span class="na">charset=</span><span class="s">"UTF-8"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;title&gt;</span>EXCEL DOWNLOAD<span class="nt">&lt;/title&gt;</span>
  <span class="nt">&lt;/head&gt;</span>
  <span class="nt">&lt;body&gt;</span>
      
  <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"contents_wrap"</span><span class="nt">&gt;</span>
      
      <span class="nt">&lt;div&gt;</span>
          <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"button"</span> <span class="na">value=</span><span class="s">"fastexcel get windowopen"</span> <span class="na">onclick=</span><span class="s">"fastexcelWindowOpen()"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;/div&gt;</span>
      <span class="nt">&lt;br/&gt;</span>
      
      <span class="nt">&lt;form</span> <span class="na">action=</span><span class="s">"http://localhost:8080/home/download"</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">&gt;</span>
          <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">name=</span><span class="s">"name"</span> <span class="na">value=</span><span class="s">"name"</span><span class="nt">&gt;</span>
          <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"submit"</span> <span class="na">value=</span><span class="s">"fastexcel post form with filter"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;/form&gt;</span>
      <span class="nt">&lt;br/&gt;</span>
  <span class="nt">&lt;/div&gt;</span>
      
  <span class="nt">&lt;script
          </span><span class="na">src=</span><span class="s">"https://code.jquery.com/jquery-3.6.1.js"</span>
          <span class="na">integrity=</span><span class="s">"sha256-3zlB5s2uwoUzrXK3BT7AX3FyvojsraNFxCc2vC/7pNI="</span>
          <span class="na">crossorigin=</span><span class="s">"anonymous"</span><span class="nt">&gt;&lt;/script&gt;</span>
  <span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"text/javascript"</span><span class="nt">&gt;</span>
      
      <span class="kd">function</span> <span class="nx">fastexcelWindowOpen</span><span class="p">()</span> <span class="p">{</span>
          <span class="kd">var</span> <span class="nx">excelDownloadUrl</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">http://localhost:8080/home/download</span><span class="dl">"</span><span class="p">;</span>
          <span class="nb">window</span><span class="p">.</span><span class="nx">open</span><span class="p">(</span><span class="nx">excelDownloadUrl</span><span class="p">,</span> <span class="dl">"</span><span class="s2">_self</span><span class="dl">"</span><span class="p">,</span> <span class="dl">'</span><span class="s1">width=200, height=200, left=2000, top=2000</span><span class="dl">'</span><span class="p">);</span>
      <span class="p">}</span>
      
  <span class="nt">&lt;/script&gt;</span>
  <span class="nt">&lt;/body&gt;</span>
  <span class="nt">&lt;/html&gt;</span>
</code></pre></div>        </div>
      </li>
    </ul>
  </li>
</ul>

<p>다른 자세한 코드는 <a href="https://github.com/isntyet/java-practice/commit/8d5b845eccc279c622da8c6d5fe0064a9fb19826">여기</a>를 확인 하면 된다</p>

<p><img src="/assets/images/fastexcel로 streaming download 해보기/0.png" alt="fastexcel로 streaming download 해보기/0.png" /></p>

<hr />

<h2 id="다운로드-해보기">다운로드 해보기</h2>

<p>이제 다운로드 버튼을 눌러 실행해보면 실행과 동시에 다운로드가 즉시 진행되며,<br />
쿼리도 내가 정한 사이즈만큼 실행되어 즉시 cell을 만들고 지정한 flush 사이즈 만큼씩 내보내는것을 확인 할 수 있다</p>

<ul>
  <li>
    <p>다운로드 버튼을 누르는 즉시 다운로드 진행하는 모습
<img src="/assets/images/fastexcel로 streaming download 해보기/1.png" alt="fastexcel로 streaming download 해보기/1.png" /></p>
  </li>
  <li>
    <p>다운로드 중간에 paging 만큼 계속 쿼리가 실행되는 모습
<img src="/assets/images/fastexcel로 streaming download 해보기/2.png" alt="fastexcel로 streaming download 해보기/2.png" /></p>
  </li>
</ul>

<hr />

<h2 id="추가-문제">추가 문제</h2>

<p>해당 코드를 실제 운영에서 반영하는데 다운로드가 즉시 진행되지 않고,<br />
파일이 다 만들어지고 나서 다운로드가 진행되는 현상이 나타났다</p>

<ul>
  <li>
    <p>즉시 다운로드되는 버전의 Response Header
<img src="/assets/images/fastexcel로 streaming download 해보기/3.png" alt="fastexcel로 streaming download 해보기/3.png" /></p>
  </li>
  <li>
    <p>즉시 다운로드 되지 않는 버전의 Response Header
<img src="/assets/images/fastexcel로 streaming download 해보기/4.png" alt="fastexcel로 streaming download 해보기/4.png" /></p>
  </li>
</ul>

<p>즉시 다운로드 되는 버전의 Response Header를 보면 Transfer-Encoding가 chunked고<br />
안되는 버전은 Transfer-Encoding이 없는 대신에 Content-Length가 있다</p>

<p>안되는 버전에서 코드를 소거해보면서 확인한 결과 안되는 버전에서만 있던 아래 코드가 문제였다</p>

<ul>
  <li>CacheRequestFilter
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@Component</span>
  <span class="kd">public</span> <span class="kd">class</span> <span class="nc">CacheRequestFilter</span> <span class="kd">extends</span> <span class="nc">OncePerRequestFilter</span> <span class="o">{</span>
      <span class="nd">@Override</span>
      <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">doFilterInternal</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span> <span class="nc">FilterChain</span> <span class="n">filterChain</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">ServletException</span><span class="o">,</span> <span class="nc">IOException</span> <span class="o">{</span>
          <span class="nc">ContentCachingRequestWrapper</span> <span class="n">wrappingRequest</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ContentCachingRequestWrapper</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
          <span class="nc">ContentCachingResponseWrapper</span> <span class="n">wrappingResponse</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ContentCachingResponseWrapper</span><span class="o">(</span><span class="n">response</span><span class="o">);</span>
    
          <span class="n">filterChain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">wrappingRequest</span><span class="o">,</span> <span class="n">wrappingResponse</span><span class="o">);</span>
    
          <span class="n">wrappingResponse</span><span class="o">.</span><span class="na">copyBodyToResponse</span><span class="o">();</span>
      <span class="o">}</span>
  <span class="o">}</span>
</code></pre></div>    </div>
  </li>
</ul>

<p>Request와 Response 로깅을 위해 캐싱하려고 만든 RequestFilter가 문제였던 것이다</p>

<p><code class="language-plaintext highlighter-rouge">ContentCachingResponseWrapper</code> 에서 response를 wrapping하는데 이때 OutPutStream을 <code class="language-plaintext highlighter-rouge">ResponseServletOutputStream</code> 을 사용한다</p>

<ul>
  <li>ResponseServletOutputStream
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">private</span> <span class="kd">class</span> <span class="nc">ResponseServletOutputStream</span> <span class="kd">extends</span> <span class="nc">ServletOutputStream</span> <span class="o">{</span>
    
  		<span class="kd">private</span> <span class="kd">final</span> <span class="nc">ServletOutputStream</span> <span class="n">os</span><span class="o">;</span>
    
  		<span class="kd">public</span> <span class="nf">ResponseServletOutputStream</span><span class="o">(</span><span class="nc">ServletOutputStream</span> <span class="n">os</span><span class="o">)</span> <span class="o">{</span>
  			<span class="k">this</span><span class="o">.</span><span class="na">os</span> <span class="o">=</span> <span class="n">os</span><span class="o">;</span>
  		<span class="o">}</span>
    
  		<span class="nd">@Override</span>
  		<span class="kd">public</span> <span class="kt">void</span> <span class="nf">write</span><span class="o">(</span><span class="kt">int</span> <span class="n">b</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
  			<span class="n">content</span><span class="o">.</span><span class="na">write</span><span class="o">(</span><span class="n">b</span><span class="o">);</span>
  		<span class="o">}</span>
    
  		<span class="nd">@Override</span>
  		<span class="kd">public</span> <span class="kt">void</span> <span class="nf">write</span><span class="o">(</span><span class="kt">byte</span><span class="o">[]</span> <span class="n">b</span><span class="o">,</span> <span class="kt">int</span> <span class="n">off</span><span class="o">,</span> <span class="kt">int</span> <span class="n">len</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
  			<span class="n">content</span><span class="o">.</span><span class="na">write</span><span class="o">(</span><span class="n">b</span><span class="o">,</span> <span class="n">off</span><span class="o">,</span> <span class="n">len</span><span class="o">);</span>
  		<span class="o">}</span>
    		 
  		<span class="o">...</span>
  	<span class="o">}</span>
</code></pre></div>    </div>
  </li>
</ul>

<p>뭔가 content에 write하고 있는데 ContentCachingResponseWrapper 자체가 content 캐싱을 위한거니 content가 완성 되어야 캐싱이 가능하기 때문에 모아서 content가 완성된 후에 내보내기 때문에 즉시 streming이 안된 것으로 생각된다</p>

<p>해당 문제를 해결하기 위해 OutputStream을 기존 HttpServletResponse의 OutputStream을 가져오게 Controller를 수정하였다 (어차피 파일 다운로드는 content 캐싱이 필요 없으니까)</p>

<ul>
  <li>Controller
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@PostMapping</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"/download"</span><span class="o">)</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">downloadPost</span><span class="o">(</span>
          <span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span>
          <span class="nc">HomeFilter</span> <span class="n">filter</span>
  <span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span> <span class="o">{</span>
      <span class="n">response</span><span class="o">.</span><span class="na">setContentType</span><span class="o">(</span><span class="s">"application/vnd.ms-excel"</span><span class="o">);</span>
      <span class="n">response</span><span class="o">.</span><span class="na">setCharacterEncoding</span><span class="o">(</span><span class="s">"utf-8"</span><span class="o">);</span>
      <span class="nc">String</span> <span class="n">fileNameUtf8</span> <span class="o">=</span> <span class="nc">URLEncoder</span><span class="o">.</span><span class="na">encode</span><span class="o">(</span><span class="s">"FAST_EXCEL"</span><span class="o">,</span> <span class="nc">StandardCharsets</span><span class="o">.</span><span class="na">UTF_8</span><span class="o">);</span>
      <span class="n">response</span><span class="o">.</span><span class="na">setHeader</span><span class="o">(</span><span class="s">"Content-Disposition"</span><span class="o">,</span> <span class="s">"attachment; filename="</span> <span class="o">+</span> <span class="n">fileNameUtf8</span> <span class="o">+</span> <span class="s">".xlsx"</span><span class="o">);</span>
    
  <span class="c1">//  try (OutputStream os = response.getOutputStream()) { // 기존 코드</span>
  <span class="c1">// 아래는 새로운 코드</span>
      <span class="k">try</span> <span class="o">(</span><span class="nc">OutputStream</span> <span class="n">os</span> <span class="o">=</span> <span class="o">((</span><span class="nc">ContentCachingResponseWrapper</span><span class="o">)</span> <span class="n">response</span><span class="o">).</span><span class="na">getResponse</span><span class="o">().</span><span class="na">getOutputStream</span><span class="o">())</span> <span class="o">{</span> 
          <span class="k">this</span><span class="o">.</span><span class="na">homeService</span><span class="o">.</span><span class="na">excelDownload</span><span class="o">(</span><span class="n">os</span><span class="o">,</span> <span class="n">filter</span><span class="o">);</span>
      <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">IOException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
          <span class="n">log</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"error {}"</span><span class="o">,</span> <span class="n">e</span><span class="o">.</span><span class="na">getMessage</span><span class="o">());</span>
      <span class="o">}</span>
  <span class="o">}</span>
</code></pre></div>    </div>
  </li>
</ul>

<p>나 같이 ContentCachingResponseWrapper를 사용하는 곳에서만 사용하도록 주의하자<br />
(casting 에러 나니까)</p>

<p>위와 같이 즉시 다운로드 진행이 되지 않는다면<br />
중간에 response를 조작하는 코드가 없는지 확인하도록 하자</p>

<hr />

<h2 id="reference">Reference</h2>

<ul>
  <li>fastexcel - <a href="https://github.com/dhatim/fastexcel">https://github.com/dhatim/fastexcel</a></li>
  <li>fastexcel 사용기 - <a href="https://jaimemin.tistory.com/2191?category=1084044">https://jaimemin.tistory.com/2191?category=1084044</a></li>
  <li>fastexcel 사용 예제 - <a href="https://github.dev/jaimemin/SampleExcelDownloadProject">https://github.dev/jaimemin/SampleExcelDownloadProject</a></li>
  <li>node로 streaming download 구현 - <a href="https://d2.naver.com/helloworld/9423440">https://d2.naver.com/helloworld/9423440</a></li>
</ul>

<hr />

<h2 id="끝">끝.</h2>]]></content><author><name>Jo JaeYoung</name></author><category term="java" /><category term="java" /><category term="excel" /><category term="fastexcel" /><category term="download" /><category term="spring boot" /><summary type="html"><![CDATA[개발하고있는 서비스에 대량의 데이터를 엑셀로 다운로드해야하는 API가 필요하여 알아보던 중 아래와 같은 목표를 가지고 방법을 찾게되었다.]]></summary></entry><entry><title type="html">JPA Custom Repository 클래스 이름 문제</title><link href="https://isntyet.github.io/jpa/JPA-Custom-Repository-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%9D%B4%EB%A6%84-%EB%AC%B8%EC%A0%9C/" rel="alternate" type="text/html" title="JPA Custom Repository 클래스 이름 문제" /><published>2023-03-05T07:30:30+00:00</published><updated>2023-03-05T07:30:30+00:00</updated><id>https://isntyet.github.io/jpa/JPA-Custom-Repository-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%9D%B4%EB%A6%84-%EB%AC%B8%EC%A0%9C</id><content type="html" xml:base="https://isntyet.github.io/jpa/JPA-Custom-Repository-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%9D%B4%EB%A6%84-%EB%AC%B8%EC%A0%9C/"><![CDATA[<p>QueryDSL 사용을 위해서 JPA Custom Repository를 만드는데 아래와 같은 에러가 났다.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">springframework.data.repository.query.QueryCreationException: 
Could not create query for public abstract java.util.List com.isntyet.java.practice.home.domain.HomeRepositoryCustom.findAllHomesByFilter(java.lang.String); 
Reason: Failed to create query for method public abstract java.util.List com.isntyet.java.practice.home.domain.HomeRepositoryCustom.findAllHomesByFilter(java.lang.String)! No property 'filter' found for type 'Home'; 
nested exception is java.lang.IllegalArgumentException: Failed to create query for method public abstract java.util.List com.isntyet.java.practice.home.domain.HomeRepositoryCustom.findAllHomesByFilter(java.lang.String)! No property 'filter' found for type 'Home'
</span></code></pre></div></div>

<hr />

<h2 id="현재-상태--원인">현재 상태 &amp; 원인</h2>

<p>내가 구현한 CustomRepository를 아래와 같이 평범했다.</p>

<ul>
  <li>HomeRepository
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">public</span> <span class="kd">interface</span> <span class="nc">HomeRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">Home</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;,</span> <span class="nc">HomeRepositoryCustom</span> <span class="o">{</span>
      <span class="nc">Home</span> <span class="nf">findByName</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">);</span>
  <span class="o">}</span>
</code></pre></div>    </div>
  </li>
  <li>HomeRepositoryCustom
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">public</span> <span class="kd">interface</span> <span class="nc">HomeRepositoryCustom</span> <span class="o">{</span>
      <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Home</span><span class="o">&gt;</span> <span class="nf">findAllHomesByFilter</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">);</span>
  <span class="o">}</span>
</code></pre></div>    </div>
  </li>
  <li>HomeRepositoryCustomImpl
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@RequiredArgsConstructor</span>
  <span class="nd">@Repository</span>
  <span class="kd">public</span> <span class="kd">class</span> <span class="nc">HomeRepositoryCustomImpl</span> <span class="kd">implements</span> <span class="nc">HomeRepositoryCustom</span> <span class="o">{</span>
      <span class="kd">private</span> <span class="kd">final</span> <span class="nc">JPAQueryFactory</span> <span class="n">queryFactory</span><span class="o">;</span>
    
      <span class="nd">@Override</span>
      <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Home</span><span class="o">&gt;</span> <span class="nf">findAllHomesByFilter</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
          <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
      <span class="o">}</span>
  <span class="o">}</span>
</code></pre></div>    </div>
  </li>
</ul>

<p><img src="/assets/images/JPA Custom Repository 클래스 이름 문제/0.png" alt="JPA Custom Repository 클래스 이름 문제/0.png" /></p>

<p>위의 <a href="https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.single-repository-behavior">4.6.1 Customizing Individual Repositories</a> 에서 말했듯이
Custom Repository 구현체 클래스 뒤에 ~Impl 만 붙여주면 될 줄 알았다.</p>

<p>그래서 <strong>HomeRepositoryCustomImpl</strong> 라고 대충 이름 지어버린게 문제였다.</p>

<hr />

<h2 id="해결">해결</h2>

<p>구현체의 이름은 Repository Interface 이름 그대로 뒤에 Impl을 붙여야 한다.
HomeRepository<strong>Custom</strong>Impl 처럼 가운데에 Custom이라는 이름이 들어가버려서 생긴 문제였다.</p>

<p>HomeRepositoryCustomImpl → <strong>HomeRepositoryImpl</strong> 이렇게 수정해주니 문제는 해결되었다.</p>

<hr />

<h2 id="끝">끝.</h2>]]></content><author><name>Jo JaeYoung</name></author><category term="jpa" /><category term="jpa" /><category term="custom repository" /><summary type="html"><![CDATA[QueryDSL 사용을 위해서 JPA Custom Repository를 만드는데 아래와 같은 에러가 났다.]]></summary></entry><entry><title type="html">feign client 사용해보기</title><link href="https://isntyet.github.io/java/feign-client-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0/" rel="alternate" type="text/html" title="feign client 사용해보기" /><published>2022-12-11T01:30:30+00:00</published><updated>2022-12-11T01:30:30+00:00</updated><id>https://isntyet.github.io/java/feign-client-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</id><content type="html" xml:base="https://isntyet.github.io/java/feign-client-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0/"><![CDATA[<p>Feign은 Http Client 도구인데 example을 보는 순간 직관적인 형태에 끌려 사용해보게 되었다.
(RestTemplate, WebClient 등을 쓰다가 Feign을 써보게 되면 Feign만 쓰게될꺼다)
Feign은 인터페이스와 annotation을 이용하여 아주 간단하게 요청을 날릴 수 있다.</p>

<ul>
  <li><a href="https://spring.io/projects/spring-cloud-openfeign">https://spring.io/projects/spring-cloud-openfeign</a></li>
  <li>https://github.com/isntyet/java-practice/commit/23d25624b5042ef32d5be814c82a8367d179dd2a</li>
</ul>

<hr />

<h2 id="시작하기">시작하기</h2>

<p>feign을 사용하기 위해서 의존성을 추가해주자.
Spring Cloud OpenFeign을 사용할꺼다. (옛날에는 Spring Cloud Netflix Feign 였으나, 현재는 오픈소스 프로젝트인 <strong>OpenFeign</strong> 로 변경되고 <strong>Spring Cloud OpenFeign</strong> 에 통합되었다<strong>)</strong></p>

<p>우선 Spring Cloud 관련 패키지들의 버전에 맞는 의존성 자동 설정을 위해
spring-cloud-dependencies 를 사용해주자. 그리고 openfeign dependency도 추가해주자.</p>

<ul>
  <li>build.gradle
    <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="c1">// 참고: gradle 7.2 ,  Spring boot 2.7.2 사용중</span>

  <span class="n">dependencies</span> <span class="o">{</span>
  	<span class="o">.</span>
  	<span class="o">.</span>
      <span class="n">implementation</span> <span class="nf">platform</span><span class="o">(</span><span class="s2">"org.springframework.cloud:spring-cloud-dependencies:2021.0.5"</span><span class="o">)</span>
      <span class="n">implementation</span> <span class="s2">"org.springframework.cloud:spring-cloud-starter-openfeign"</span>
  	<span class="o">.</span>
  	<span class="o">.</span>
  <span class="o">}</span>
</code></pre></div>    </div>

    <p>Spring boot 버전에 맞는 spring-cloud-dependencies 를 써야하는데
  나 같은 경우는 2.7.2 버전을 사용중이니 아래 사진처럼</p>

    <p><img src="/assets/images/feign client 사용해보기/0.png" alt="feign client 사용해보기/0.png" /><br />
  2.7.x 에 해당되니까 2021.0.x 버전대를 사용해야하는데
  2021.0.x 의 제일 최신 버전이 <strong>2021.0.5</strong> 이기 때문에 해당 버전으로 설정해줬다.</p>

    <p>참고 - <a href="https://spring.io/projects/spring-cloud">https://spring.io/projects/spring-cloud</a></p>

    <p>이렇게 되면 내 프로젝트의 OpenFeign 은 3.1.5 버전을 사용하게 되는거다.<br />
  <img src="/assets/images/feign client 사용해보기/1.png" alt="feign client 사용해보기/1.png" /></p>
  </li>
  <li>
    <p>OpenFeign 관련 컴포넌트 스캔을 위해 Application에 <strong>@EnableFeignClients</strong> 를 붙여주자</p>

    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@SpringBootApplication</span>
  <span class="nd">@EnableFeignClients</span> <span class="c1">// 여기</span>
  <span class="kd">public</span> <span class="kd">class</span> <span class="nc">PracticeApplication</span> <span class="o">{</span>

      <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
          <span class="nc">SpringApplication</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="nc">PracticeApplication</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">args</span><span class="o">);</span>

      <span class="o">}</span>
  <span class="o">}</span>
</code></pre></div>    </div>
  </li>
</ul>

<p>Application에 붙이기 싫다면 configration 파일은 만들어서 설정해줘도 된다</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span>
<span class="nd">@EnableFeignClients</span><span class="o">(</span><span class="n">basePackages</span> <span class="o">=</span> <span class="s">"com.isntyet.java.practice"</span><span class="o">)</span>
	<span class="kd">public</span> <span class="kd">class</span> <span class="nc">FeignClientConfig</span> <span class="o">{</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="간단하게-사용해보기">간단하게 사용해보기</h2>

<p>먼저 최대한 간단하게 <a href="https://randomuser.me/documentation">무료 API</a>의 유저를 조회하는 API를 호출해보자.</p>

<p>내가 조회할 외부 API는 아래처럼 <a href="https://randomuser.me/api/?nat=us">https://randomuser.me/api/?nat=us</a> 를 호출하는 것 이다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl --location --request GET 'https://randomuser.me/api/?nat=us'
</code></pre></div></div>

<p>Response 형태는 대략 아래와 같다</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"results"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"gender"</span><span class="p">:</span><span class="w"> </span><span class="s2">"male"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Mr"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"first"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Juan"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"last"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Thomas"</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"location"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"street"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"number"</span><span class="p">:</span><span class="w"> </span><span class="mi">706</span><span class="p">,</span><span class="w">
                    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Brown Terrace"</span><span class="w">
                </span><span class="p">},</span><span class="w">
                </span><span class="nl">"city"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Torrance"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"New Mexico"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"country"</span><span class="p">:</span><span class="w"> </span><span class="s2">"United States"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"postcode"</span><span class="p">:</span><span class="w"> </span><span class="mi">86286</span><span class="p">,</span><span class="w">
                </span><span class="nl">"coordinates"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"latitude"</span><span class="p">:</span><span class="w"> </span><span class="s2">"27.4589"</span><span class="p">,</span><span class="w">
                    </span><span class="nl">"longitude"</span><span class="p">:</span><span class="w"> </span><span class="s2">"-104.4062"</span><span class="w">
                </span><span class="p">},</span><span class="w">
                </span><span class="nl">"timezone"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"offset"</span><span class="p">:</span><span class="w"> </span><span class="s2">"-4:00"</span><span class="p">,</span><span class="w">
                    </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Atlantic Time (Canada), Caracas, La Paz"</span><span class="w">
                </span><span class="p">}</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"juan.thomas@example.com"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"login"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"8e410e9d-5e03-4f61-8bc7-f91acb2b8b77"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"username"</span><span class="p">:</span><span class="w"> </span><span class="s2">"smallladybug981"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"password"</span><span class="p">:</span><span class="w"> </span><span class="s2">"redbull"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"salt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"v6cl7yeX"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"md5"</span><span class="p">:</span><span class="w"> </span><span class="s2">"c8b57e7ceb81ff6fc37fcccea206ea1d"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"sha1"</span><span class="p">:</span><span class="w"> </span><span class="s2">"4c917b2e324f4067076ed1fd472512ef57792ab2"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"sha256"</span><span class="p">:</span><span class="w"> </span><span class="s2">"42f60f0a6a8ad6f23b21c367b418619b226894ee9291c58947150ba139ea18c2"</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"dob"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1979-03-07T07:23:39.525Z"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"age"</span><span class="p">:</span><span class="w"> </span><span class="mi">43</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"registered"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2004-02-16T02:39:27.302Z"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"age"</span><span class="p">:</span><span class="w"> </span><span class="mi">18</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"phone"</span><span class="p">:</span><span class="w"> </span><span class="s2">"(871) 757-4425"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"cell"</span><span class="p">:</span><span class="w"> </span><span class="s2">"(794) 866-7740"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SSN"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"291-54-9615"</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"picture"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"large"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://randomuser.me/api/portraits/men/64.jpg"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"medium"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://randomuser.me/api/portraits/med/men/64.jpg"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"thumbnail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://randomuser.me/api/portraits/thumb/men/64.jpg"</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"nat"</span><span class="p">:</span><span class="w"> </span><span class="s2">"US"</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">],</span><span class="w">
    </span><span class="nl">"info"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"seed"</span><span class="p">:</span><span class="w"> </span><span class="s2">"c5e5e99edd73d274"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"results"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
        </span><span class="nl">"page"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
        </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.4"</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<ul>
  <li>FeignClient 인터페이스 생성
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@FeignClient</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"userClient"</span><span class="o">,</span> <span class="n">url</span> <span class="o">=</span> <span class="s">"https://randomuser.me"</span><span class="o">)</span>
  <span class="kd">public</span> <span class="kd">interface</span> <span class="nc">UserClient</span> <span class="o">{</span>

      <span class="nd">@GetMapping</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"/api/"</span><span class="o">)</span>
      <span class="nc">GetUsersResponse</span> <span class="nf">getUsers</span><span class="o">(</span><span class="nd">@RequestParam</span><span class="o">(</span><span class="s">"nat"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">nation</span><span class="o">);</span>
  <span class="o">}</span>
</code></pre></div>    </div>

    <p><strong>name</strong>: FeignClient의 bean name (다른 FeignClient의 name과 겹치면 안됨)<br />
  <strong>url</strong>: 해당 client의 base url<br />
  <strong>@GetMapping(value = “/api/”)</strong>: RequestMethod와 api Path</p>
  </li>
  <li>Response DTO 생성
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@Getter</span>
  <span class="nd">@AllArgsConstructor</span>
  <span class="nd">@NoArgsConstructor</span>
  <span class="kd">public</span> <span class="kd">class</span> <span class="nc">GetUsersResponse</span> <span class="o">{</span>
      <span class="kd">private</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Result</span><span class="o">&gt;</span> <span class="n">results</span><span class="o">;</span>

      <span class="nd">@Getter</span>
      <span class="nd">@AllArgsConstructor</span>
      <span class="nd">@NoArgsConstructor</span>
      <span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">Result</span> <span class="o">{</span>
          <span class="kd">private</span> <span class="nc">String</span> <span class="n">gender</span><span class="o">;</span>
          <span class="kd">private</span> <span class="nc">String</span> <span class="n">email</span><span class="o">;</span>
      <span class="o">}</span>
  <span class="o">}</span>
</code></pre></div>    </div>

    <p>Response 에 정보는 더 많지만 귀찮으니 gender와 email만 받아오자.</p>
  </li>
</ul>

<p>이렇게 구현후 UserClient.getUsers()를 호출해주면 User 정보를 가져와서 GetUsersResponse에 담아서 사용할 수 있게된다.</p>

<p><img src="/assets/images/feign client 사용해보기/2.png" alt="feign client 사용해보기/2.png" /></p>

<p>아래 위에 구현한 FeignClient 인터페이스를 보면 알겠지만 엄청 간단하다.</p>

<p>그리고 얼핏보면 Controller 정의부분을 닮은 것 같기도하고, JPA Repository 와 비슷하기도 하다.
(밑에 소스를 보면서 비교해보면 매우 눈에 익숙함을 알 수 있다)</p>

<ul>
  <li>controller
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@RestController</span>
  <span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/human"</span><span class="o">)</span>
  <span class="nd">@RequiredArgsConstructor</span>
  <span class="kd">public</span> <span class="kd">class</span> <span class="nc">HumanController</span> <span class="o">{</span>
  	<span class="kd">private</span> <span class="kd">final</span> <span class="nc">HumanService</span> <span class="n">humanService</span><span class="o">;</span>

  	<span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/external-users"</span><span class="o">)</span>
    <span class="kd">public</span> <span class="nc">GetUsersResponse</span> <span class="nf">getUsers</span><span class="o">(</span><span class="nd">@RequestParam</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"nation"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">nation</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">var</span> <span class="n">result</span> <span class="o">=</span> <span class="n">humanService</span><span class="o">.</span><span class="na">getExternalUsers</span><span class="o">(</span><span class="n">nation</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">result</span><span class="o">;</span>
    <span class="o">}</span>
  <span class="o">}</span>
</code></pre></div>    </div>
  </li>
  <li>jpa repository
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">public</span> <span class="kd">interface</span> <span class="nc">UserRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">&gt;</span> <span class="o">{</span>
      <span class="nc">List</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="nf">findAllByNation</span><span class="o">(</span><span class="nc">String</span> <span class="n">nation</span><span class="o">);</span>
  <span class="o">}</span>
</code></pre></div>    </div>
  </li>
  <li>FeignClient
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@FeignClient</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"userClient"</span><span class="o">,</span> <span class="n">url</span> <span class="o">=</span> <span class="s">"https://randomuser.me"</span><span class="o">)</span>
  <span class="kd">public</span> <span class="kd">interface</span> <span class="nc">UserClient</span> <span class="o">{</span>

      <span class="nd">@GetMapping</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"/api/"</span><span class="o">)</span>
      <span class="nc">GetUsersResponse</span> <span class="nf">getUsers</span><span class="o">(</span><span class="nd">@RequestParam</span><span class="o">(</span><span class="s">"nat"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">nation</span><span class="o">);</span>
  <span class="o">}</span>
</code></pre></div>    </div>
  </li>
</ul>

<p>이렇게 구현이 간단한점과 친숙함이 장점이 될 수 있을 것 같다고 생각된다.</p>

<hr />

<h2 id="세부-설정">세부 설정</h2>

<p>위에 구현한 것에서 더 나아가 보자.</p>

<hr />

<h3 id="1-url을-applicationyml에서-가져오기">1. url을 application.yml에서 가져오기</h3>

<p>@FeignClient 에서 설정한 url을 application.yml 에서 가져올 수 있다.</p>

<ul>
  <li>application.yml
    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">external</span><span class="pi">:</span>
<span class="na">user-service</span><span class="pi">:</span>
  <span class="na">host</span><span class="pi">:</span> <span class="s1">'</span><span class="s">https://randomuser.me'</span>
</code></pre></div>    </div>
  </li>
  <li>FeignClient url 수정
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@FeignClient</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"userClient"</span><span class="o">,</span> <span class="n">url</span> <span class="o">=</span> <span class="s">"${external.user-service.host}"</span><span class="o">)</span>
</code></pre></div>    </div>
  </li>
</ul>

<hr />

<h3 id="2-path-값-설정하기">2. Path 값 설정하기</h3>

<ul>
  <li>FeignClient
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@FeignClient</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"userClient"</span><span class="o">,</span> <span class="n">url</span> <span class="o">=</span> <span class="s">"https://randomuser.me"</span><span class="o">)</span>
  <span class="kd">public</span> <span class="kd">interface</span> <span class="nc">UserClient</span> <span class="o">{</span>

      <span class="nd">@GetMapping</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"/api/{nation}"</span><span class="o">)</span>
      <span class="nc">GetUsersResponse</span> <span class="nf">getUsers</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="nc">String</span> <span class="n">nation</span><span class="o">);</span>
  <span class="o">}</span>
</code></pre></div>    </div>
  </li>
</ul>

<hr />

<h3 id="3-다른-request-method-사용하기">3. 다른 Request Method 사용하기</h3>

<p>당연히 POST, PUT, PATCH 등 사용이 가능하다.</p>

<ul>
  <li>HumanClient.java
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@FeignClient</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"humanClient"</span><span class="o">,</span> <span class="n">url</span> <span class="o">=</span> <span class="s">"${external.human-service.host}"</span><span class="o">)</span>
  <span class="kd">public</span> <span class="kd">interface</span> <span class="nc">HumanClient</span> <span class="o">{</span>

      <span class="nd">@PostMapping</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"/human"</span><span class="o">)</span>
      <span class="nc">CreateHumanResponse</span> <span class="nf">createHuman</span><span class="o">(</span><span class="nc">CreateHumanRequest</span> <span class="n">request</span><span class="o">);</span>
  <span class="o">}</span>
</code></pre></div>    </div>
  </li>
  <li>CreateHumanRequest.java (Requet body 정의)
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@Getter</span>
  <span class="nd">@Builder</span>
  <span class="nd">@RequiredArgsConstructor</span>
  <span class="kd">public</span> <span class="kd">class</span> <span class="nc">CreateHumanRequest</span> <span class="o">{</span>
      <span class="kd">private</span> <span class="kd">final</span> <span class="nc">String</span> <span class="n">name</span><span class="o">;</span>

      <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Integer</span> <span class="n">money</span><span class="o">;</span>

      <span class="nd">@JsonFormat</span><span class="o">(</span><span class="n">pattern</span> <span class="o">=</span> <span class="s">"yyyy-MM-dd"</span><span class="o">)</span>
      <span class="kd">private</span> <span class="kd">final</span> <span class="nc">LocalDate</span> <span class="n">birth</span><span class="o">;</span>
  <span class="o">}</span>
</code></pre></div>    </div>

    <hr />
  </li>
</ul>

<h3 id="4-client에-커스텀-configration-적용하기">4. client에 커스텀 configration 적용하기</h3>

<p>client마다 custom config를 적용 할 수 있다.
예를들어 해당 외부 서비스에 API 를 호출할 때 무조건 공통으로 들어가야하는 header가 있거나
공통 Response가 있어서 필요한 필드만 decode해오거나 등의 행위들을 할 수 있다.</p>

<p>ex) 해당 client에서 api 호출할 떄 마다 header에 값넣기</p>

<ul>
  <li>HumanFeignClientConfig.class
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">public</span> <span class="kd">class</span> <span class="nc">HumanFeignClientConfig</span> <span class="o">{</span>
      <span class="nd">@Bean</span>
      <span class="kd">public</span> <span class="nc">RequestInterceptor</span> <span class="nf">requestInterceptor</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">InterruptedException</span> <span class="o">{</span>
          <span class="k">return</span> <span class="n">requestTemplate</span> <span class="o">-&gt;</span> <span class="n">requestTemplate</span><span class="o">.</span><span class="na">header</span><span class="o">(</span><span class="s">"header-name"</span><span class="o">,</span> <span class="s">"header-value"</span><span class="o">);</span>
      <span class="o">}</span>
  <span class="o">}</span>
</code></pre></div>    </div>
  </li>
  <li>configration에 config 적용
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@FeignClient</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"humanClient"</span><span class="o">,</span> <span class="n">url</span> <span class="o">=</span> <span class="s">"${external.human-service.host}"</span><span class="o">,</span> <span class="n">configuration</span> <span class="o">=</span> <span class="nc">HumanFeignClientConfig</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
  <span class="kd">public</span> <span class="kd">interface</span> <span class="nc">HumanClient</span> <span class="o">{</span>

      <span class="nd">@GetMapping</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"/human/list"</span><span class="o">)</span>
      <span class="nc">List</span><span class="o">&lt;</span><span class="nc">HumanInfo</span><span class="o">&gt;</span> <span class="nf">getHumans</span><span class="o">(</span><span class="nd">@RequestParam</span><span class="o">(</span><span class="s">"name"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">name</span><span class="o">);</span>
  <span class="o">}</span>
</code></pre></div>    </div>

    <p><img src="/assets/images/feign client 사용해보기/3.png" alt="feign client 사용해보기/3.png" /></p>
  </li>
</ul>

<hr />

<h3 id="5-공통-response-decode-하기">5. 공통 Response decode 하기</h3>

<p>custom configration을 통해서 response body를 디코딩 할 수 있다.
예를들어 외부서비스가 아래와 같이 특정 포맷의 공통 Response를 사용중이라고 할 때</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"result"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SUCCESS"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
    </span><span class="nl">"errorCode"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
    </span><span class="nl">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"idx"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
            </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"jojo"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"money"</span><span class="p">:</span><span class="w"> </span><span class="mi">3000</span><span class="p">,</span><span class="w">
            </span><span class="nl">"birth"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1991-02-25"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"idx"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w">
            </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"jojo"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"money"</span><span class="p">:</span><span class="w"> </span><span class="mi">3000</span><span class="p">,</span><span class="w">
            </span><span class="nl">"birth"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1991-02-25"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>이런식으로 항상 response가 온다면
FeignClient의 Response dto를 만들떄 매번 공통 Response 형태로 만들어서 data를 가져와 사용해야 하는 번거로움이 생긴다.</p>

<p>공통 포맷이 아닌 해당 body를 조작해서 data의 값만 가져오게
Custom Configration에서 설정이 가능하다.</p>

<ul>
  <li>HumanFeignClientConfig.class
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kn">import</span> <span class="nn">com.fasterxml.jackson.databind.type.TypeFactory</span><span class="o">;</span>
  <span class="kn">import</span> <span class="nn">feign.FeignException</span><span class="o">;</span>
  <span class="kn">import</span> <span class="nn">feign.RequestInterceptor</span><span class="o">;</span>
  <span class="kn">import</span> <span class="nn">feign.Response</span><span class="o">;</span>
  <span class="kn">import</span> <span class="nn">feign.codec.Decoder</span><span class="o">;</span>
  <span class="kn">import</span> <span class="nn">lombok.Getter</span><span class="o">;</span>
  <span class="kn">import</span> <span class="nn">lombok.Setter</span><span class="o">;</span>
  <span class="kn">import</span> <span class="nn">org.springframework.beans.factory.ObjectFactory</span><span class="o">;</span>
  <span class="kn">import</span> <span class="nn">org.springframework.boot.autoconfigure.http.HttpMessageConverters</span><span class="o">;</span>
  <span class="kn">import</span> <span class="nn">org.springframework.cloud.openfeign.support.SpringDecoder</span><span class="o">;</span>
  <span class="kn">import</span> <span class="nn">org.springframework.context.annotation.Bean</span><span class="o">;</span>
  <span class="kn">import</span> <span class="nn">org.springframework.core.ResolvableType</span><span class="o">;</span>

  <span class="kn">import</span> <span class="nn">java.io.IOException</span><span class="o">;</span>
  <span class="kn">import</span> <span class="nn">java.lang.reflect.Type</span><span class="o">;</span>

  <span class="kd">public</span> <span class="kd">class</span> <span class="nc">HumanFeignClientConfig</span> <span class="o">{</span>

      <span class="nd">@Bean</span>
      <span class="kd">public</span> <span class="nc">Decoder</span> <span class="nf">decoder</span><span class="o">(</span><span class="nc">ObjectFactory</span><span class="o">&lt;</span><span class="nc">HttpMessageConverters</span><span class="o">&gt;</span> <span class="n">messageConverters</span><span class="o">)</span> <span class="o">{</span>
          <span class="k">return</span> <span class="k">new</span> <span class="nf">HumanServiceDecoder</span><span class="o">(</span><span class="k">new</span> <span class="nc">SpringDecoder</span><span class="o">(</span><span class="n">messageConverters</span><span class="o">));</span>
      <span class="o">}</span>

      <span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">HumanServiceDecoder</span> <span class="kd">implements</span> <span class="nc">Decoder</span> <span class="o">{</span>
          <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Decoder</span> <span class="n">decoder</span><span class="o">;</span>

          <span class="kd">public</span> <span class="nf">HumanServiceDecoder</span><span class="o">(</span><span class="nc">Decoder</span> <span class="n">decoder</span><span class="o">)</span> <span class="o">{</span>
              <span class="k">this</span><span class="o">.</span><span class="na">decoder</span> <span class="o">=</span> <span class="n">decoder</span><span class="o">;</span>
          <span class="o">}</span>

          <span class="nd">@Override</span>
          <span class="kd">public</span> <span class="nc">Object</span> <span class="nf">decode</span><span class="o">(</span><span class="nc">Response</span> <span class="n">response</span><span class="o">,</span> <span class="nc">Type</span> <span class="n">type</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">IOException</span><span class="o">,</span> <span class="nc">FeignException</span> <span class="o">{</span>
              <span class="kt">var</span> <span class="n">returnType</span> <span class="o">=</span> <span class="nc">TypeFactory</span><span class="o">.</span><span class="na">rawClass</span><span class="o">(</span><span class="n">type</span><span class="o">);</span>
              <span class="kt">var</span> <span class="n">forClassWithGenerics</span> <span class="o">=</span>
                      <span class="nc">ResolvableType</span><span class="o">.</span><span class="na">forClassWithGenerics</span><span class="o">(</span><span class="nc">HumanServiceCommonResponse</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">returnType</span><span class="o">);</span>

              <span class="k">try</span> <span class="o">{</span>
                  <span class="k">return</span> <span class="o">((</span><span class="nc">HumanServiceCommonResponse</span><span class="o">&lt;?&gt;)</span> <span class="n">decoder</span><span class="o">.</span><span class="na">decode</span><span class="o">(</span><span class="n">response</span><span class="o">,</span>
                          <span class="n">forClassWithGenerics</span><span class="o">.</span><span class="na">getType</span><span class="o">())).</span><span class="na">getData</span><span class="o">();</span>
              <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
                  <span class="k">return</span> <span class="n">decoder</span><span class="o">.</span><span class="na">decode</span><span class="o">(</span><span class="n">response</span><span class="o">,</span> <span class="n">forClassWithGenerics</span><span class="o">.</span><span class="na">getType</span><span class="o">());</span>
              <span class="o">}</span>
          <span class="o">}</span>
      <span class="o">}</span>

  		<span class="c1">// 외부 서비스의 공통 Response 형태</span>
      <span class="nd">@Getter</span>
      <span class="nd">@Setter</span>
      <span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">HumanServiceCommonResponse</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="o">{</span>
          <span class="kd">private</span> <span class="nc">Result</span> <span class="n">result</span><span class="o">;</span>
          <span class="kd">private</span> <span class="no">T</span> <span class="n">data</span><span class="o">;</span>
          <span class="kd">private</span> <span class="nc">String</span> <span class="n">message</span><span class="o">;</span>
          <span class="kd">private</span> <span class="nc">String</span> <span class="n">errorCode</span><span class="o">;</span>

          <span class="kd">public</span> <span class="kd">enum</span> <span class="nc">Result</span> <span class="o">{</span>
              <span class="no">SUCCESS</span><span class="o">,</span> <span class="no">FAIL</span>
          <span class="o">}</span>
      <span class="o">}</span>
  <span class="o">}</span>
</code></pre></div>    </div>

    <p>이렇게 커스텀한 Decoder bean을 생성해주면 공통 Response에서 원하는 부위만 가져와서 원하는 형태의 객체를 만들 수 있다.
  마찬가지로 <strong>ErrorDecoder</strong> Bean을 만들어주면 에러 났을 떄의 Response Body 디코딩도 가능하다.</p>
  </li>
</ul>

<hr />

<h2 id="마치며">마치며</h2>

<p>난 해당 OpenFeign Client를 현재 실무에서도 사용중인데 아직까진 별다른 이슈 없이 사용하고 있다.
그리고 내 기준으로 RestTemplate, WebClient를 쓸 때보다 훨신 가독성있고 편하게 사용하고 있는 중이다.</p>

<p>더 많은 기능, 설정법들이 있을것이므로 실무에서 새로운 설정이나 구현을 하게되면 그때 그때 해당 글에 업데이트할 예정이다.</p>

<h2 id="끝">끝.</h2>

<hr />]]></content><author><name>Jo JaeYoung</name></author><category term="java" /><category term="spring boot" /><category term="openfeign" /><category term="httpclient" /><summary type="html"><![CDATA[Feign은 Http Client 도구인데 example을 보는 순간 직관적인 형태에 끌려 사용해보게 되었다. (RestTemplate, WebClient 등을 쓰다가 Feign을 써보게 되면 Feign만 쓰게될꺼다) Feign은 인터페이스와 annotation을 이용하여 아주 간단하게 요청을 날릴 수 있다.]]></summary></entry><entry><title type="html">Spring boot Timezone 설정하기</title><link href="https://isntyet.github.io/java/Spring-boot-Timezone-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0/" rel="alternate" type="text/html" title="Spring boot Timezone 설정하기" /><published>2022-05-14T01:30:30+00:00</published><updated>2022-05-14T01:30:30+00:00</updated><id>https://isntyet.github.io/java/Spring-boot-Timezone-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</id><content type="html" xml:base="https://isntyet.github.io/java/Spring-boot-Timezone-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0/"><![CDATA[<p>로컬에서는 분명히 적상적으로 작동을 했는데 서버 올라갔을 때<br />
비정상으로 작동하여 원인을 확인해 보니 타임존 문제였다.</p>

<p>application에 따로 타임존 설정을 따로 안해줬더니<br />
로컬(내 맥북)의 시간을 가져와서 KST로 설정 되었던 것이다.</p>

<p>Production 환경은 모두 UTC라 동일한 환경을 만들기 위해<br />
Application <strong>기본 Timezone을 UTC로 맞추기</strong>로 하자.</p>

<hr />

<h3 id="application-timezone-설정">Application timezone 설정</h3>

<p>Spring boot 에 기본 timezone 설정을 하기는 쉽다.<br />
SpringBootApplication에 <strong>@PostConstruct</strong> 를 이용하여 타임존 설정을 해주면 된다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@SpringBootApplication</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">PracticeApplication</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">SpringApplication</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="nc">PracticeApplication</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">args</span><span class="o">);</span>

        <span class="nc">LocalDateTime</span> <span class="n">now</span> <span class="o">=</span> <span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">();</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"현재시간 "</span> <span class="o">+</span> <span class="n">now</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@PostConstruct</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">init</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// timezone 설정</span>
        <span class="nc">TimeZone</span><span class="o">.</span><span class="na">setDefault</span><span class="o">(</span><span class="nc">TimeZone</span><span class="o">.</span><span class="na">getTimeZone</span><span class="o">(</span><span class="s">"UTC"</span><span class="o">));</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><img src="/assets/images/Spring boot Timezone 설정하기/0.png" alt="Spring boot Timezone 설정하기/0.png" /></p>

<blockquote>
  <p><strong><em>@PostConstruct</em></strong>:  빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출</p>

</blockquote>

<p>해당 설정이 안먹을 때는 <em>SpringApplication.run</em> 되기 전에 설정해보자.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@SpringBootApplication</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">PracticeApplication</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// timezone 설정</span>
        <span class="nc">TimeZone</span><span class="o">.</span><span class="na">setDefault</span><span class="o">(</span><span class="nc">TimeZone</span><span class="o">.</span><span class="na">getTimeZone</span><span class="o">(</span><span class="s">"UTC"</span><span class="o">));</span>
        <span class="nc">SpringApplication</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="nc">PracticeApplication</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">args</span><span class="o">);</span>

        <span class="nc">LocalDateTime</span> <span class="n">now</span> <span class="o">=</span> <span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">();</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"현재시간 "</span> <span class="o">+</span> <span class="n">now</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h3 id="junit-timezone-설정">Junit timezone 설정</h3>

<p>하다 보니 또 문제가 발생됬다.  로컬에서 실행해서 하는건 이제 해결되었는데 <strong>Test Code</strong>를 돌릴 때 똑같은 문제가 있었다.</p>

<p><strong>@SpringBootTest</strong> 시에는 상관없었지만<br />
(SpringBootTest시 Bean을 등록하게 되는데 Bean이 등록되면 위에서 설정했던 <strong>@PostConstruct</strong> 이 실행되기 때문에)</p>

<p><strong>@SpringBootTest</strong>  없이 <strong>@Test</strong> 단위 테스트시에는 Bean 등록을 따로 하지 않기 때문에 <strong>@PostConstruct</strong> 를 따로 호출하지 않아서 timezone이 그대로 KST였던 것이다.</p>

<p>Test시에도 TImezone을 설정할 수 있는 방법 중 하나는
각 테스트 class마다 default timezone을 설정해주는 것 이다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@TestInstance</span><span class="o">(</span><span class="nc">TestInstance</span><span class="o">.</span><span class="na">Lifecycle</span><span class="o">.</span><span class="na">PER_CLASS</span><span class="o">)</span>
<span class="kd">class</span> <span class="nc">HumanTest</span> <span class="o">{</span>

    <span class="nd">@BeforeAll</span>
    <span class="kt">void</span> <span class="nf">setup</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">TimeZone</span><span class="o">.</span><span class="na">setDefault</span><span class="o">(</span><span class="nc">TimeZone</span><span class="o">.</span><span class="na">getTimeZone</span><span class="o">(</span><span class="s">"UTC"</span><span class="o">));</span>
    <span class="o">}</span>

    <span class="nd">@Test</span>
    <span class="kt">void</span> <span class="nf">test</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">LocalDateTime</span> <span class="n">now</span> <span class="o">=</span> <span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">();</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"시간 "</span> <span class="o">+</span> <span class="n">now</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h3 id="gradle-test-에서-timezone-설정">Gradle test 에서 timezone 설정</h3>

<p>또 다른 방법은 <strong>Gradle</strong> 에 설정하는 방법이 있다.</p>

<ul>
  <li>build.gradle
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">dependencies</span> <span class="o">{</span>
      <span class="o">.</span>
      <span class="o">.</span>
      <span class="o">.</span>
  <span class="o">}</span>

  <span class="n">test</span> <span class="o">{</span>
      <span class="c1">// timezone 설정</span>
      <span class="n">systemProperty</span> <span class="err">'</span><span class="n">user</span><span class="o">.</span><span class="na">timezone</span><span class="err">'</span><span class="o">,</span> <span class="err">'</span><span class="no">UTC</span><span class="err">'</span>
  <span class="o">}</span>
</code></pre></div>    </div>
  </li>
</ul>

<p>위 설정을 했다면 아래처럼 Preferences에서 test실행을 intellij가 아니라 Gradle로 하게 바꿔야 한다</p>

<p><img src="/assets/images/Spring boot Timezone 설정하기/1.png" alt="Spring boot Timezone 설정하기/1.png" /></p>

<hr />

<h3 id="intellij-idea-에서-timezone-설정">IntelliJ IDEA 에서 timezone 설정</h3>

<p>Run tests using 설정을 <strong>IntelliJ IDEA</strong>로 유지하고 테스트를 진행하고 싶다면<br />
<strong>Edit Configurations…</strong> 에서
<img src="/assets/images/Spring boot Timezone 설정하기/2.png" alt="Spring boot Timezone 설정하기/2.png" /></p>

<p><strong>Edit configuration templates…</strong> 로 들어가면
<img src="/assets/images/Spring boot Timezone 설정하기/3.png" alt="Spring boot Timezone 설정하기/3.png" /></p>

<p>JUnit 항목의 <strong>vm option</strong> 에</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-Duser.timezone=UTC
</code></pre></div></div>
<p>를 추가하면 된다
<img src="/assets/images/Spring boot Timezone 설정하기/4.png" alt="Spring boot Timezone 설정하기/4.png" /></p>

<hr />
<h3 id="끝">끝.</h3>

<p>repo = <a href="https://github.com/isntyet/java-practice/commit/0583e93ac8af8f03c0574646bdc7cdd3c8c312d6">https://github.com/isntyet/java-practice/commit/0583e93ac8af8f03c0574646bdc7cdd3c8c312d6</a></p>

<hr />]]></content><author><name>Jo JaeYoung</name></author><category term="java" /><category term="spring boot" /><category term="timezone" /><category term="test" /><summary type="html"><![CDATA[로컬에서는 분명히 적상적으로 작동을 했는데 서버 올라갔을 때 비정상으로 작동하여 원인을 확인해 보니 타임존 문제였다.]]></summary></entry><entry><title type="html">TestContainers로 test 멱등성 높이기</title><link href="https://isntyet.github.io/java/TestContainers%EB%A1%9C-test-%EB%A9%B1%EB%93%B1%EC%84%B1-%EB%86%92%EC%9D%B4%EA%B8%B0/" rel="alternate" type="text/html" title="TestContainers로 test 멱등성 높이기" /><published>2021-07-31T01:30:30+00:00</published><updated>2021-07-31T01:30:30+00:00</updated><id>https://isntyet.github.io/java/TestContainers%EB%A1%9C-test-%EB%A9%B1%EB%93%B1%EC%84%B1-%EB%86%92%EC%9D%B4%EA%B8%B0</id><content type="html" xml:base="https://isntyet.github.io/java/TestContainers%EB%A1%9C-test-%EB%A9%B1%EB%93%B1%EC%84%B1-%EB%86%92%EC%9D%B4%EA%B8%B0/"><![CDATA[<p>h2 in-memory db에서 테스트를 하였는데 뭔가 이상했다.
production환경에서 사용중인 mariadb로 배포 전에 혹시나 해서 테스트해보기 위해 로컬에서 docker로 mariadb를 띄워서 테스트를 했을 때와 결과가 달랐다….</p>

<p>내가 하고있던 테스트는 jpa 관련해서 <code class="language-plaintext highlighter-rouge">Pessimistic lock</code> 테스트 중이었는데,
동시에 5개가 들어왔을때 하나만 성공 해야하는 테스트였다.</p>

<p>테스트 결과가 어떻게 나왔냐면</p>

<ul>
  <li>
    <p>maria db</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  Hibernate: insert into locker (status, target_date, id) values (?, ?, ?)
  Hibernate: insert into locker (status, target_date, id) values (?, ?, ?)
  Hibernate: insert into locker (status, target_date, id) values (?, ?, ?)
  Hibernate: insert into locker (status, target_date, id) values (?, ?, ?)
  Hibernate: insert into locker (status, target_date, id) values (?, ?, ?)
  [pool-1-thread-3] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1213, SQLState: 40001
  [pool-1-thread-3] o.h.engine.jdbc.spi.SqlExceptionHelper   : Deadlock found when trying to get lock; try restarting transaction
  [pool-1-thread-5] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1213, SQLState: 40001
  [pool-1-thread-4] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1213, SQLState: 40001
  [pool-1-thread-2] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1213, SQLState: 40001
  [pool-1-thread-4] o.h.engine.jdbc.spi.SqlExceptionHelper   : Deadlock found when trying to get lock; try restarting transaction
  [pool-1-thread-5] o.h.engine.jdbc.spi.SqlExceptionHelper   : Deadlock found when trying to get lock; try restarting transaction
  [pool-1-thread-2] o.h.engine.jdbc.spi.SqlExceptionHelper   : Deadlock found when trying to get lock; try restarting transaction
</code></pre></div>    </div>

    <p>이렇게 동시에 insert하는 순간 Deadlock이 발생했는데</p>
  </li>
  <li>
    <p>h2 db</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  [pool-1-thread-5] o.h.engine.jdbc.spi.SqlExceptionHelper   : Unique index or primary key violation: "PUBLIC.UK_Q7TY4EN85RSD1VAL96JEUFDLA_INDEX_8 ON PUBLIC.LOCKER(TARGET_DATE) VALUES 4"; SQL statement:
  insert into locker (status, target_date, id) values (?, ?, ?) [23505-200]
  [pool-1-thread-2] o.h.engine.jdbc.spi.SqlExceptionHelper   : Unique index or primary key violation: "PUBLIC.UK_Q7TY4EN85RSD1VAL96JEUFDLA_INDEX_8 ON PUBLIC.LOCKER(TARGET_DATE) VALUES 4"; SQL statement:
  insert into locker (status, target_date, id) values (?, ?, ?) [23505-200]
  [pool-1-thread-1] o.h.engine.jdbc.spi.SqlExceptionHelper   : Unique index or primary key violation: "PUBLIC.UK_Q7TY4EN85RSD1VAL96JEUFDLA_INDEX_8 ON PUBLIC.LOCKER(TARGET_DATE) VALUES 4"; SQL statement:
  insert into locker (status, target_date, id) values (?, ?, ?) [23505-200]
  [pool-1-thread-3] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 23505, SQLState: 23505
  [pool-1-thread-3] o.h.engine.jdbc.spi.SqlExceptionHelper   : Unique index or primary key violation: "PUBLIC.UK_Q7TY4EN85RSD1VAL96JEUFDLA_INDEX_8 ON PUBLIC.LOCKER(TARGET_DATE) VALUES 4"; SQL statement:
</code></pre></div>    </div>

    <p>h2는 Deadlock이 발생하지 않고 내가 걸어놓은 Unique index에서 걸려버린다…</p>
  </li>
</ul>

<p>뭔가 h2 는 lock이 걸리지 않고 진행된 느낌이다.
조심스럽게 추측하기에는 각 db에서 지원하는 locktimeout의 차이 때문이 아닐까 생각했다. (뇌피셜)</p>

<p><img src="/assets/images/TestContainers로 test 멱등성 높이기/0.png" alt="TestContainers로 test 멱등성 높이기/0.png" /></p>

<p>출처: <a href="https://blog.mimacom.com/handling-pessimistic-locking-jpa-oracle-mysql-postgresql-derbi-h2/">https://blog.mimacom.com/handling-pessimistic-locking-jpa-oracle-mysql-postgresql-derbi-h2/</a></p>

<p>local에서 테스트할 때 어차피 docker로 mariadb를 쭉 띄워놓고 테스트하면 상관 없지만
Azure pipeline이나 Github action 같이 CI 중에 테스트 실행 할 때도 테스트에 대한 <code class="language-plaintext highlighter-rouge">멱등성</code>을 유지하고 싶어서 찾아보니 <code class="language-plaintext highlighter-rouge">TestContainers</code> 라는 것이 있었다.</p>

<p><a href="https://www.testcontainers.org/">Testcontainers</a></p>

<p>Testcontainers 를 사용하면 테스트 실행시 내가 설정한 db를 <code class="language-plaintext highlighter-rouge">container</code>에 띄워서 테스트를 진행 할 수 있다.
한번 사용해 보자.</p>

<hr />

<h3 id="testcontainers-의존성-추가">Testcontainers 의존성 추가</h3>

<p>dependency 추가를 해주자.
나는 JUnit 5를 사용중이기 때문에 해당 기준으로 진행을 할 것 이다.</p>

<p><a href="https://www.testcontainers.org/quickstart/junit_5_quickstart/">JUnit 5 Quickstart</a></p>

<p>Quickstart를 참고해서 의존성 추가를 해주자.
내가 테스트에 사용할 db는 mariadb이기 때문에 아래 module 정보를 보고 추가하자.<br />
다른 db를 추가 해줘야 하면 공식 홈페이지 모듈메뉴를 참조해서 추가해 주면 되겠다.</p>

<p><a href="https://www.testcontainers.org/modules/databases/mariadb/">MariaDB Module</a></p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dependencies</span> <span class="o">{</span>
		<span class="n">implementation</span> <span class="s1">'com.h2database:h2'</span>
    <span class="n">implementation</span> <span class="s1">'org.mariadb.jdbc:mariadb-java-client:2.2.1'</span>
		<span class="n">testImplementation</span><span class="o">(</span><span class="s1">'org.springframework.boot:spring-boot-starter-test'</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">exclude</span> <span class="nl">group:</span> <span class="s1">'org.junit.vintage'</span><span class="o">,</span> <span class="nl">module:</span> <span class="s1">'junit-vintage-engine'</span>
    <span class="o">}</span>
    <span class="o">.</span>
    <span class="o">.</span>
    <span class="o">.</span>
		
    <span class="n">testImplementation</span><span class="o">(</span><span class="s1">'org.assertj:assertj-core:3.15.0'</span><span class="o">)</span>
    <span class="n">testImplementation</span> <span class="s1">'org.testcontainers:testcontainers:1.15.3'</span>
    <span class="n">testImplementation</span> <span class="s1">'org.testcontainers:junit-jupiter:1.15.3'</span>
    <span class="n">testImplementation</span> <span class="s1">'org.testcontainers:mariadb:1.15.3'</span>

    <span class="o">.</span>
    <span class="o">.</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h3 id="property-설정">property 설정</h3>

<p>이제 spring properties에서 db연결정보를 입력하면 된다.
나는 test용 property를 따로 만들었는데 아래와 같이 만들었다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">spring</span><span class="pi">:</span>
  <span class="na">datasource</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">com.zaxxer.hikari.HikariDataSource</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s">jdbc:tc:mariadb:10.2:///test</span>
    <span class="na">driver-class-name</span><span class="pi">:</span> <span class="s">org.testcontainers.jdbc.ContainerDatabaseDriver</span>
    <span class="na">username</span><span class="pi">:</span> <span class="s">test</span>
    <span class="na">password</span><span class="pi">:</span> <span class="s">test</span>
</code></pre></div></div>

<p>datasource.url 에는 mariadb:10.2 라고 하였는데 내가 production 환경에서 사용하는 mariadb가 10.2 버전이라서 저렇게 사용하였다. docker image라 생각하면 된다.</p>

<p>db name, username, password는 의존성 추가한 org.testcontainers:mariadb를 까보면 default정보를 획득 할 수 있다.</p>

<p><img src="/assets/images/TestContainers로 test 멱등성 높이기/1.png" alt="TestContainers로 test 멱등성 높이기/1.png" /></p>

<hr />

<h3 id="test-실행">Test 실행</h3>

<p>이제 ActiveProfiles을 test로 해서 테스트 실행을 해보자.</p>

<p><img src="/assets/images/TestContainers로 test 멱등성 높이기/2.png" alt="TestContainers로 test 멱등성 높이기/2.png" /></p>

<p>실행되면 이렇게 docker container를 띄우고 있는게 확인된다!!</p>

<hr />

<h3 id="또-문제-발생">또 문제 발생</h3>

<p>이렇게 db를 띄우게 되면 문제점이 있는데 default 설정으로 띄워지기 때문에 charset이나 timezone 같은 설정들을 할 수 가 없다…
그래서 한글 insert test를 하였더니 아래와 같이 에러가 났다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Caused by: java.sql.SQLDataException: (conn=9) Incorrect string value: '\xEC\xA1\xB0\xEC\x9E\xAC...' for column `test`.`human`.`name` at row 1
	at org.mariadb.jdbc.internal.util.exceptions.ExceptionMapper.get(ExceptionMapper.java:167)
	at org.mariadb.jdbc.internal.util.exceptions.ExceptionMapper.getException(ExceptionMapper.java:110)
	at org.mariadb.jdbc.MariaDbStatement.executeExceptionEpilogue(MariaDbStatement.java:228)
</code></pre></div></div>

<p>검색해보면 docker-compose를 이용해서 설정 후 testcontainer를 띄우는 방법이 있는데 귀찮기 떄문에 꼼수를 부려봤다.</p>

<ul>
  <li>
    <p>resources/sql/schema-test.sql</p>

    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">ALTER</span> <span class="k">DATABASE</span> <span class="n">test</span> <span class="nb">CHARACTER</span> <span class="k">SET</span> <span class="o">=</span> <span class="n">utf8mb4</span> <span class="k">COLLATE</span> <span class="o">=</span> <span class="n">utf8mb4_unicode_ci</span><span class="p">;</span>
</code></pre></div>    </div>

    <p>이렇게 charset을 설정하는 쿼리를 작성한 후</p>
  </li>
  <li>
    <p>resources/application-test.yaml</p>

    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="na">spring</span><span class="pi">:</span>
    <span class="na">datasource</span><span class="pi">:</span>
      <span class="na">type</span><span class="pi">:</span> <span class="s">com.zaxxer.hikari.HikariDataSource</span>
      <span class="na">url</span><span class="pi">:</span> <span class="s">jdbc:tc:mariadb:10.2:///test</span>
      <span class="na">driver-class-name</span><span class="pi">:</span> <span class="s">org.testcontainers.jdbc.ContainerDatabaseDriver</span>
      <span class="na">username</span><span class="pi">:</span> <span class="s">test</span>
      <span class="na">password</span><span class="pi">:</span> <span class="s">test</span>
      <span class="na">initialization-mode</span><span class="pi">:</span> <span class="s">always</span>
      <span class="na">schema</span><span class="pi">:</span> <span class="s">classpath:sql/schema-test.sql</span>
</code></pre></div>    </div>

    <p>property에 initialization-mode, schema를 추가하여 해당 쿼리를 실행해줬다.</p>
  </li>
</ul>

<p>이렇게 문제는 넘겼지만 더 세세한 설정이나
db뿐만아니라 다른 환경들(redis, kafka 등)도 컨테이너에 같이 띄워서 테스트해야 하는 상황이 오게되면 docker-compose를 이용해서 testcontainer를 띄우는 방법을 고려하면 좋겠다.</p>

<hr />
<h3 id="끝">끝.</h3>]]></content><author><name>Jo JaeYoung</name></author><category term="java" /><category term="spring boot" /><category term="testcontainers" /><category term="test" /><category term="mariadb" /><summary type="html"><![CDATA[h2 in-memory db에서 테스트를 하였는데 뭔가 이상했다. production환경에서 사용중인 mariadb로 배포 전에 혹시나 해서 테스트해보기 위해 로컬에서 docker로 mariadb를 띄워서 테스트를 했을 때와 결과가 달랐다….]]></summary></entry><entry><title type="html">db character set 바꾸기</title><link href="https://isntyet.github.io/db/db-character-set-%EB%B0%94%EA%BE%B8%EA%B8%B0/" rel="alternate" type="text/html" title="db character set 바꾸기" /><published>2021-05-25T03:30:30+00:00</published><updated>2021-05-25T03:30:30+00:00</updated><id>https://isntyet.github.io/db/db-character-set-%EB%B0%94%EA%BE%B8%EA%B8%B0</id><content type="html" xml:base="https://isntyet.github.io/db/db-character-set-%EB%B0%94%EA%BE%B8%EA%B8%B0/"><![CDATA[<p>Spring 어플리케이션에서 maria db의 varchar type의 컬럼에 이모티콘(🍯)을 insert하려 했더니</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SQLDataException Incorrect string value: '\xF0\x9F\x8D\xAF'....
</code></pre></div></div>
<p>요런 에러가 발생했다.<br />
저 이상한 형식의 string(\xF0\x9F\x8D\xAF)을 검색해보니 내가 db에 insert하려했던 값인 🍯 이모티콘으로 확인 되었다. <br />
뭔가 character set이 안맞아서 insert에 실패하는 것 같다.</p>

<p>insert 하려는 테이블을 아래 쿼리로 살펴보면</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">show</span> <span class="k">full</span> <span class="n">columns</span> <span class="k">from</span> <span class="s1">'테이블 이름'</span><span class="p">;</span>
</code></pre></div></div>
<p>컬럼별 type, collation 등을 확인 할 수 있는데</p>

<p><img src="/assets/images/db character set 바꾸기/0.png" alt="db character set 바꾸기/0.png" />
요렇게 collation이 latin… 로 들어가 있는 것 을 확인 할 수 있다.</p>

<p>아래 쿼리로 db의 캐릭터셋을 <strong><em>utf8mb4</em></strong>로 바꿔주고 collate도 <strong><em>utf8mb4_unicode_ci</em></strong>로 바꿔주자.</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ALTER</span> <span class="k">DATABASE</span> <span class="s1">'db이름'</span> <span class="nb">CHARACTER</span> <span class="k">SET</span> <span class="n">utf8mb4</span> <span class="k">COLLATE</span> <span class="n">utf8mb4_unicode_ci</span><span class="p">;</span>
</code></pre></div></div>

<hr />

<p>그래도 이모티콘이 insert가 안된다면 아래 쿼리로 global 변수 값을 확인 해 보자.</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">show</span> <span class="k">global</span> <span class="n">variables</span> <span class="k">where</span> <span class="n">Variable_name</span> <span class="k">like</span> <span class="s1">'character</span><span class="se">\_</span><span class="s1">set</span><span class="se">\_</span><span class="s1">%'</span> <span class="k">or</span> <span class="n">variable_name</span> <span class="k">like</span> <span class="s1">'collation%'</span><span class="p">;</span>
</code></pre></div></div>

<p><img src="/assets/images/db character set 바꾸기/1.png" alt="db character set 바꾸기/1.png" />
DB의 char-set과 collation을 바꿨지만 데이터베이스 기본설정은 바뀌지 않은 것을 확인 할 수 있다.</p>

<p>내가 사용하는 db는 aws rds의 mariadb 10.4 인데<br />
해당 글로벌 변수를 바꾸기 위해서는 파라미터 그룹을 바꿔 줘야 한다.</p>

<p>aws rds에 들어가서 해당 database의 구성을 확인해 보면
<img src="/assets/images/db character set 바꾸기/2.png" alt="db character set 바꾸기/2.png" /></p>

<p>파라미터 그룹이 default.mariadb10.4 로 설정되어 있는데, 해당 파라미터 그룹에서 이전에 조회했던 글로벌 변수들을 확인해보면…
<img src="/assets/images/db character set 바꾸기/3.png" alt="db character set 바꾸기/3.png" />
<code class="language-plaintext highlighter-rouge">값</code> 이 비워져 있는 것을 확인 할 수 있다.</p>

<p>해당 파라미터 그룹은 default라서 수정이 불가능 하니 mariadb 10.4 기반으로 새로운 파라미터 그룹을 만들어 주자.
<img src="/assets/images/db character set 바꾸기/4.png" alt="db character set 바꾸기/4.png" /></p>

<p>파라미터 그룹을 만들고 아래처럼 값을 utf8mb4로 바꿔주자.
<img src="/assets/images/db character set 바꾸기/5.png" alt="db character set 바꾸기/5.png" /></p>

<p>collation_connection 와 collation_server 값은 <strong><em>utf8mb4_unicode_ci</em></strong> 로 바꿔주자.
<img src="/assets/images/db character set 바꾸기/6.png" alt="db character set 바꾸기/6.png" /></p>

<p>이제 데이터베이스에서 해당 파라미터 그룹으로 바꿔줘야 한다.<br />
해당 데이터베이스의 수정 → 데이터베이스 옵션 항목에서 <br />
DB 파라미터 그룹을 위에서 만든 파라미터 그룹으로 변경하고 저장한다.
<img src="/assets/images/db character set 바꾸기/7.png" alt="db character set 바꾸기/7.png" /></p>

<p><img src="/assets/images/db character set 바꾸기/8.png" alt="db character set 바꾸기/8.png" /></p>

<p>나는 test db라 즉시 적용을 선택했다.</p>

<p><strong>DB 인스턴스 수정</strong>을 누르고 해당 db 상태값을 보면 <strong>수정 중</strong> 이라고 표시되는데 기다리다가<br />
<strong>사용 가능</strong>으로 변경되고<br />
구성 항목에서 파라미터 그룹을 확인 해 보면 <strong>재시작 보류중</strong> 이라고 표시되어 있는데….<br />
db 재부팅을 해야 적용이 된다…
<img src="/assets/images/db character set 바꾸기/9.png" alt="db character set 바꾸기/9.png" /></p>

<p>딱히 다운타임이 문제가 되지 않는다면 db 재부팅을 해주자.</p>

<p>재부팅 후 아래 쿼리로 다시 글로벌 변수를 확인 해 보면 값이 바뀐 것을 확인 할 수 있다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">show</span> <span class="k">global</span> <span class="n">variables</span> <span class="k">where</span> <span class="n">Variable_name</span> <span class="k">like</span> <span class="s1">'character</span><span class="se">\_</span><span class="s1">set</span><span class="se">\_</span><span class="s1">%'</span> <span class="k">or</span> <span class="n">variable_name</span> <span class="k">like</span> <span class="s1">'collation%'</span><span class="p">;</span>
</code></pre></div></div>

<p><img src="/assets/images/db character set 바꾸기/10.png" alt="db character set 바꾸기/10.png" /></p>]]></content><author><name>Jo JaeYoung</name></author><category term="db" /><category term="mariadb" /><category term="char-set" /><category term="aws-rds" /><summary type="html"><![CDATA[Spring 어플리케이션에서 maria db의 varchar type의 컬럼에 이모티콘(🍯)을 insert하려 했더니 SQLDataException Incorrect string value: '\xF0\x9F\x8D\xAF'.... 요런 에러가 발생했다. 저 이상한 형식의 string(\xF0\x9F\x8D\xAF)을 검색해보니 내가 db에 insert하려했던 값인 🍯 이모티콘으로 확인 되었다. 뭔가 character set이 안맞아서 insert에 실패하는 것 같다.]]></summary></entry><entry><title type="html">spring boot에서 aws kms를 이용해 프로퍼티값 암호화 하기</title><link href="https://isntyet.github.io/java/spring-boot%EC%97%90%EC%84%9C-aws-kms%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0%EA%B0%92-%EC%95%94%ED%98%B8%ED%99%94-%ED%95%98%EA%B8%B0/" rel="alternate" type="text/html" title="spring boot에서 aws kms를 이용해 프로퍼티값 암호화 하기" /><published>2021-05-16T06:30:30+00:00</published><updated>2021-05-16T06:30:30+00:00</updated><id>https://isntyet.github.io/java/spring-boot%EC%97%90%EC%84%9C-aws-kms%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0%EA%B0%92-%EC%95%94%ED%98%B8%ED%99%94-%ED%95%98%EA%B8%B0</id><content type="html" xml:base="https://isntyet.github.io/java/spring-boot%EC%97%90%EC%84%9C-aws-kms%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0%EA%B0%92-%EC%95%94%ED%98%B8%ED%99%94-%ED%95%98%EA%B8%B0/"><![CDATA[<p>프로퍼티에 db 연결정보나 api key같은 정보가 그대로 들어갔다가 git 계정이 털리는 등 소스를 탈취 당하게 됬을 경우 아주 곤란해 질 수 있다.<br />
보안이 필요한 값들은 애초에 암호화 해주면 그런 걱정을 덜 수 있겠다.<br />
AWS KMS를 이용해서 spring boot 프로젝트의 프로퍼티 값들을 암호화 해보자.</p>

<hr />

<h3 id="시작하기">시작하기</h3>

<p>아래 repo를 보고 진행 해보자.<br />
<a href="https://github.com/zalando/spring-cloud-config-aws-kms">zalando/spring-cloud-config-aws-kms</a><br />
아래 프로퍼티의 db연결정보를 암호화 하는것이 목표다.</p>

<p><img src="/assets/images/spring boot에서 aws kms를 이용해 프로퍼티값 암호화 하기/0.png" alt="spring boot에서 aws kms를 이용해 프로퍼티값 암호화 하기/0.png" /></p>

<hr />

<h3 id="aws에서-iam-생성하기">AWS에서 IAM 생성하기</h3>

<p>kms는 aws iam user나 role을 통해서 권한을 가질 수 있으니 우선 local에서 사용할 iam을 만들어 보자.<br />
AWS → IAM 에서 사용자를 만들어주면 된다. (만드는 방법은 알아서 찾아보자)<br />
나는 jo-mac이라는 이름으로 user를 만들었다.</p>

<hr />

<h3 id="aws에서-kmskey-management-service-생성하기">AWS에서 KMS(Key Management Service) 생성하기</h3>

<p>aws에 접속해서 KMS를 생성 해 주자.</p>

<p><img src="/assets/images/spring boot에서 aws kms를 이용해 프로퍼티값 암호화 하기/1.png" alt="spring boot에서 aws kms를 이용해 프로퍼티값 암호화 하기/1.png" /></p>

<p>키 사양에 대해선 따로 알아보자. 여기선 RSA_4096 사양으로 선택한다.
다음 별칭을 알아서 정하고</p>

<p><code class="language-plaintext highlighter-rouge">키 관리 권한 정의</code> 에서 만들었던 iam user를 선택 해 주자.</p>

<p><img src="/assets/images/spring boot에서 aws kms를 이용해 프로퍼티값 암호화 하기/2.png" alt="spring boot에서 aws kms를 이용해 프로퍼티값 암호화 하기/2.png" /></p>

<p><code class="language-plaintext highlighter-rouge">키 사용 권한 정의</code> 에서도 만들었던 iam user를 선택 해 주자.</p>

<p><img src="/assets/images/spring boot에서 aws kms를 이용해 프로퍼티값 암호화 하기/3.png" alt="spring boot에서 aws kms를 이용해 프로퍼티값 암호화 하기/3.png" /></p>

<p>생성을 완료 한 후 <code class="language-plaintext highlighter-rouge">키 ID</code> 를 기억해 주자.</p>

<p><img src="/assets/images/spring boot에서 aws kms를 이용해 프로퍼티값 암호화 하기/4.png" alt="spring boot에서 aws kms를 이용해 프로퍼티값 암호화 하기/4.png" /></p>

<p>팀 단위로 사용해야 할 때는  IAM User 하나를 직접 등록 하지 말고, 사용자 그룹에 Role을 부여해서 해당 Role을  등록해 주는것이 편할 것 같다.</p>

<hr />

<h3 id="aws-cli로-암호화-되는지-확인-해-보기">AWS CLI로 암호화 되는지 확인 해 보기</h3>

<p>Spring boot 프로젝트에 적용해보기 전에 local에서 aws cli로 암호화, 복호화를 해보자.<br />
먼저 aws configure을 이용해 credentials 설정을 해주자. (설정법을 모른다면 따로 알아보자.)</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws configure
</code></pre></div></div>

<p>아래 커맨드를 이용해서 ‘Hello World’를 <code class="language-plaintext highlighter-rouge">암호화</code> 해보자.<br />
key-id는 아까 kms 생성 후 해당 kms에 대한 키 id 이고, 암호화 알고리즘은 <code class="language-plaintext highlighter-rouge">RSAES_OAEP_SHA_1</code> 와 <code class="language-plaintext highlighter-rouge">RSAES_OAEP_SHA_256</code> 을 지원한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws kms encrypt <span class="nt">--key-id</span> 키아이디 <span class="nt">--plaintext</span> fileb://&lt;<span class="o">(</span><span class="nb">echo</span> <span class="nt">-n</span> <span class="s1">'암호화할 내용'</span><span class="o">)</span> <span class="nt">--encryption-algorithm</span> 암호화 알고리즘
</code></pre></div></div>

<p>ex)  key-id 가 ‘123456’이고 암호화할 내용은 ‘Hello World’이고 알고리즘은 RSAES_OAEP_SHA_256을 사용한다면?</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws kms encrypt <span class="nt">--key-id</span> 123456 <span class="nt">--plaintext</span> fileb://&lt;<span class="o">(</span><span class="nb">echo</span> <span class="nt">-n</span> <span class="s1">'Hello World'</span><span class="o">)</span> <span class="nt">--encryption-algorithm</span> RSAES_OAEP_SHA_256
</code></pre></div></div>

<p>위 커맨드를 사용하면 아래 같이 <code class="language-plaintext highlighter-rouge">CiphertextBlob</code>가 나오는것을 확인 할 수 있다.</p>

<p><img src="/assets/images/spring boot에서 aws kms를 이용해 프로퍼티값 암호화 하기/5.png" alt="spring boot에서 aws kms를 이용해 프로퍼티값 암호화 하기/5.png" /></p>

<p><code class="language-plaintext highlighter-rouge">CiphertextBlob</code> 값이 암호문이라 생각하면 된다.</p>

<hr />

<h3 id="aws-cli로-복호화-해보기">AWS CLI로 복호화 해보기</h3>

<p>위와 반대로 복호화를 해보자. 위에 암호화된 내용을 넣어서 실행하면…</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws kms decrypt <span class="nt">--key-id</span>  <span class="nt">--ciphertext-blob</span> fileb://&lt;<span class="o">(</span><span class="nb">echo</span> <span class="nt">-n</span> <span class="s1">'복호화할 내용'</span> | <span class="nb">base64</span> <span class="nt">--decode</span><span class="o">)</span> <span class="nt">--output</span> text <span class="nt">--encryption-algorithm</span> 암호화 알고리즘 <span class="nt">--query</span> Plaintext | <span class="nb">base64</span> <span class="nt">--decode</span>
</code></pre></div></div>

<p><img src="/assets/images/spring boot에서 aws kms를 이용해 프로퍼티값 암호화 하기/6.png" alt="spring boot에서 aws kms를 이용해 프로퍼티값 암호화 하기/6.png" /></p>

<p>요렇게 ‘Hello World’ 로 복호화 되는 것을 확인 할 수 있다.</p>

<hr />

<h3 id="spring-boot-프로젝트에-dependency-추가하기">Spring boot 프로젝트에 dependency 추가하기</h3>

<p>이제 프로젝트에 적용해보자.
아래 디펜던시를 추가해주자. (버전은 다 최신버전으로 했다.)</p>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">implementation</span><span class="o">(</span><span class="s2">"org.zalando:spring-cloud-config-aws-kms:5.1.2"</span><span class="o">)</span>
<span class="n">implementation</span><span class="o">(</span><span class="s2">"com.amazonaws:aws-java-sdk-core:1.11.1019"</span><span class="o">)</span>
<span class="n">implementation</span><span class="o">(</span><span class="s2">"com.amazonaws:aws-java-sdk-kms:1.11.1019"</span><span class="o">)</span>
<span class="n">implementation</span><span class="o">(</span><span class="s2">"com.amazonaws:jmespath-java:1.11.1019"</span><span class="o">)</span>
</code></pre></div></div>

<hr />

<h3 id="bootstrapyml-추가하기">bootstrap.yml 추가하기</h3>

<p>project resources경로에 bootstrap.yml 파일을 추가해서 key-id와 사용할 알고리즘을 넣어준다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">aws</span><span class="pi">:</span>
  <span class="na">kms</span><span class="pi">:</span>
    <span class="na">keyId</span><span class="pi">:</span> <span class="s">키 아이디</span>
    <span class="na">encryptionAlgorithm</span><span class="pi">:</span> <span class="s2">"</span><span class="s">RSAES_OAEP_SHA_256"</span>
</code></pre></div></div>

<p><img src="/assets/images/spring boot에서 aws kms를 이용해 프로퍼티값 암호화 하기/7.png" alt="spring boot에서 aws kms를 이용해 프로퍼티값 암호화 하기/7.png" /></p>

<hr />

<h3 id="암복호화-test-code-작성">암복호화 Test code 작성</h3>

<p>cli로 암호화 하면 되지만 매번 그러면 귀찮으니까 테스트 코드를 작성해보자.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">com.amazonaws.regions.Regions</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.amazonaws.services.kms.AWSKMS</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.amazonaws.services.kms.AWSKMSClientBuilder</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.amazonaws.services.kms.model.DecryptRequest</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.amazonaws.services.kms.model.EncryptRequest</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.amazonaws.services.kms.model.EncryptResult</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.amazonaws.services.kms.model.EncryptionAlgorithmSpec</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.apache.commons.codec.binary.Base64</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.junit.jupiter.api.Test</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.test.context.ActiveProfiles</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">java.nio.ByteBuffer</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.nio.charset.StandardCharsets</span><span class="o">;</span>

<span class="nd">@ActiveProfiles</span><span class="o">(</span><span class="s">"test"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">KmsTest</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">KEY_ID</span> <span class="o">=</span> <span class="s">"Key-ID"</span><span class="o">;</span>

    <span class="nd">@Test</span>
    <span class="kt">void</span> <span class="nf">encrypt</span><span class="o">()</span> <span class="o">{</span>
        <span class="kd">final</span> <span class="nc">String</span> <span class="n">plaintext</span> <span class="o">=</span> <span class="s">"암호화할 값"</span><span class="o">;</span>

        <span class="k">try</span> <span class="o">{</span>
            <span class="no">AWSKMS</span> <span class="n">kmsClient</span> <span class="o">=</span> <span class="nc">AWSKMSClientBuilder</span><span class="o">.</span><span class="na">standard</span><span class="o">()</span>
                    <span class="o">.</span><span class="na">withRegion</span><span class="o">(</span><span class="nc">Regions</span><span class="o">.</span><span class="na">AP_NORTHEAST_2</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">build</span><span class="o">();</span>

            <span class="nc">EncryptRequest</span> <span class="n">request</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">EncryptRequest</span><span class="o">();</span>
            <span class="n">request</span><span class="o">.</span><span class="na">withKeyId</span><span class="o">(</span><span class="no">KEY_ID</span><span class="o">);</span>
            <span class="n">request</span><span class="o">.</span><span class="na">withPlaintext</span><span class="o">(</span><span class="nc">ByteBuffer</span><span class="o">.</span><span class="na">wrap</span><span class="o">(</span><span class="n">plaintext</span><span class="o">.</span><span class="na">getBytes</span><span class="o">(</span><span class="nc">StandardCharsets</span><span class="o">.</span><span class="na">UTF_8</span><span class="o">)));</span>
            <span class="n">request</span><span class="o">.</span><span class="na">withEncryptionAlgorithm</span><span class="o">(</span><span class="nc">EncryptionAlgorithmSpec</span><span class="o">.</span><span class="na">RSAES_OAEP_SHA_256</span><span class="o">);</span>

            <span class="nc">EncryptResult</span> <span class="n">result</span> <span class="o">=</span> <span class="n">kmsClient</span><span class="o">.</span><span class="na">encrypt</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
            <span class="nc">ByteBuffer</span> <span class="n">ciphertextBlob</span> <span class="o">=</span> <span class="n">result</span><span class="o">.</span><span class="na">getCiphertextBlob</span><span class="o">();</span>

            <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"ciphertextBlob: "</span> <span class="o">+</span> <span class="k">new</span> <span class="nc">String</span><span class="o">(</span><span class="nc">Base64</span><span class="o">.</span><span class="na">encodeBase64</span><span class="o">(</span><span class="n">ciphertextBlob</span><span class="o">.</span><span class="na">array</span><span class="o">())));</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"encrypt fail: "</span> <span class="o">+</span> <span class="n">e</span><span class="o">.</span><span class="na">getMessage</span><span class="o">());</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="nd">@Test</span>
    <span class="kt">void</span> <span class="nf">decrypt</span><span class="o">()</span> <span class="o">{</span>
        <span class="kd">final</span> <span class="nc">String</span> <span class="n">encriptedText</span> <span class="o">=</span> <span class="s">"복호화할 값 (암호화된 값)"</span><span class="o">;</span>

        <span class="k">try</span> <span class="o">{</span>
            <span class="no">AWSKMS</span> <span class="n">kmsClient</span> <span class="o">=</span> <span class="nc">AWSKMSClientBuilder</span><span class="o">.</span><span class="na">standard</span><span class="o">()</span>
                    <span class="o">.</span><span class="na">withRegion</span><span class="o">(</span><span class="nc">Regions</span><span class="o">.</span><span class="na">AP_NORTHEAST_2</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">build</span><span class="o">();</span>

            <span class="nc">DecryptRequest</span> <span class="n">request</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DecryptRequest</span><span class="o">();</span>
            <span class="n">request</span><span class="o">.</span><span class="na">withCiphertextBlob</span><span class="o">(</span><span class="nc">ByteBuffer</span><span class="o">.</span><span class="na">wrap</span><span class="o">(</span><span class="nc">Base64</span><span class="o">.</span><span class="na">decodeBase64</span><span class="o">(</span><span class="n">encriptedText</span><span class="o">)));</span>
            <span class="n">request</span><span class="o">.</span><span class="na">withKeyId</span><span class="o">(</span><span class="no">KEY_ID</span><span class="o">);</span>
            <span class="n">request</span><span class="o">.</span><span class="na">withEncryptionAlgorithm</span><span class="o">(</span><span class="nc">EncryptionAlgorithmSpec</span><span class="o">.</span><span class="na">RSAES_OAEP_SHA_256</span><span class="o">);</span>
            <span class="nc">ByteBuffer</span> <span class="n">plainText</span> <span class="o">=</span> <span class="n">kmsClient</span><span class="o">.</span><span class="na">decrypt</span><span class="o">(</span><span class="n">request</span><span class="o">).</span><span class="na">getPlaintext</span><span class="o">();</span>

            <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"plainText: "</span> <span class="o">+</span> <span class="k">new</span> <span class="nc">String</span><span class="o">(</span><span class="n">plainText</span><span class="o">.</span><span class="na">array</span><span class="o">()));</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"decrypt fail: "</span> <span class="o">+</span> <span class="n">e</span><span class="o">.</span><span class="na">getMessage</span><span class="o">());</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>이제 테스트 코드를 이용해 암호화, 복호화 할 수 있다.</p>

<hr />

<h3 id="프로퍼티-값-암호화-하기">프로퍼티 값 암호화 하기</h3>

<p>위 테스트 코드를 통해 프로퍼티 값을 암호화 하자.</p>

<p>프로퍼티의 아래 값을 암호화 하면</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">url</span><span class="pi">:</span> <span class="s">jdbc:mysql://localhost:3306/jojae?serverTimezone=UTC&amp;characterEncoding=UTF-8</span>
</code></pre></div></div>

<p>대충 이런 형태의 값이 나오는데</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>lE2vSpNUiPPVWGb/F3p1zvK9vWIE8s.....길어서 줄임......SOht20JJMP9sGtBXwntdux7mA=
</code></pre></div></div>

<p>아래와 같이 암호화된 값을 넣어주면 되는데 앞에 <code class="language-plaintext highlighter-rouge">{cipher}</code> 를 포함해서 값을 넣어주면 된다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">url</span><span class="pi">:</span> <span class="s1">'</span><span class="s">{cipher}lE2vSpNUiPPVWGb/F3p1zvK9vWIE8s.....길어서</span><span class="nv"> </span><span class="s">줄임......SOht20JJMP9sGtBXwntdux7mA='</span>
</code></pre></div></div>

<p>런타임시에 모든 프로퍼티를 읽은 후 위의 라이브러리에서 제공되는 파서가 {cipher}가 포함된 프로퍼티 값을 이용하여 복호화 한다.</p>

<p>실행해보면 db가 정상적으로 연결되는 것을 확인 할 수 있다.</p>

<hr />

<h3 id="local-환경이-아닌-곳에서-사용하기">Local 환경이 아닌 곳에서 사용하기</h3>

<p>위에는 로컬환경 기준으로 하였는데 로컬환경에서 띄울게 아니라면 Role을 만들어 연결해 주자.<br />
위에서 iam user를 만들어서 해당 user에게 KMS 사용권한을 줬던 것 처럼<br />
Iam Role을 만들고 해당 Role을 연결 해 주면 된다.</p>

<hr />
<h2 id="끝">끝.</h2>]]></content><author><name>Jo JaeYoung</name></author><category term="java" /><category term="spring boot" /><category term="kms" /><category term="encrypt" /><summary type="html"><![CDATA[프로퍼티에 db 연결정보나 api key같은 정보가 그대로 들어갔다가 git 계정이 털리는 등 소스를 탈취 당하게 됬을 경우 아주 곤란해 질 수 있다. 보안이 필요한 값들은 애초에 암호화 해주면 그런 걱정을 덜 수 있겠다. AWS KMS를 이용해서 spring boot 프로젝트의 프로퍼티 값들을 암호화 해보자.]]></summary></entry><entry><title type="html">Kotest 해보기</title><link href="https://isntyet.github.io/kotlin/Kotest-%ED%95%B4%EB%B3%B4%EA%B8%B0/" rel="alternate" type="text/html" title="Kotest 해보기" /><published>2021-04-24T07:30:30+00:00</published><updated>2021-04-24T07:30:30+00:00</updated><id>https://isntyet.github.io/kotlin/Kotest-%ED%95%B4%EB%B3%B4%EA%B8%B0</id><content type="html" xml:base="https://isntyet.github.io/kotlin/Kotest-%ED%95%B4%EB%B3%B4%EA%B8%B0/"><![CDATA[<h3 id="kotest">Kotest</h3>

<p>새로 들어가게된 프로젝트가 코틀린으로 되어있어서,
해당 프로젝트에서 사용중인 kotest를 알아보기 위해 정리를 해보자.</p>

<p><a href="https://kotest.io/">Kotest.io 공식 사이트</a></p>

<hr />

<h3 id="kotest-들어가기-전에">kotest 들어가기 전에</h3>

<ul>
  <li>gradle dependencies 추가
    <div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dependencies</span> <span class="o">{</span>
  <span class="o">.</span>
  <span class="o">.</span>
  <span class="o">.</span>

  <span class="n">testImplementation</span><span class="o">(</span><span class="s2">"io.kotest:kotest-runner-junit5:$kotestVersion"</span><span class="o">)</span>
  <span class="n">testImplementation</span><span class="o">(</span><span class="s2">"io.kotest:kotest-assertions-core:$kotestVersion"</span><span class="o">)</span>
<span class="o">}</span>
</code></pre></div>    </div>
  </li>
  <li>intellij plugin 설치<br />
인텔리제이를 사용한다면 <code class="language-plaintext highlighter-rouge">Kotest</code> 플러그인을 설치해 주자
    <blockquote>
      <p>Preference → Plugins →  ‘Kotest’ 검색 → 설치</p>
    </blockquote>
  </li>
</ul>

<hr />

<h3 id="test-style">Test Style</h3>

<p>kotest에는 테스트 레이아웃이 10개 있는데 이중에 하나를 상속받아 진행한다.
여러 테스트 프레임워크에서 영향을 받아 만들어진 것도 있고, 코틀린만을 위해 만들어진 것도 있다.</p>

<p><a href="https://kotest.io/docs/framework/testing-styles.html#free-spec">Testing Styles</a></p>

<p>어떤 스타일을 고르던 기능적 차이는 없다. 취향에 따라, 팀 또는 개인의 스타일에 따라 고르면 될 듯 하다.</p>

<ul>
  <li>ex) FreeSpec으로 하려고 한다면</li>
</ul>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">internal</span> <span class="kd">class</span> <span class="nc">HumanTest</span> <span class="p">:</span> <span class="nc">FreeSpec</span><span class="p">()</span> <span class="p">{</span>

<span class="p">}</span>
</code></pre></div></div>
<p>아래부터 예제코드는 FreeSpec 기준으로 작성함.</p>

<hr />

<h3 id="전후-처리">전후 처리</h3>

<p>기존 @BeforeEach, @BeforeAll, @AfterEach 등과 같은 전후처리를 위한 기본 어노테이션을 사용하지않고
각 Spec의 SpecFunctionCallbacks 인터페이스에 의해 override를 하여 구현 할 수 있다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">interface</span> <span class="nc">SpecFunctionCallbacks</span> <span class="p">{</span>
   <span class="k">fun</span> <span class="nf">beforeSpec</span><span class="p">(</span><span class="n">spec</span><span class="p">:</span> <span class="nc">Spec</span><span class="p">)</span> <span class="p">{}</span>
   <span class="k">fun</span> <span class="nf">afterSpec</span><span class="p">(</span><span class="n">spec</span><span class="p">:</span> <span class="nc">Spec</span><span class="p">)</span> <span class="p">{}</span>
   <span class="k">fun</span> <span class="nf">beforeTest</span><span class="p">(</span><span class="n">testCase</span><span class="p">:</span> <span class="nc">TestCase</span><span class="p">)</span> <span class="p">{}</span>
   <span class="k">fun</span> <span class="nf">afterTest</span><span class="p">(</span><span class="n">testCase</span><span class="p">:</span> <span class="nc">TestCase</span><span class="p">,</span> <span class="n">result</span><span class="p">:</span> <span class="nc">TestResult</span><span class="p">)</span> <span class="p">{}</span>
   <span class="k">fun</span> <span class="nf">beforeContainer</span><span class="p">(</span><span class="n">testCase</span><span class="p">:</span> <span class="nc">TestCase</span><span class="p">)</span> <span class="p">{}</span>
   <span class="k">fun</span> <span class="nf">afterContainer</span><span class="p">(</span><span class="n">testCase</span><span class="p">:</span> <span class="nc">TestCase</span><span class="p">,</span> <span class="n">result</span><span class="p">:</span> <span class="nc">TestResult</span><span class="p">)</span> <span class="p">{}</span>
   <span class="k">fun</span> <span class="nf">beforeEach</span><span class="p">(</span><span class="n">testCase</span><span class="p">:</span> <span class="nc">TestCase</span><span class="p">)</span> <span class="p">{}</span>
   <span class="k">fun</span> <span class="nf">afterEach</span><span class="p">(</span><span class="n">testCase</span><span class="p">:</span> <span class="nc">TestCase</span><span class="p">,</span> <span class="n">result</span><span class="p">:</span> <span class="nc">TestResult</span><span class="p">)</span> <span class="p">{}</span>
   <span class="k">fun</span> <span class="nf">beforeAny</span><span class="p">(</span><span class="n">testCase</span><span class="p">:</span> <span class="nc">TestCase</span><span class="p">)</span> <span class="p">{}</span>
   <span class="k">fun</span> <span class="nf">afterAny</span><span class="p">(</span><span class="n">testCase</span><span class="p">:</span> <span class="nc">TestCase</span><span class="p">,</span> <span class="n">result</span><span class="p">:</span> <span class="nc">TestResult</span><span class="p">)</span> <span class="p">{}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>위 인터페이스를 참고하여 작성해보면 아래와 같이 사용 할 수 있다.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">internal</span> <span class="kd">class</span> <span class="nc">HumanTest</span> <span class="p">:</span> <span class="nc">FreeSpec</span><span class="p">()</span> <span class="p">{</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">beforeSpec</span><span class="p">(</span><span class="n">spec</span><span class="p">:</span> <span class="nc">Spec</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"beforeSpec"</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">beforeTest</span><span class="p">(</span><span class="n">testCase</span><span class="p">:</span> <span class="nc">TestCase</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"beforeTest"</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">beforeContainer</span><span class="p">(</span><span class="n">testCase</span><span class="p">:</span> <span class="nc">TestCase</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"beforeContainer"</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">beforeEach</span><span class="p">(</span><span class="n">testCase</span><span class="p">:</span> <span class="nc">TestCase</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"beforeEach"</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">override</span> <span class="k">fun</span> <span class="nf">beforeAny</span><span class="p">(</span><span class="n">testCase</span><span class="p">:</span> <span class="nc">TestCase</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"beforeAny"</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="nf">init</span> <span class="p">{</span>
        <span class="s">"그냥 컨테이너"</span> <span class="p">-</span> <span class="p">{</span>
            <span class="s">"그냥 테스트1"</span> <span class="p">{</span>
                <span class="nf">println</span><span class="p">(</span><span class="s">"그냥 테스트1"</span><span class="p">)</span>
                <span class="s">""</span><span class="p">.</span><span class="n">length</span> <span class="n">shouldBe</span> <span class="mi">0</span>
            <span class="p">}</span>
            <span class="s">"그냥 테스트2"</span> <span class="p">{</span>
                <span class="nf">println</span><span class="p">(</span><span class="s">"그냥 테스트2"</span><span class="p">)</span>
                <span class="s">"12345"</span><span class="p">.</span><span class="n">length</span> <span class="n">shouldBe</span> <span class="mi">5</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>결과</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">실행결과</span>

<span class="n">beforeSpec</span>

<span class="n">beforeContainer</span>
<span class="n">beforeAny</span>
<span class="n">beforeTest</span>

<span class="n">beforeEach</span>
<span class="n">beforeAny</span>
<span class="n">beforeTest</span>
<span class="err">그냥</span> <span class="err">테스트</span><span class="mi">1</span>

<span class="n">beforeEach</span>
<span class="n">beforeAny</span>
<span class="n">beforeTest</span>
<span class="err">그냥</span> <span class="err">테스트</span><span class="mi">2</span>
</code></pre></div></div>

<p>결과를 보면 각 fun들이 어느시점에 실행되는지 확인 가능하다.</p>

<p>AnnotationSpec 을 사용하면 아래와 같이 사용도 가능하다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">internal</span> <span class="kd">class</span> <span class="nc">HumanTest</span> <span class="p">:</span> <span class="nc">AnnotationSpec</span><span class="p">()</span> <span class="p">{</span>

    <span class="nd">@BeforeEach</span>
    <span class="k">fun</span> <span class="nf">beforeTest</span><span class="p">()</span> <span class="p">{</span>
        <span class="nf">println</span><span class="p">(</span><span class="s">"Before each test"</span><span class="p">)</span>
    <span class="p">}</span>
    
    <span class="nf">init</span><span class="p">{</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h3 id="assertion-알아보기">Assertion 알아보기</h3>

<p>kotest는 아주 풍부한 assertion을 제공하는데, 몇가지 assertion 사용법에 대해 알아보자.</p>

<p><a href="https://kotest.io/docs/assertions/assertions.html">Assertions</a></p>

<p>assertion을 다 알아보기에는 너무 많으니 예제로 대체한다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">init</span> <span class="p">{</span>
    <span class="s">"Matchers"</span> <span class="p">-</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">testStr</span> <span class="p">=</span> <span class="s">"I am iron man"</span>
        <span class="kd">val</span> <span class="py">testNum</span> <span class="p">=</span> <span class="mi">5</span>
        <span class="kd">val</span> <span class="py">testList</span> <span class="p">=</span> <span class="n">listOf</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">&gt;(</span><span class="s">"iron"</span><span class="p">,</span> <span class="s">"bronze"</span><span class="p">,</span> <span class="s">"silver"</span><span class="p">)</span>

        <span class="s">"일치 하는지"</span> <span class="p">{</span>
            <span class="n">testStr</span> <span class="n">shouldBe</span> <span class="s">"I am iron man"</span>
        <span class="p">}</span>
        <span class="s">"일치 안 하는지"</span> <span class="p">{</span>
            <span class="n">testStr</span> <span class="n">shouldNotBe</span> <span class="s">"I am silver man"</span>
        <span class="p">}</span>
        <span class="s">"해당 문자열로 시작하는지"</span> <span class="p">{</span>
            <span class="n">testStr</span> <span class="n">shouldStartWith</span> <span class="s">"I am"</span>
        <span class="p">}</span>
        <span class="s">"해당 문자열을 포함하는지"</span> <span class="p">{</span>
            <span class="n">testStr</span> <span class="n">shouldContain</span> <span class="s">"iron"</span>
        <span class="p">}</span>
        <span class="s">"리스트에서 해당 리스트의 값들이 모두 포함되는지"</span> <span class="p">{</span>
            <span class="n">testList</span> <span class="n">shouldContainAll</span> <span class="nf">listOf</span><span class="p">(</span><span class="s">"iron"</span><span class="p">,</span> <span class="s">"silver"</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="s">"대소문자 무시하고 일치하는지"</span> <span class="p">{</span>
            <span class="n">testStr</span> <span class="n">shouldBeEqualIgnoringCase</span> <span class="s">"I AM IRON MAN"</span>
        <span class="p">}</span>
        <span class="s">"보다 큰거나 같은지"</span> <span class="p">{</span>
            <span class="n">testNum</span> <span class="n">shouldBeGreaterThanOrEqualTo</span> <span class="mi">3</span>
        <span class="p">}</span>
        <span class="s">"해당 문자열과 길이가 같은지"</span> <span class="p">{</span>
            <span class="n">testStr</span> <span class="n">shouldHaveSameLengthAs</span> <span class="s">"I AM SUPERMAN"</span>
        <span class="p">}</span>
        <span class="s">"문자열 길이"</span> <span class="p">{</span>
            <span class="n">testStr</span> <span class="n">shouldHaveLength</span> <span class="mi">13</span>
        <span class="p">}</span>
        <span class="s">"여러개 체이닝"</span> <span class="p">{</span>
            <span class="n">testStr</span><span class="p">.</span><span class="nf">shouldStartWith</span><span class="p">(</span><span class="s">"I"</span><span class="p">).</span><span class="nf">shouldHaveLength</span><span class="p">(</span><span class="mi">13</span><span class="p">).</span><span class="nf">shouldContainIgnoringCase</span><span class="p">(</span><span class="s">"IRON"</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Exception 발생하는지도 체크 가능하다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"Exception"</span> <span class="p">-</span> <span class="p">{</span>
    <span class="s">"ArithmeticException Exception 발생하는지"</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">exception</span> <span class="p">=</span> <span class="n">shouldThrow</span><span class="p">&lt;</span><span class="nc">ArithmeticException</span><span class="p">&gt;</span> <span class="p">{</span>
            <span class="mi">1</span> <span class="p">/</span> <span class="mi">0</span>
        <span class="p">}</span>
        <span class="n">exception</span><span class="p">.</span><span class="n">message</span> <span class="nf">shouldStartWith</span><span class="p">(</span><span class="s">"/ by zero"</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="s">"어떤 Exception이든 발생하는지"</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">exception</span> <span class="p">=</span> <span class="nf">shouldThrowAny</span> <span class="p">{</span>
            <span class="mi">1</span> <span class="p">/</span> <span class="mi">0</span>
        <span class="p">}</span>
        <span class="n">exception</span><span class="p">.</span><span class="n">message</span> <span class="nf">shouldStartWith</span><span class="p">(</span><span class="s">"/ by zero"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Clues를 이용해서 에러메세지에 실마리?를 남길 수 도 있다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"Clues"</span> <span class="p">-</span> <span class="p">{</span>
    <span class="kd">data class</span> <span class="nc">HttpResponse</span><span class="p">(</span><span class="kd">val</span> <span class="py">status</span><span class="p">:</span> <span class="nc">Int</span><span class="p">,</span> <span class="kd">val</span> <span class="py">body</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">response</span> <span class="p">=</span> <span class="nc">HttpResponse</span><span class="p">(</span><span class="mi">404</span><span class="p">,</span> <span class="s">"the content"</span><span class="p">)</span>
    
    <span class="s">"Not Use Clues"</span> <span class="p">{</span>
        <span class="n">response</span><span class="p">.</span><span class="n">status</span> <span class="n">shouldBe</span> <span class="mi">200</span>
        <span class="n">response</span><span class="p">.</span><span class="n">body</span> <span class="n">shouldBe</span> <span class="s">"the content"</span>
        <span class="c1">// 결과: expected:&lt;200&gt; but was:&lt;404&gt;</span>
    <span class="p">}</span>
    <span class="s">"With Clues"</span> <span class="p">{</span>
        <span class="nf">withClue</span><span class="p">(</span><span class="s">"status는 200이여야 되고 body는 'the content'여야 한다"</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">response</span><span class="p">.</span><span class="n">status</span> <span class="n">shouldBe</span> <span class="mi">200</span>
            <span class="n">response</span><span class="p">.</span><span class="n">body</span> <span class="n">shouldBe</span> <span class="s">"the content"</span>
        <span class="p">}</span>
        <span class="c1">// 결과: status는 200이여야 되고 body는 'the content'여야 한다</span>
    <span class="p">}</span>
    <span class="s">"As Clues"</span> <span class="p">{</span>
        <span class="n">response</span><span class="p">.</span><span class="nf">asClue</span> <span class="p">{</span>
            <span class="n">it</span><span class="p">.</span><span class="n">status</span> <span class="n">shouldBe</span> <span class="mi">200</span>
            <span class="n">it</span><span class="p">.</span><span class="n">body</span> <span class="n">shouldBe</span> <span class="s">"the content"</span>
        <span class="p">}</span>
        <span class="c1">// 결과: HttpResponse(status=404, body=the content)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>위의 결과(주석) 처럼 test실패 했을 때 더 자세한 단서를 남길 수 있다.</p>

<p>Soft Assertion을 사용하면 중간에 asert가 실패해도 끝까지 체크가 가능하다. assertAll 처럼</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"Soft Assertions"</span> <span class="p">-</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">testStr</span> <span class="p">=</span> <span class="s">"I am iron man"</span>
    <span class="kd">val</span> <span class="py">testNum</span> <span class="p">=</span> <span class="mi">5</span>

    <span class="s">"Not Soft"</span> <span class="p">{</span>
        <span class="n">testStr</span> <span class="n">shouldBe</span> <span class="s">"IronMan"</span>
        <span class="n">testNum</span> <span class="n">shouldBe</span> <span class="mi">1</span>
        <span class="c1">// 결과: expected:&lt;"IronMan"&gt; but was:&lt;"I am iron man"&gt;</span>
    <span class="p">}</span>
    <span class="s">"Use Soft"</span> <span class="p">{</span>
        <span class="nf">assertSoftly</span> <span class="p">{</span>
            <span class="n">testStr</span> <span class="n">shouldBe</span> <span class="s">"IronMan"</span>
            <span class="n">testNum</span> <span class="n">shouldBe</span> <span class="mi">1</span>
        <span class="p">}</span>
        <span class="c1">// 결과: expected:&lt;"IronMan"&gt; but was:&lt;"I am iron man"&gt;</span>
        <span class="c1">//      expected:&lt;1&gt; but was:&lt;5&gt;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h3 id="data-driven-testing">Data Driven Testing</h3>

<p>아래 기능을 이용해서 다른 매개변수를 정의하여 각각 테스트가 가능하다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"data test"</span> <span class="p">-</span> <span class="p">{</span>
    <span class="s">"forAll"</span> <span class="p">{</span>
        <span class="nf">forAll</span><span class="p">(</span>
            <span class="nf">row</span><span class="p">(</span><span class="s">"haha"</span><span class="p">,</span> <span class="mi">13</span><span class="p">),</span>
            <span class="nf">row</span><span class="p">(</span><span class="s">"hoho"</span><span class="p">,</span> <span class="mi">22</span><span class="p">),</span>
        <span class="p">)</span> <span class="p">{</span> <span class="n">name</span><span class="p">,</span> <span class="n">age</span> <span class="p">-&gt;</span>
            <span class="n">name</span><span class="p">.</span><span class="n">length</span> <span class="n">shouldBe</span> <span class="mi">4</span>
            <span class="n">age</span> <span class="n">shouldBeGreaterThanOrEqualTo</span> <span class="mi">10</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="s">"table forAll"</span> <span class="p">{</span>
        <span class="nf">table</span><span class="p">(</span>
            <span class="nf">headers</span><span class="p">(</span><span class="s">"name"</span><span class="p">,</span> <span class="s">"age"</span><span class="p">),</span>
            <span class="nf">row</span><span class="p">(</span><span class="s">"haha"</span><span class="p">,</span> <span class="mi">13</span><span class="p">),</span>
            <span class="nf">row</span><span class="p">(</span><span class="s">"hoho"</span><span class="p">,</span> <span class="mi">22</span><span class="p">)</span>
        <span class="p">).</span><span class="nf">forAll</span> <span class="p">{</span> <span class="n">name</span><span class="p">,</span> <span class="n">age</span> <span class="p">-&gt;</span>
            <span class="n">name</span><span class="p">.</span><span class="n">length</span> <span class="n">shouldBe</span> <span class="mi">4</span>
            <span class="n">age</span> <span class="n">shouldBeGreaterThanOrEqualTo</span> <span class="mi">10</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="s">"collection"</span> <span class="p">{</span>
        <span class="nf">listOf</span><span class="p">(</span>
            <span class="nf">row</span><span class="p">(</span><span class="s">"haha"</span><span class="p">,</span> <span class="mi">13</span><span class="p">),</span>
            <span class="nf">row</span><span class="p">(</span><span class="s">"hoho"</span><span class="p">,</span> <span class="mi">22</span><span class="p">)</span>
        <span class="p">).</span><span class="nf">map</span> <span class="p">{</span> <span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span> <span class="n">age</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">-&gt;</span>
            <span class="n">name</span><span class="p">.</span><span class="n">length</span> <span class="n">shouldBe</span> <span class="mi">4</span>
            <span class="n">age</span> <span class="n">shouldBeGreaterThanOrEqualTo</span> <span class="mi">10</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>이렇게 데이터를 세팅하고, 각 행 별로 테스트 할 수 있다.</p>

<hr />

<p>아주 간단한 것 만 해보았는데, 문서를 보면 해보지 않은 여러가지 기능, 장점들(특정 주기로 테스트, Generators를 이용한 속성 기반 테스트, 광범위한 확장성 등)이 많아서 필요할 때 참고해 보는것도 좋을 것 같다.</p>]]></content><author><name>Jo JaeYoung</name></author><category term="kotlin" /><category term="kotlin" /><category term="kotest" /><category term="test" /><summary type="html"><![CDATA[Kotest]]></summary></entry><entry><title type="html">JPA 비관적 잠금(Pessimistic Lock)</title><link href="https://isntyet.github.io/jpa/JPA-%EB%B9%84%EA%B4%80%EC%A0%81-%EC%9E%A0%EA%B8%88(Pessimistic-Lock)/" rel="alternate" type="text/html" title="JPA 비관적 잠금(Pessimistic Lock)" /><published>2020-10-30T07:30:30+00:00</published><updated>2020-10-30T07:30:30+00:00</updated><id>https://isntyet.github.io/jpa/JPA-%EB%B9%84%EA%B4%80%EC%A0%81-%EC%9E%A0%EA%B8%88(Pessimistic-Lock)</id><content type="html" xml:base="https://isntyet.github.io/jpa/JPA-%EB%B9%84%EA%B4%80%EC%A0%81-%EC%9E%A0%EA%B8%88(Pessimistic-Lock)/"><![CDATA[<h3 id="비관적-잠금pessimistic-lock-이란">비관적 잠금(Pessimistic Lock) 이란?</h3>
<ul>
  <li>선점 잠금이라고 불리기도 함</li>
  <li>트랜잭션끼리의 충돌이 발생한다고 가정하고 우선 락을 거는 방법</li>
  <li>DB에서 제공하는 락기능을 사용</li>
</ul>

<hr />

<h3 id="참고">참고</h3>

<ul>
  <li>Repository 참고
    <ul>
      <li><a href="https://github.com/isntyet/java-practice">java-practice</a></li>
      <li>Home domain 참고</li>
      <li>inmemory db는 h2사용 (쿼리는 schema.sql, data.sql 참고)</li>
      <li>db console은 http://localhost:8080/h2 로 접속</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="lock-걸지-않고-시도해보기">Lock 걸지 않고 시도해보기</h3>

<ul>
  <li>Home (Entity)
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Entity</span>
<span class="nd">@Getter</span>
<span class="nd">@NoArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Home</span> <span class="o">{</span>
  <span class="nd">@Id</span>
  <span class="nd">@GeneratedValue</span><span class="o">(</span><span class="n">strategy</span> <span class="o">=</span> <span class="nc">GenerationType</span><span class="o">.</span><span class="na">AUTO</span><span class="o">)</span>
  <span class="kd">private</span> <span class="nc">Long</span> <span class="n">idx</span><span class="o">;</span>
  <span class="kd">private</span> <span class="nc">String</span> <span class="n">name</span><span class="o">;</span>
  <span class="kd">private</span> <span class="nc">String</span> <span class="n">address</span><span class="o">;</span>
  <span class="kd">private</span> <span class="kt">int</span> <span class="n">price</span><span class="o">;</span>

  <span class="kd">public</span> <span class="nf">Home</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">,</span> <span class="nc">String</span> <span class="n">address</span><span class="o">,</span> <span class="kt">int</span> <span class="n">price</span><span class="o">)</span> <span class="o">{</span>
      <span class="k">this</span><span class="o">.</span><span class="na">name</span> <span class="o">=</span> <span class="n">name</span><span class="o">;</span>
      <span class="k">this</span><span class="o">.</span><span class="na">address</span> <span class="o">=</span> <span class="n">address</span><span class="o">;</span>
      <span class="k">this</span><span class="o">.</span><span class="na">price</span> <span class="o">=</span> <span class="n">price</span><span class="o">;</span>
  <span class="o">}</span>

  <span class="kd">public</span> <span class="kt">int</span> <span class="nf">decreasePrice</span><span class="o">(</span><span class="kt">int</span> <span class="n">price</span><span class="o">)</span> <span class="o">{</span>
      <span class="k">if</span> <span class="o">(</span><span class="k">this</span><span class="o">.</span><span class="na">price</span> <span class="o">-</span> <span class="n">price</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span>
          <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"가격이 부족해"</span><span class="o">);</span>
      <span class="o">}</span>
      <span class="k">return</span> <span class="k">this</span><span class="o">.</span><span class="na">price</span> <span class="o">-=</span> <span class="n">price</span><span class="o">;</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div>    </div>
  </li>
  <li>HomeRepository
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">HomeRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">Home</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="nc">Home</span> <span class="nf">findByName</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div>    </div>
  </li>
  <li>HomeService.class
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="nd">@Slf4j</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">HomeService</span> <span class="o">{</span>
  <span class="kd">private</span> <span class="kd">final</span> <span class="nc">HomeRepository</span> <span class="n">homeRepository</span><span class="o">;</span>

  <span class="nd">@Transactional</span>
  <span class="kd">public</span> <span class="kt">int</span> <span class="nf">currentPrice</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
      <span class="nc">Home</span> <span class="n">home</span> <span class="o">=</span> <span class="n">homeRepository</span><span class="o">.</span><span class="na">findByName</span><span class="o">(</span><span class="n">name</span><span class="o">);</span>
      <span class="k">return</span> <span class="n">home</span><span class="o">.</span><span class="na">getPrice</span><span class="o">();</span>
  <span class="o">}</span>

  <span class="nd">@Transactional</span>
  <span class="kd">public</span> <span class="kt">int</span> <span class="nf">decreasePrice</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">,</span> <span class="kt">int</span> <span class="n">price</span><span class="o">)</span> <span class="o">{</span>
      <span class="nc">Home</span> <span class="n">home</span> <span class="o">=</span> <span class="n">homeRepository</span><span class="o">.</span><span class="na">findWithNameForUpdate</span><span class="o">(</span><span class="n">name</span><span class="o">);</span>
      <span class="n">home</span><span class="o">.</span><span class="na">decreasePrice</span><span class="o">(</span><span class="n">price</span><span class="o">);</span>
      <span class="k">return</span> <span class="n">home</span><span class="o">.</span><span class="na">getPrice</span><span class="o">();</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div>    </div>
    <p>이름과 가격을 입력하면 해당하는 집의 가격이 깎이는 기능을 만들어주자.</p>
  </li>
  <li>HomeController.class
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@RestController</span>
<span class="nd">@Slf4j</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/home"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">HomeController</span> <span class="o">{</span>
  <span class="kd">private</span> <span class="kd">final</span> <span class="nc">HomeService</span> <span class="n">homeService</span><span class="o">;</span>

  <span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/decrease"</span><span class="o">)</span>
  <span class="kd">public</span> <span class="nc">String</span> <span class="nf">decreasePrice</span><span class="o">(</span><span class="nd">@RequestParam</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"name"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">name</span><span class="o">,</span> <span class="nd">@RequestParam</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"price"</span><span class="o">)</span> <span class="kt">int</span> <span class="n">price</span><span class="o">)</span> <span class="o">{</span>
      <span class="nc">String</span> <span class="n">result</span><span class="o">;</span>
      <span class="k">try</span> <span class="o">{</span>
          <span class="n">homeService</span><span class="o">.</span><span class="na">decreasePrice</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">price</span><span class="o">);</span>
          <span class="n">result</span> <span class="o">=</span> <span class="s">"현재 가격 : "</span> <span class="o">+</span> <span class="n">homeService</span><span class="o">.</span><span class="na">currentPrice</span><span class="o">(</span><span class="n">name</span><span class="o">);</span>
      <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
          <span class="n">result</span> <span class="o">=</span> <span class="n">e</span><span class="o">.</span><span class="na">getMessage</span><span class="o">();</span>
      <span class="o">}</span>
      <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="n">result</span><span class="o">);</span>
      <span class="k">return</span> <span class="n">result</span><span class="o">;</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div>    </div>
    <p>여러번 call을 해보기위한 컨트롤러도 만들어주자.</p>
  </li>
  <li>
    <p>실행, 테스트 해보기<br />
<strong><em>‘한옥’이라는 집에 1000원을 동시에! 여러번! 차감 테스트해보자.</em></strong></p>

    <p>해당 어플리케이션을 실행하고 터미널에 curl을 이용해서 동시에 여러번 호출을 해보자.<br />
터미널 창을 열고<br />
<code class="language-plaintext highlighter-rouge">curl url &amp; curl url &amp; curl url &amp; ....</code> 이런식으로 입력해주면 간단하게 테스트가 가능하다.</p>
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="s1">'http://localhost:8080/home/decrease?name=%ED%95%9C%EC%98%A5&amp;price=1000'</span> &amp; curl <span class="s1">'http://localhost:8080/home/decrease?name=%ED%95%9C%EC%98%A5&amp;price=1000'</span> &amp; curl <span class="s1">'http://localhost:8080/home/decrease?name=%ED%95%9C%EC%98%A5&amp;price=1000'</span> &amp; curl <span class="s1">'http://localhost:8080/home/decrease?name=%ED%95%9C%EC%98%A5&amp;price=1000'</span> &amp; curl <span class="s1">'http://localhost:8080/home/decrease?name=%ED%95%9C%EC%98%A5&amp;price=1000'</span>
</code></pre></div>    </div>
    <p><img src="/assets/images/JPA 비관적 잠금(Pessimistic Lock)/0.png" alt="터미널에서 호출해보기" /></p>
  </li>
  <li>실행 결과
 <img src="/assets/images/JPA 비관적 잠금(Pessimistic Lock)/1.png" alt="콘솔 로그 보기" /><br />
 <img src="/assets/images/JPA 비관적 잠금(Pessimistic Lock)/2.png" alt="디비 보기" /><br />
 처음 <code class="language-plaintext highlighter-rouge">한옥</code>의 값은 <code class="language-plaintext highlighter-rouge">20000원</code>을 가지고 있었다.<br />
 다섯번을 호출했으니 15000천원이 남아있어야 되지만 남은돈은 <code class="language-plaintext highlighter-rouge">19000원</code>이다.<br />
 모든 트랜잭션이 동시에 20000원을 읽어서 1000을 뺐기때문에,<br />
 다 19000원으로 업데이트 된것이다.</li>
</ul>

<hr />

<h3 id="비관적-락-구현해보기">비관적 락 구현해보기</h3>
<p>이제 위의 소스를 수정해서 비관적 락을 구현해보자.</p>

<ul>
  <li>HomeRepository
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">HomeRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">Home</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="nc">Home</span> <span class="nf">findByName</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">);</span>

    <span class="nd">@Lock</span><span class="o">(</span><span class="nc">LockModeType</span><span class="o">.</span><span class="na">PESSIMISTIC_WRITE</span><span class="o">)</span>
    <span class="nd">@Query</span><span class="o">(</span><span class="s">"select h from Home h where h.name = :name"</span><span class="o">)</span>
    <span class="nc">Home</span> <span class="nf">findWithNameForUpdate</span><span class="o">(</span><span class="nd">@Param</span><span class="o">(</span><span class="s">"name"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">name</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div>    </div>
    <p>비관적 잠금을 하기 위해 업데이트용 find method를 구현하고<br />
해당 메소드에 @Lock 어노테이션과 모드를 설정해주자.<br />
LockModeType은 아래에서 다시 설명.</p>
  </li>
  <li>HomeService
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="nd">@Slf4j</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">HomeService</span> <span class="o">{</span>
  <span class="kd">private</span> <span class="kd">final</span> <span class="nc">HomeRepository</span> <span class="n">homeRepository</span><span class="o">;</span>

  <span class="nd">@Transactional</span>
  <span class="kd">public</span> <span class="kt">int</span> <span class="nf">currentPrice</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
      <span class="nc">Home</span> <span class="n">home</span> <span class="o">=</span> <span class="n">homeRepository</span><span class="o">.</span><span class="na">findByName</span><span class="o">(</span><span class="n">name</span><span class="o">);</span>
      <span class="k">return</span> <span class="n">home</span><span class="o">.</span><span class="na">getPrice</span><span class="o">();</span>
  <span class="o">}</span>

  <span class="nd">@Transactional</span>
  <span class="kd">public</span> <span class="kt">int</span> <span class="nf">decreasePrice</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">,</span> <span class="kt">int</span> <span class="n">price</span><span class="o">)</span> <span class="o">{</span>
      <span class="nc">Home</span> <span class="n">home</span> <span class="o">=</span> <span class="n">homeRepository</span><span class="o">.</span><span class="na">findWithNameForUpdate</span><span class="o">(</span><span class="n">name</span><span class="o">);</span> <span class="c1">//수정</span>
      <span class="n">home</span><span class="o">.</span><span class="na">decreasePrice</span><span class="o">(</span><span class="n">price</span><span class="o">);</span>
      <span class="k">return</span> <span class="n">home</span><span class="o">.</span><span class="na">getPrice</span><span class="o">();</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>위에 했던 curl테스트 다시 진행 후의 콘솔로그
<img src="/assets/images/JPA 비관적 잠금(Pessimistic Lock)/3.png" alt="콘솔 로그 보기" /><br />
결과를 보면 5번을 시도하여 낙관적 락 일때와는 다르게 전부 순차적으로 가격이 차감된것을 확인 할 수 있다.</p>
  </li>
  <li>이유
    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Hibernate</span><span class="p">:</span>
    <span class="k">select</span>
        <span class="n">home0_</span><span class="p">.</span><span class="n">idx</span> <span class="k">as</span> <span class="n">idx1_0_</span><span class="p">,</span>
        <span class="n">home0_</span><span class="p">.</span><span class="n">address</span> <span class="k">as</span> <span class="n">address2_0_</span><span class="p">,</span>
        <span class="n">home0_</span><span class="p">.</span><span class="n">name</span> <span class="k">as</span> <span class="n">name3_0_</span><span class="p">,</span>
        <span class="n">home0_</span><span class="p">.</span><span class="n">price</span> <span class="k">as</span> <span class="n">price4_0_</span>
    <span class="k">from</span>
        <span class="n">home</span> <span class="n">home0_</span>
    <span class="k">where</span>
        <span class="n">home0_</span><span class="p">.</span><span class="n">name</span><span class="o">=?</span> <span class="k">for</span> <span class="k">update</span>
</code></pre></div>    </div>
    <p>위 쿼리는 find 실행될 때 찍어본 쿼리인데,
<strong><em>SELECT FOR ~ UPDATE</em></strong> 쿼리가 나가는것을 확인할 수 있다.<br />
<strong><em>SELECT FOR UPDATE</em></strong>=<em>동시성 제어를 위해 특정row에 배타적 LOCK을 거는 행위</em><br />
<strong><em>“데이터 수정하려고 찾은 것이니, 다른분들은 건드리지 마세요!”</em></strong></p>
  </li>
</ul>

<hr />

<h3 id="lockmode-종류">LockMode 종류</h3>

<ul>
  <li>
    <p>LockModeType.PESSIMISTIC_WRITE<br />
일반적인 옵션. 데이터베이스에 쓰기 락<br />
다른 트랜잭션에서 읽기도 쓰기도 못함. (배타적 잠금)</p>
  </li>
  <li>
    <p>LockModeType.PESSIMISTIC_READ<br />
반복 읽기만하고 수정하지 않는 용도로 락을 걸 때 사용<br />
다른 트랜잭션에서 읽기는 가능함. (공유 잠금)</p>
  </li>
  <li>
    <p>LockModeType.PESSINISTIC_FORCE_INCREMENT<br />
Version 정보를 사용하는 비관적 락</p>
  </li>
</ul>

<hr />

<h3 id="테스트-코드-작성">테스트 코드 작성</h3>

<ul>
  <li>HomeServiceTest
    <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@SpringBootTest</span>
<span class="kd">class</span> <span class="nc">HomeServiceTest</span> <span class="o">{</span>
  <span class="nd">@Autowired</span>
  <span class="nc">HomeService</span> <span class="n">homeService</span><span class="o">;</span>

  <span class="nd">@Autowired</span>
  <span class="nc">HomeRepository</span> <span class="n">homeRepository</span><span class="o">;</span>

  <span class="nd">@BeforeEach</span>
  <span class="kt">void</span> <span class="nf">beforeEach</span><span class="o">()</span> <span class="o">{</span>
      <span class="nc">Home</span> <span class="n">home</span> <span class="o">=</span> <span class="nc">Home</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
              <span class="o">.</span><span class="na">name</span><span class="o">(</span><span class="s">"양옥"</span><span class="o">)</span>
              <span class="o">.</span><span class="na">address</span><span class="o">(</span><span class="s">"address"</span><span class="o">)</span>
              <span class="o">.</span><span class="na">price</span><span class="o">(</span><span class="mi">20000</span><span class="o">)</span>
              <span class="o">.</span><span class="na">build</span><span class="o">();</span>

      <span class="n">homeRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">home</span><span class="o">);</span>
  <span class="o">}</span>

  <span class="nd">@Test</span>
  <span class="nd">@DisplayName</span><span class="o">(</span><span class="s">"가격 줄여보기(멀티 스레드) 테스트"</span><span class="o">)</span>
  <span class="kt">void</span> <span class="nf">decreasePriceForMultiThreadTest</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">InterruptedException</span> <span class="o">{</span>
      <span class="nc">AtomicInteger</span> <span class="n">successCount</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AtomicInteger</span><span class="o">();</span>
      <span class="kt">int</span> <span class="n">numberOfExecute</span> <span class="o">=</span> <span class="mi">100</span><span class="o">;</span>
      <span class="nc">ExecutorService</span> <span class="n">service</span> <span class="o">=</span> <span class="nc">Executors</span><span class="o">.</span><span class="na">newFixedThreadPool</span><span class="o">(</span><span class="mi">10</span><span class="o">);</span>
      <span class="nc">CountDownLatch</span> <span class="n">latch</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CountDownLatch</span><span class="o">(</span><span class="n">numberOfExecute</span><span class="o">);</span>

      <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">numberOfExecute</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
          <span class="n">service</span><span class="o">.</span><span class="na">execute</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="o">{</span>
              <span class="k">try</span> <span class="o">{</span>
                  <span class="n">homeService</span><span class="o">.</span><span class="na">decreasePrice</span><span class="o">(</span><span class="s">"양옥"</span><span class="o">,</span> <span class="mi">1000</span><span class="o">);</span>
                  <span class="n">successCount</span><span class="o">.</span><span class="na">getAndIncrement</span><span class="o">();</span>
                  <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"성공"</span><span class="o">);</span>
              <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
                  <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">e</span><span class="o">);</span>
              <span class="o">}</span>
              <span class="n">latch</span><span class="o">.</span><span class="na">countDown</span><span class="o">();</span>
          <span class="o">});</span>
      <span class="o">}</span>
      <span class="n">latch</span><span class="o">.</span><span class="na">await</span><span class="o">();</span>

      <span class="n">assertThat</span><span class="o">(</span><span class="n">successCount</span><span class="o">.</span><span class="na">get</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="mi">20</span><span class="o">);</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div>    </div>
    <p>이렇게 스레드풀을 생성하고 비동기적으로 여러번 실행시켜보는것으로 테스트가 가능할것 같다.<br />
총 100번을 시도하는데, 20000원에서 1000원씩 20번만 성공하고<br />
이미 20번 성공한 후의 시도에서는 가격이 부족하다고 출력된다.<br />
이렇게해서 성공 카운트는 딱 20번이 되게된다.</p>
  </li>
</ul>

<hr />]]></content><author><name>Jo JaeYoung</name></author><category term="jpa" /><category term="jpa" /><category term="pessimistic lock" /><summary type="html"><![CDATA[비관적 잠금(Pessimistic Lock) 이란? 선점 잠금이라고 불리기도 함 트랜잭션끼리의 충돌이 발생한다고 가정하고 우선 락을 거는 방법 DB에서 제공하는 락기능을 사용]]></summary></entry></feed>