<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>cooper25_dev.log</title>
        <link>https://velog.io/</link>
        <description>막연함을 명료함으로 만드는 공간 😃</description>
        <lastBuildDate>Sat, 30 Dec 2023 13:34:36 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>cooper25_dev.log</title>
            <url>https://velog.velcdn.com/images/cooper25_dev/profile/56e63b31-c5cc-48b4-aca9-d0219b810374/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. cooper25_dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/cooper25_dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[테스트에서 @Sql 로 테스트 데이터가 들어가지 않았던 이유 (with. custom TestExecutionListener)]]></title>
            <link>https://velog.io/@cooper25_dev/%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%97%90%EC%84%9C-Sql-%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%EB%93%A4%EC%96%B4%EA%B0%80%EC%A7%80-%EC%95%8A%EC%95%98%EB%8D%98-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@cooper25_dev/%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%97%90%EC%84%9C-Sql-%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%EB%93%A4%EC%96%B4%EA%B0%80%EC%A7%80-%EC%95%8A%EC%95%98%EB%8D%98-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sat, 30 Dec 2023 13:34:36 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<p>친구와 토이 프로젝트를 하며 테스트를 위한 데이터를 추가하고자 @Sql annotation 을 적용하고자 했다. 하지만 테스트 데이터가 들어가지 않는 이유를 찾기 위한 삽질 로그를 기록하고자 한다.</p>
<h2 id="background">Background</h2>
<h3 id="1-springboottestwebenvironmentrandom_port-사용">1. SpringBootTest.WebEnvironment.RANDOM_PORT 사용</h3>
<p>실제 개발 환경과 유사한 환경을 위해 <code>RANDOM_PORT</code> 를 설정했다. 해당 옵션을 설정하면 클라이언트와 서버가 분리되어 멀티 스레드 환경이 된다. 그러므로 Thread Local 기반으로 동작하는 <code>@Transaction</code> 을 사용하여 롤백 처리를 할 수 없다.</p>
<p> 롤백을 해야 하는 이유는 <strong>테스트 격리(Isolated Test)</strong> 을 위함이다. 테스트 격리는 공유 자원을 사용하는 여러 테스트끼리 격리하여 서로 영향을 끼치지 않는 방법을 말하여 대표적인 예시가 DB 데이터 이다. DB 데이터 격리를 위해 롤백을 하며, 스프링에서는 일반적으로 동일 스레드에서 동작하여 <code>@Transactional</code> 을 사용하여 롤백한다. 하지만 앞서 이야기했 듯, 이 방법으로는 롤백을 할 수 없어 RDB 에서 제공하는 <code>TRUNCATE</code> 명령어를 활용하고자 했다.</p>
<blockquote>
</blockquote>
<p>webEnvironment option in @SpringBootTest</p>
<blockquote>
</blockquote>
<p><code>MOCK</code> : Mocking된 웹 환경을 제공(MockMvc를 사용한 테스트 가능 = 가짜 웹 환경에서 테스트)
<code>RANDOM_PORT</code> : 실제 웹 환경을 구성 (실제 웹 환경(with tomcat) 에서 테스트)
<code>DEFINED_PORT</code> : 실제 웹 환경을 구성 + 지정한 포트에서 동작</p>
<br>

<h3 id="2-custom-testexecutionlistener-사용">2. custom TestExecutionListener 사용</h3>
<p>sql 파일을 활용하는 방법도 있지만 스키마 변경(e.g. 추가, 삭제) 때마다 코드를 수정해야 하는 번거로움이 있기 때문에 <code>INFORMATION_SCHEMA.TABLES</code>(mysql 8.0 기준) 에 존재하는 스키마 정보를 가져와 데이터를 초기화하는 AcceptanceTestExecutionListener 를 작성했다.</p>
<p> Spring testContext 는 테스트를 위한 확장, 편의 기능을 <code>TestExecutionListener</code> 를 제공한다. 즉, 편리한 테스트를 위한 편의 기능을 TestExecutionListener 를 통해 제공한다. 대표적인 예시로 <code>MockitoTestExecutionListener</code> 이 존재한다. @MocbBean 어노테이션을 선언하기만 하면 테스트 이전에 해당 리스너에서 모킹(mocking) 하고 테스트가 끝난 시점에서는 모킹을 제거한다.</p>
<pre><code class="language-java">public class AcceptanceTestExecutionListener extends AbstractTestExecutionListener {

    @Override
    public void beforeTestMethod(final TestContext testContext) {
        final JdbcTemplate jdbcTemplate = getJdbcTemplate(testContext);
        final List&lt;String&gt; truncateQueries = getTruncateQueries(jdbcTemplate);
        truncateTables(jdbcTemplate, truncateQueries);
    }

    private List&lt;String&gt; getTruncateQueries(final JdbcTemplate jdbcTemplate) {
        try (Connection connection = jdbcTemplate.getDataSource().getConnection()) {
            String schema = connection.getCatalog();
            return jdbcTemplate.queryForList(
                &quot;SELECT concat(&#39;TRUNCATE TABLE &#39;, TABLE_NAME) &quot;
                    + &quot;FROM INFORMATION_SCHEMA.TABLES &quot;
                    + &quot;WHERE TABLE_SCHEMA = &#39;&quot; + schema + &quot;&#39;&quot;, String.class);
        } catch (Exception exception) {
            throw new RuntimeException();
        }
    }

    private JdbcTemplate getJdbcTemplate(final TestContext testContext) {
        return testContext.getApplicationContext().getBean(JdbcTemplate.class);
    }

    private void truncateTables(final JdbcTemplate jdbcTemplate, final List&lt;String&gt; truncateQueries) {
        execute(jdbcTemplate, &quot;SET FOREIGN_KEY_CHECKS = FALSE&quot;);
        truncateQueries.forEach(v -&gt; execute(jdbcTemplate, v));
        execute(jdbcTemplate, &quot;SET FOREIGN_KEY_CHECKS = TRUE&quot;);
    }

    private void execute(final JdbcTemplate jdbcTemplate, final String query) {
        jdbcTemplate.execute(query);
    }

}
</code></pre>
<h2 id="문제-상황--원인">문제 상황 &amp; 원인</h2>
<h3 id="문제--테스트-데이터가-들어오지-않았던-문제">문제 : 테스트 데이터가 들어오지 않았던 문제</h3>
<p>  테스트를 위해 테스트 데이터를 @Sql 어노테이션을 통해 주입하고자 했지만 데이터가 추가되지 않았다.</p>
<pre><code class="language-java"> @DisplayName(&quot;인증 테스트&quot;)
@Sql(&quot;classpath:member.sql&quot;)
public class AuthenticationTest {
    ...
}</code></pre>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/4cf017ba-d5bc-4a14-973d-9e6eb1c87a12/image.png" alt=""></p>
<h3 id="원인--testexecutionlistener-순서로-인한-문제">원인 : TestExecutionListener 순서로 인한 문제</h3>
<p> 원인은 TestExecutionListener 순서 문제였다. TestExecutionListener 는 개발자가 명시한 순서에 따라 실행된다. custom TestExecutionListener 작성시 별도로 순서(Ordered) 를 설정이 없으면 우선 순위가 가장 낮게 설정되어 가장 늦게 수행한다. 반면, @Sql 을 동작시키는 리스너인 SqlScriptsTestExecutionListener 를 확인해보면 순서(Ordered)가 선언되어 있다. 즉, @Sql 실행하고 TRUNCATE 로직이 실행되어 데이터 추가나서 다시 초기화되는 문제였다.(<code>@Sql 실행 → TRUNCATE 실행</code> 🫠)</p>
<img src = "https://velog.velcdn.com/images/cooper25_dev/post/94975c79-08c7-43a5-bb03-d58d71b75bac/image.png" width = 80%>


<h3 id="해결--custom-testexecutionlistener-순서-입력">해결 : custom TestExecutionListener 순서 입력</h3>
<p>TRUNCATE 로직 이후에 @Sql 실행되기 위해 Order 값을 SqlScriptsTestExecutionListener 의 값보다 낮게 설정했다. ( <code>TRUNCATE 실행</code> → <code>@Sql 실행</code>)</p>
<pre><code class="language-java">public class DataCleanupTestExecutionListener extends AbstractTestExecutionListener {

    @Override
    public int getOrder() {
        return 4500;
    }

}</code></pre>
<br>

<p>정상적으로 테스트 데이터가 등록되었다. 👍</p>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/06651fc1-c6c3-4f60-9fbb-80442848c313/image.png" alt=""></p>
<h3 id="오늘의-교훈">오늘의 교훈</h3>
<blockquote>
<p>custom TestExecutionListener 만들 때는 기존의 TestExecutionListener 와의 순서를 비교해서 값을 추가하자.</p>
</blockquote>
<h3 id="기타-지식properties">기타 지식(properties)</h3>
<ol>
<li>sql.init.mode : 스크립트 동작 설정<ul>
<li>ALWAYS: 모든 데이터베이스에 sql 스크립트를 동작시킨다.</li>
</ul>
</li>
<li>spring.jpa.defer-datasource-initialization: true<ul>
<li>2.5이상의 버전부터 data.sql 스크립트는 Hibernate가 초기화되기 전에 실행</li>
<li>hibernate ddl-auto property 가 동작하고 data.sql 이 동작하도록 하는 옵션</li>
</ul>
</li>
</ol>
<br>

<h2 id="reference">Reference</h2>
<ul>
<li><a href="https://www.baeldung.com/spring-testexecutionlistener">https://www.baeldung.com/spring-testexecutionlistener</a></li>
<li><a href="https://docs.spring.io/spring-framework/reference/testing/testcontext-framework/bootstrapping.html">https://docs.spring.io/spring-framework/reference/testing/testcontext-framework/bootstrapping.html</a></li>
<li><a href="https://mangkyu.tistory.com/264">https://mangkyu.tistory.com/264</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[분산락은 Tx commit 이후에 락을 해제하자 summary]]></title>
            <link>https://velog.io/@cooper25_dev/%EB%B6%84%EC%82%B0%EB%9D%BD%EC%9D%80-Tx-commit-%EC%9D%B4%ED%9B%84%EC%97%90-%EB%9D%BD%EC%9D%84-%ED%95%B4%EC%A0%9C%ED%95%98%EC%9E%90</link>
            <guid>https://velog.io/@cooper25_dev/%EB%B6%84%EC%82%B0%EB%9D%BD%EC%9D%80-Tx-commit-%EC%9D%B4%ED%9B%84%EC%97%90-%EB%9D%BD%EC%9D%84-%ED%95%B4%EC%A0%9C%ED%95%98%EC%9E%90</guid>
            <pubDate>Tue, 28 Nov 2023 15:50:26 GMT</pubDate>
            <description><![CDATA[<h1 id="introduction">Introduction</h1>
<h2 id="분산락-distributed-lock-을-사용하는-이유">분산락 (Distributed Lock) 을 사용하는 이유?</h2>
<ol>
<li>분산 환경에서 공유 자원에 관한 상호 배제(mutual exclusion) 을 보장하기 위해 사용한다.</li>
<li>공유 자원의 임계 영역을 접근하는 가능 여부를 확인하여 분산 환경의 원자성(atomic) 을 보장할 수 있다.</li>
</ol>
<br>

<h2 id="분산락을-tx-commit-이후-해제해야-하는-이유">분산락을 Tx commit 이후 해제해야 하는 이유</h2>
<p>동시성 환경에서 데이터 정합성을 위함이다. 락을 먼저 해제할 경우, 선행 트랜잭션의 내용이 반영되지 않고 후행 트랜잭션 내용만 반영되는 <code>second lost updates problem</code> 문제가 발생할 수 있다.</p>
<br>

<h1 id="테스트-해보자">테스트 해보자</h1>
<h2 id="1-도메인-구성">1. 도메인 구성</h2>
<img src = "https://velog.velcdn.com/images/cooper25_dev/post/254d6b0f-dccf-40ba-b4a6-dea8fb41fd3a/image.png" width = "300px">

<p><code>Promotion</code></p>
<pre><code class="language-java">@Entity
@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Promotion {

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

   @Getter
   @Column(length = 100, nullable = false)
   private String name;

   @Column(length = 20, nullable = false)
   private int ticketAmount; // shared resources (warning race condition!)

    public Promotion(final String name, final int ticketAmount) {
      this.name = name;
      this.ticketAmount = ticketAmount;
   }

   public void decreaseTicketAmount() {
      this.ticketAmount -= 1;
   }

   public boolean soldOut() {
      return ticketAmount &lt;= 0;
   }

   public int remainingTickets() {
      return this.ticketAmount;
   }

}</code></pre>
<p><code>Ticker</code></p>
<pre><code class="language-java">@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Ticket {

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

    @Column(length = 15, nullable = false)
    private Long price;

    @Column(length = 20, nullable = false)
    private Long promotionId;

    public Ticket(final Long price, final Long promotionId) {
        this.price = price;
        this.promotionId = promotionId;
    }

}</code></pre>
<ol>
<li><p>연관관계를 사용하는 경우</p>
<ul>
<li><p>장점</p>
<ol>
<li><p>매번 티켓 수를 조회하기 위한 조회 쿼리를 동작하므로 <code>race condition</code> 에 관해 할 필요가 없다.</p>
<p> 다만, 여러 트랜잭션이 insert 하는 시점에 따라 티켓 사이즈가 정확히 일치하는 않는 이슈가 발생할 수 있다.</p>
</li>
<li><p>Promotion 안에 Ticket 을 관리하는 형태로 <code>로직을 응집화</code> 할 수 있는 장점이 있다.</p>
</li>
</ol>
</li>
<li><p>단점</p>
<ol>
<li>생성된 티켓 수를 확인하기 위한 조회 쿼리가 필요하다.<ul>
<li>Ticket 테이블의 데이터양이 증가함에 따라 인덱스의 데이터 추가와 DB 부하를 줄 수 있다.</li>
</ul>
</li>
</ol>
</li>
</ul>
</li>
<li><p>연관 관계를 사용하지 않는 경우(티켓 잔여량을 관리하는 컬럼 활용하는 경우)</p>
<ul>
<li>장점<ol>
<li>티켓 사이즈를 확인하는 부수적 쿼리를 발생하지 않아 <code>index row insert</code>, <code>query 수행</code> 에 관한 DB 부하를 줄일 수 있는 장점이 있다.</li>
</ol>
</li>
<li>단점<ol>
<li>잔여 티켓 데이터의 <code>race condition</code> 을 고려해야 한다. 동시 접근할 경우, 데이터 정합성이 깨질 수 있다.</li>
</ol>
</li>
</ul>
</li>
</ol>
<h2 id="2-jmeter-를-이용한-테스트">2. JMeter 를 이용한 테스트</h2>
<table>
<thead>
<tr>
<th>index</th>
<th>api</th>
<th>explanation</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>/api/v1/promotions/{promotionId}/ticket</td>
<td>@Transactional</td>
</tr>
<tr>
<td>2</td>
<td>/api/v2/promotions/{promotionId}/ticket</td>
<td>rLock.unlock()-&gt; @Transactional commit</td>
</tr>
<tr>
<td>3</td>
<td>/api/v3/promotions/{promotionId}/ticket</td>
<td>@Transactional commit -&gt; rLock.unlock()</td>
</tr>
</tbody></table>
<h3 id="1-ticketamount-user-100개-동일하게-세팅">(1) ticketAmount, user 100개 동일하게 세팅</h3>
<p>(1) thread condition</p>
<img src = "https://velog.velcdn.com/images/cooper25_dev/post/b599136a-381c-46fe-84ce-d0036e8bbabc/image.png" width = "200px">

<p>(2) 100개 HTTP Requests</p>
<table>
<thead>
<tr>
<th>type</th>
<th>잔여 티켓 수</th>
<th>실제 생성 티켓</th>
<th>데이터 정합성 유지</th>
</tr>
</thead>
<tbody><tr>
<td>@Transactional</td>
<td>45 / 100</td>
<td>100</td>
<td>X</td>
</tr>
<tr>
<td>rLock.unlock()→ @Transactional commit</td>
<td>38 / 100</td>
<td>100</td>
<td>X</td>
</tr>
<tr>
<td>@Transactional commit → rLock.unlock()</td>
<td>0 / 100</td>
<td>100</td>
<td>O</td>
</tr>
</tbody></table>
<h3 id="reference">Reference</h3>
<ul>
<li><a href="https://github.com/pbg0205/BE-tutorials/blob/b9e740377c09de0a79e26d49070f501cb38fbcd2/cooper-lab/spring-redisson-integrity-test/README.md">https://github.com/pbg0205/BE-tutorials/blob/b9e740377c09de0a79e26d49070f501cb38fbcd2/cooper-lab/spring-redisson-integrity-test/README.md</a></li>
<li><a href="https://helloworld.kurly.com/blog/distributed-redisson-lock/">https://helloworld.kurly.com/blog/distributed-redisson-lock/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[GC simple summary]]></title>
            <link>https://velog.io/@cooper25_dev/GC-simple-summary</link>
            <guid>https://velog.io/@cooper25_dev/GC-simple-summary</guid>
            <pubDate>Tue, 28 Nov 2023 15:29:16 GMT</pubDate>
            <description><![CDATA[<h1 id="jvm--gc">JVM &amp; GC</h1>
<h2 id="java-패러다임">Java 패러다임</h2>
<blockquote>
<p>(1) Platform Independence - Java applications are compiled into <em>bytecode</em> which is stored in class files and loaded in a JVM. Since applications run in a JVM, they can be run on many different operating systems and devices.</p>
</blockquote>
<p>(2) Object-Oriented - Java is an object-oriented language that take many of the features of C and C++ and improves upon them.</p>
<blockquote>
</blockquote>
<p><strong>(3) Automatic Garbage Collection - Java automatically allocates and deallocates memory so programs are not burdened with that task.</strong></p>
<ul>
<li>Java는 메모리를 자동으로 할당하고 할당 해제하므로 프로그램이 해당 작업에 부담을 느끼지 않는다.
  (<em>= 개발자가 메모리 할당/해제할 필요가 없어 생산성을 증가한다.</em>)</li>
</ul>
<br>

<h2 id="jvm">JVM</h2>
<ul>
<li>자바 프로그램을 실행시키는 프로그램</li>
<li>장점 : 플랫폼 독립성을 보장<ol>
<li>기존 프로그램 실행 방식<ul>
<li>소스 코드를 컴파일러(compiler)를 통해 어셈블리어로 변환하고, 변환된 어셈블리어를 다시 어셈블러(assembler)를 통해 기계가 해석할 수 있는 <code>기계어</code>로 변환</li>
</ul>
</li>
<li>Java 실행 방식<ul>
<li>자바의 컴파일러를 통해 바이트 코드(Byte Code)라는 특수한 형태의 중간 코드를 생성해 class 확장자를 부여하고, JVM(Java Virtual Machine)이라는 프로그램을 통해 기계어로 번역되어 실행한다.</li>
</ul>
</li>
<li>JVM 의 장점<ul>
<li><strong>하나의 소스코드를 통해 class 파일을 생성해서 배포하고, 어느 환경에서나 그 환경에 맞는 JVM만 설치되어 있다면 프로그램을 실행</strong></li>
</ul>
</li>
</ol>
</li>
<li>종류 : Hotspot JVM, GraalVM<ul>
<li><code>Hotspot JVM</code> : Java 1.3부터는 Hotspot VM이 추가되었고, Hotspot VM에는 2개의 JIT 컴파일러(c1, c2)가 포함되어 있다.</li>
<li><code>GraalVM</code> : JIT 컴파일러들 중에서 기존의 c++로 작성된 C2 컴파일러인 Graal 컴파일러를 Java 기반으로 새롭게 작성된 VM</li>
</ul>
</li>
</ul>
<br>

<h2 id="key-hotspot-components"><strong>Key Hotspot Components</strong></h2>
<blockquote>
<p>성능 튜닝을 위한 JVM component</p>
</blockquote>
<ol>
<li><code>힙(heap)</code>은 오브젝트 데이터가 저장되는 곳입니다.</li>
<li>대부분의 튜닝 옵션은 힙의 크기를 조정하고 상황에 가장 적합한 <code>가비지 컬렉터</code>를 선택하는 것과 관련이 있다. </li>
<li>JIT 컴파일러도 성능에 큰 영향을 미치지만 최신 버전의 JVM에서는 튜닝이 거의 필요하지 않다.</li>
</ol>
<br>

<h2 id="describing-garbage-collection">Describing Garbage Collection</h2>
<ol>
<li>힙 메모리를 살펴보고 사용 중인 객체와 사용하지 않는 객체를 식별하여 사용하지 않는 객체를 삭제하는 프로세스</li>
<li>대표적인 알고리즘 : <code>mark-and-sweep algorithm</code> (old GC)<ol>
<li>과정 : marking - normal deletion - deletion with compacting</li>
</ol>
</li>
</ol>
<br>

<p><strong>Step 1: Marking</strong></p>
<ul>
<li><strong>가비지 컬렉터가 사용 중인 메모리 조각과 사용되지 않는 메모리를 식별하는 단계</strong></li>
<li>RootSet 식별 기준으로 한다.<ul>
<li>힙 내의 다른 객체에 의한 참조</li>
<li>Java 스택, 즉 Java 메서드 실행 시에 사용하는 지역 변수와 파라미터들에 의한 참조</li>
<li>네이티브 스택, 즉 JNI(Java Native Interface)에 의해 생성된 객체에 대한 참조</li>
<li>메서드 영역의 정적 변수에 의한 참조</li>
</ul>
</li>
</ul>
<img src = "https://velog.velcdn.com/images/cooper25_dev/post/c00dd116-28c6-4385-973c-daeb1c46ed7e/image.png" width = "500px">

<p><strong>Step 2: Normal Deletion</strong></p>
<ul>
<li><strong>참조되지 않은 객체를 제거하여 참조된 객체와 포인터를 여유 공간에 남김</strong></li>
</ul>
<img src = "https://velog.velcdn.com/images/cooper25_dev/post/d890636c-1dc1-4678-88eb-e4c4fb3b782b/image.png" width = "500px">

<p><strong>Step 2a: Deletion with Compacting</strong></p>
<ul>
<li>성능을 더욱 향상시키기 위해 <strong>참조되지 않는 객체를 삭제</strong>하는 것 외에도 나머지 <strong>참조 객체를 압축</strong>할 수도 있다.</li>
</ul>
<img src = "https://velog.velcdn.com/images/cooper25_dev/post/5b08f23a-d07f-4d67-9cbd-fe187510b3b7/image.png" width = "500px">


<br>

<h2 id="jvm-generations-in-heap-area">JVM Generations (in Heap Area)</h2>
<blockquote>
<p><code>stop the world</code> : 작업이 완료될 때까지 모든 애플리케이션 스레드가 중지됩니다.
(minor, major GC 모두 해당)</p>
</blockquote>
<ol>
<li><strong>Young Generation :</strong> 새로운 객체가 할당되는 공간 (minor GC 공간)</li>
<li><strong>Old Generation :</strong> 오래 생존한 객체가 할당저장되는 공간(major GC 공간)<ul>
<li>major GC 는 생존한 모든 객체를 포함하기 때문에 훨씬 느리다.</li>
<li>(<em>old object → new object 를 참조하거 같이 제거해야 할 수도 있기 때문이다.</em>) ⇒ <code>card table</code></li>
<li><code>card table</code> : Old 영역에 있는 객체가 Young 영역의 객체를 참조할 때마다 정보가 표시</li>
</ul>
</li>
<li><strong>Permanent generation :</strong> 클래스 및 메서드를 설명하는 데 필요한 메타데이터가 포함되어 있습니다.</li>
</ol>
<br>

<h2 id="old-영역에-대한-gc">Old 영역에 대한 GC</h2>
<ul>
<li>Serial GC</li>
<li>Parallel GC</li>
<li>Parallel Old GC(Parallel Compacting GC)</li>
<li>Concurrent Mark &amp; Sweep GC(이하 CMS)</li>
<li>G1(Garbage First) GC</li>
</ul>
<h3 id="serial-gc--xxuseserialgc"><strong>Serial GC (-XX:+UseSerialGC)</strong></h3>
<ul>
<li>old 영역 사용 알고리즘 : mark-and-sweep algorithm</li>
<li>적은 메모리와 CPU 코어 개수가 적을 때 적합한 방식</li>
<li>운영에서 절대 사용 금지</li>
</ul>
<h3 id="parallel-gc--xxuseparallelgc">Parallel GC <strong>(-XX:+UseParallelGC)</strong></h3>
<ul>
<li>Serial GC와 기본적인 알고리즘은 동일</li>
<li>GC를 처리하는 쓰레드가 여러 개</li>
<li>Parallel GC는 메모리가 충분하고 코어의 개수가 많을 때 유리</li>
</ul>
<h3 id="parallel-old-gc-xxuseparalleloldgc"><strong>Parallel Old GC(-XX:+UseParallelOldGC)</strong></h3>
<ul>
<li>Old 영역의 GC 가 <code>Mark-Summary-Compaction</code> 알고리즘 사용</li>
<li>sweep 대신 summary 과정으로 대체한다.</li>
</ul>
<h3 id="g1-gc"><strong>G1 GC</strong></h3>
<ul>
<li>GC는 바둑판의 각 영역에 객체를 할당하고 GC를 실행한다. 그러다가, <code>해당 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC를 실행한다.</code> (Young, Old 영역과는 아예 다르다.)</li>
<li>가장 큰 장점은 성능이다.</li>
</ul>
<br>

<h3 id="reference">Reference</h3>
<ul>
<li><strong>Oracle - JVM Generations</strong> : <a href="https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html">https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html</a></li>
<li><strong>Naver D2 - Java Garbage Collection</strong> : <a href="https://d2.naver.com/helloworld/1329">https://d2.naver.com/helloworld/1329</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[인증 객체를 DTO 로 주입하고 테스트하기]]></title>
            <link>https://velog.io/@cooper25_dev/%EC%9D%B8%EC%A6%9D-%EA%B0%9D%EC%B2%B4%EB%A5%BC-DTO-%EB%A1%9C-%EC%A3%BC%EC%9E%85%ED%95%98%EA%B3%A0-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@cooper25_dev/%EC%9D%B8%EC%A6%9D-%EA%B0%9D%EC%B2%B4%EB%A5%BC-DTO-%EB%A1%9C-%EC%A3%BC%EC%9E%85%ED%95%98%EA%B3%A0-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 28 Nov 2023 15:06:33 GMT</pubDate>
            <description><![CDATA[<ol>
<li>AS-IS<ol>
<li>인증 객체를 주입받는 로직을 추가</li>
<li>정적 코드 분석기에서 보안 취약점 D등급 판정 (sonarcube)</li>
<li>인증 객체 기반 테스트 코드 작성</li>
</ol>
</li>
<li>Challenge<ol>
<li>인증 객체를 주입받는 로직을 추가<ul>
<li>@AuthenticationPrincipal 기반 복합 어노테이션 @CurrentUser 를 통한 인증 객체 주입 방식 채택 </li>
</ul>
</li>
<li>정적 코드 분석기에서 취약점 D등급 판정 (sonarcube)<ul>
<li>인증 객체로 Entity 를 사용하고 있어 발생한 문제로 판단</li>
<li><code>CurrentUserDtoArgumentResolver</code> 를 구현하여 Entity -&gt; DTO 로 변환</li>
</ul>
</li>
<li>인증 객체 기반 테스트 코드 작성<ul>
<li>MockCustomUserSecurityContextFactory 작성하여 인증 목 객체 주입</li>
<li>sonarcube 취약점 개선을 위한 기존 DTO 형태로 주입하는 방식을 활용하기 위한 커스텀 팩토리 작성 방법</li>
</ul>
</li>
</ol>
</li>
<li>TO-BE<ol>
<li>인증 객체 주입 로직 구현</li>
<li>취약점 A등급 판정</li>
<li>인증 객체 기반 테스트 코드 구현<br>


</li>
</ol>
</li>
</ol>
<h1 id="1-인증-객체를-주입받는-로직을-추가">1. 인증 객체를 주입받는 로직을 추가</h1>
<h2 id="authenticationprincipal">@AuthenticationPrincipal??</h2>
<ul>
<li>SecurityContextHolder 에 존재하는 인증 객체(Principal)를 주입해주는 어노테이션</li>
<li><code>AuthenticationPrincipalArgumentResolver</code> 를 통해 Principal 의 subclass 을 주입한다.</li>
</ul>
<br>

<h2 id="authenticationprincipalargumentresolver-">AuthenticationPrincipalArgumentResolver ??</h2>
<h3 id="1-authenticationprincipalargumentresolver-">(1) AuthenticationPrincipalArgumentResolver ??</h3>
<ul>
<li>파라미터로 @AuthenticationPrincipal 를 찾아 인증 객체를 주입해주는 ArgumentResolver</li>
<li>package org.springframework.security.web.method.annotation 하위에 존재</li>
<li>spring 4.0 부터 도입</li>
</ul>
<h3 id="2-method-살펴보기">(2) method 살펴보기</h3>
<p><code>supportsParameter</code></p>
<ol>
<li>외부에서 MethodParameter의 annotaion 목록을 확인한다. (메서드의 각 파라미터를 탐색하면서 확인)</li>
<li>AnnotationUtils를 통해 어노테이션이 <strong><code>AuthenticationPrincipal</code> 타입</strong> 을 찾는다.</li>
<li>만약에 어노테이션 타입이 존재하면 해당 annotaion 을 반환하고 그렇지 않으면 null을 반환한다.</li>
</ol>
<pre><code class="language-java">public final class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {

        // do something...

        public boolean supportsParameter(MethodParameter parameter) {
            return this.findMethodAnnotation(AuthenticationPrincipal.class, parameter) != null;
        }

        private &lt;T extends Annotation&gt; T findMethodAnnotation(Class&lt;T&gt; annotationClass, MethodParameter parameter) {
            T annotation = parameter.getParameterAnnotation(annotationClass);
            if (annotation != null) {
                return annotation;
            } else {
                Annotation[] annotationsToSearch = parameter.getParameterAnnotations();
                Annotation[] var5 = annotationsToSearch;
                int var6 = annotationsToSearch.length;

                for(int var7 = 0; var7 &lt; var6; ++var7) {
                    Annotation toSearch = var5[var7];
                    annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass); // 여기!
                    if (annotation != null) {
                        return annotation;
                    }
                }

                return null;
    }
}</code></pre>
<br>

<p><code>resolveArgument</code></p>
<ol>
<li>해당 메서드의 파라미터에서 <code>@AuthenticationPrincipal</code> annotation 이 있는지를 탐색한다.</li>
<li><em>StandardEvaluationContext</em> 클래스는 해당 표현식(e.g. spEL) 을 평가할 개체를 지정한다. (by. reflection)</li>
<li>Expression 객체는 ExpressionParser 을 통해 표현식 문자열 구문 분석을 한 내용과 <em>StandardEvaluationContext</em> 에 평가할 개체를 토대로 원하는 객체를 호출한다.</li>
</ol>
<pre><code class="language-java">public final class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {

        // do something...

        public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
                Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                if (authentication == null) {
                    return null;
                } else {
                    Object principal = authentication.getPrincipal();
                    AuthenticationPrincipal annotation = (AuthenticationPrincipal)this.findMethodAnnotation(AuthenticationPrincipal.class, parameter); //(1)
                    String expressionToParse = annotation.expression();
                    if (StringUtils.hasLength(expressionToParse)) {
                        StandardEvaluationContext context = new StandardEvaluationContext(); // (2)
                        context.setRootObject(principal);
                        context.setVariable(&quot;this&quot;, principal);
                        context.setBeanResolver(this.beanResolver);
                        Expression expression = this.parser.parseExpression(expressionToParse); // (3)
                        principal = expression.getValue(context);
                    }

                    if (principal != null &amp;&amp; !ClassUtils.isAssignable(parameter.getParameterType(), principal.getClass())) {
                        if (annotation.errorOnInvalidType()) {
                            throw new ClassCastException(principal + &quot; is not assignable to &quot; + parameter.getParameterType());
                        } else {
                            return null;
                        }
                    } else {
                        return principal;
                    }
                }
           }
}</code></pre>
<br>

<h2 id="customannotation-활용">CustomAnnotation 활용</h2>
<ul>
<li>코드 가독성을 위해 @AuthenticationPrincipal 기반한 custom annotation 적용</li>
<li>기존 spring 지원 기능을 활용하면 별도의 로직을 작성할 필요가 없기 떄문에 인증 객체 주입 생산성 향상</li>
</ul>
<pre><code class="language-java">@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal
public @interface CurrentUser {
}</code></pre>
<pre><code class="language-java">@RequiredArgsConstructor
@RestController
@RequestMapping(&quot;/api/mypages&quot;)
public class MyPageController {

    private final MyPageQueryFacade myPageQueryFacade;
    private final MyPageCommandFacade myPageCommandFacade;

    @GetMapping(&quot;/me&quot;)
    public ResponseEntity&lt;BaseResponse&lt;UserLookUpResponse&gt;&gt; searchMe(@CurrentUser CurrentUserDto currentUserDto) {
        UserLookUpResponse userLookUpResponse = myPageQueryFacade.findUserWithDetailInfo(currentUserDto.id());
        return ResponseEntity.ok(new BaseResponse&lt;&gt;(USER_LOOK_UP_SUCCESS, userLookUpResponse));
    }
}</code></pre>
<br>

<h2 id="인증-객체-entity-→-dto-변환-custom-resolver-작성">인증 객체 Entity → DTO 변환 Custom Resolver 작성</h2>
<ol>
<li>기존 컨트롤러 코드에서 POJO 형태가 아닌 인증객체를 그대로 주입하는 방식으로 구현되어 있었음.</li>
<li>sonarcube(정적 코드 분석) 에서 취약성 부분에서 D등급 판정</li>
<li>인증 객체는 엔티티가 상속하고 있어 엔티티 객체를 표현 계층에 할당하여 발생한 원인으로 판단</li>
<li>custom resolver 인 <code>CurrenUserDtoArgumentResolver</code> 를 작업하여 반영하여 A 등급으로 판정 변경</li>
</ol>
<p><code>코드 변경 이전 등급 판정</code>
<img src="https://velog.velcdn.com/images/cooper25_dev/post/7b6b9e35-b772-4f1b-8626-e1f3a6e9a100/image.png" alt=""></p>
<p><code>CurrentUserDtoArgumentResolver</code></p>
<pre><code class="language-java">public class CurrentUserDtoArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return findMethodAnnotation(CurrentUser.class, parameter) != null;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // LoginAuthenticationToken 타입 authentication
        if (authentication == null) {
            return null;
        }

        CustomUser customUser = (CustomUser) authentication.getPrincipal(); // principal을 CustomUser타입으로 변환

        return new CurrentUserDto(customUser.getId(), customUser.getEmail()); // CustomUserDto 타입으로 반환
    }

    private &lt;T extends Annotation&gt; T findMethodAnnotation(Class&lt;T&gt; annotationClass, MethodParameter parameter) {
        T annotation = parameter.getParameterAnnotation(annotationClass);
        if (annotation != null) {
            return annotation;
        }

        Annotation[] annotationsToSearch = parameter.getParameterAnnotations();
        for (Annotation toSearch : annotationsToSearch) {
            annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass);
            if (annotation != null) {
                return annotation;
            }
        }

        return null;
    }
}</code></pre>
<p><code>코드 변경 등급 판정</code>
<img src="https://velog.velcdn.com/images/cooper25_dev/post/265331f8-9530-41d6-8735-df941c3c43eb/image.png" alt=""></p>
<h1 id="3-인증-객체-테스트-코드-작성">3. 인증 객체 테스트 코드 작성</h1>
<ol>
<li>스프링에서 제공하는 <code>@WithMockUser</code> 는 Authentication 의 Pricipal 등록타입이 User 이다.</li>
<li>DTO 방식을 등록하기 위해 <code>MockCustomUserSecurityContextFactory</code> 를 선언하고 <code>@WithSecurityContext</code> 를 할당</li>
</ol>
<pre><code class="language-java">@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = MockCustomUserSecurityContextFactory.class)
public @interface MockCustomUser {

    String value() default &quot;user&quot;;

    String username() default &quot;&quot;;

    String[] roles() default {&quot;USER&quot;};

    String password() default &quot;password&quot;;
}</code></pre>
<pre><code class="language-java">public class MockCustomUserSecurityContextFactory implements WithSecurityContextFactory&lt;MockCustomUser&gt; {

    @Override
    public SecurityContext createSecurityContext(MockCustomUser annotation) {

        String username = StringUtils.hasLength(annotation.username()) ? annotation.username() : annotation.value();
        Assert.notNull(username, () -&gt; annotation + &quot; cannot have null username on both username and value properties&quot;);

        List&lt;GrantedAuthority&gt; authorities = settingRole(annotation);
        CustomUser customUser = new CustomUser(username, annotation.password());
        Authentication authentication = UsernamePasswordAuthenticationToken.authenticated(customUser, &quot;&quot;, authorities);

        SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
        securityContext.setAuthentication(authentication);

        return securityContext;
    }

    private List&lt;GrantedAuthority&gt; settingRole(MockCustomUser annotation) {
        List&lt;GrantedAuthority&gt; grantedAuthorities = new ArrayList&lt;&gt;();

        for (String role : annotation.roles()) {
            Assert.isTrue(!role.startsWith(&quot;ROLE_&quot;), () -&gt; &quot;roles cannot start with ROLE_ Got &quot; + role);
            grantedAuthorities.add(new SimpleGrantedAuthority(&quot;ROLE_&quot; + role));
        }

        return grantedAuthorities;
    }
}</code></pre>
<br>

<h2 id="reference">Reference</h2>
<ul>
<li><a href="https://docs.spring.io/spring-security/reference/servlet/integrations/mvc.html#mvc-authentication-principal">https://docs.spring.io/spring-security/reference/servlet/integrations/mvc.html#mvc-authentication-principal</a></li>
<li><a href="https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.html">https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.html</a></li>
<li><a href="https://zuminternet.github.io/ZUM-PILOT-WONOH/">https://zuminternet.github.io/ZUM-PILOT-WONOH/</a></li>
<li><a href="https://tecoble.techcourse.co.kr/post/2020-09-30-spring-security-test/">https://tecoble.techcourse.co.kr/post/2020-09-30-spring-security-test/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[InnoDB 아키텍처 (with. In-memory structues)]]></title>
            <link>https://velog.io/@cooper25_dev/INNODB-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</link>
            <guid>https://velog.io/@cooper25_dev/INNODB-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</guid>
            <pubDate>Thu, 26 Oct 2023 01:53:41 GMT</pubDate>
            <description><![CDATA[<h2 id="1-쿼리-실행-구조">1. 쿼리 실행 구조</h2>
<h3 id="1-mysql-engine">(1) <code>MySQL Engine</code></h3>
<ul>
<li>역할<ul>
<li>쿼리의 실행 계획을 수립해 <code>handler(e.g. storage engine</code>) 로 전달하는 역할</li>
</ul>
</li>
<li>구조<ul>
<li><code>쿼리 파서</code> : 문법 체크 + 파서 트리 만들기</li>
<li><code>전처리기</code> : 파서 트리의 구조 문제 파악</li>
<li><code>옵티마이저</code> : 실행 계획 수립</li>
<li><code>실행 엔진</code> : 실행 계획대로 각 핸들러의 요청 결과를 연결해 사용자에게 전달 (DispatcherServlet 과 같은 역할)</li>
</ul>
</li>
</ul>
<h3 id="2-storage-engine-handler">(2) <code>Storage Engine</code> (handler)</h3>
<ul>
<li>역할<ul>
<li>실행 엔진의 요청에 따라 데이터를 디스크 저장 또는 조회하는 담당</li>
</ul>
</li>
<li>구조<ol>
<li>In-memory structure<ol>
<li>InnoDB buffer pool<ul>
<li>Adaptive Hash Index</li>
<li>Change Buffer</li>
</ul>
</li>
<li>Log Buffer</li>
</ol>
</li>
<li>On-Disk Structure<ol>
<li>System Tablespace</li>
</ol>
<ul>
<li>Doublewrite Buffer Files</li>
<li>Undo Tablespaces</li>
<li>RedoLog Files</li>
<li>File-Per-Table Tablespaces</li>
<li>General Tablespaces</li>
<li>Temporary Tablespaces</li>
</ul>
</li>
</ol>
</li>
</ul>
<img src = "https://velog.velcdn.com/images/cooper25_dev/post/c0e06988-eba5-4e0a-8fe8-df3f01d7f65c/image.png" width = "500px">

<p>(출처 : RealMySQL 4장. 아키텍처)</p>
<br>

<h2 id="2-innodb-기능">2. InnoDB 기능</h2>
<ol>
<li>PK 기반 클러스터링 인덱스 지원<ul>
<li>PK 기반 순서대로 저장</li>
<li>PK 기반 레인지 스캔은 빠르게 처리됨</li>
</ul>
</li>
<li>외래키 지원<ul>
<li>데이터 변경 시 데이터 체크 작업 필요 → 잠금이 여러 테이블에 전파 → 데드락 위험 ↑</li>
<li><code>foreign_key_checks</code> : 외래키 체크 작업 ON/OFF 시스템 변수</li>
</ul>
</li>
<li>MVCC 기능 지원<ol>
<li>언두 로그<ul>
<li>장점<ol>
<li>트랜잭션 보장</li>
<li>격리 수준 보장</li>
</ol>
</li>
</ul>
</li>
</ol>
</li>
<li>자동 데드락 감지</li>
<li>장애 복구 자동화</li>
<li>쓰기 지연 기능 (by. buffer pool)</li>
</ol>
<blockquote>
<p>MySQL 엔진이 각 스토리지 엔진에게 데이터를 읽어오거나 저장하도록 명령 하려면 반드시 핸들러를 통해야 한다.</p>
</blockquote>
<br>

<h2 id="3-innodb-구조">3. InnoDB 구조</h2>
<ol>
<li>In-memory Structures<ul>
<li>Buffer Pool<ul>
<li>Adaptive Hash Index (자주 접근하는 컬럼 내용에 관한 인덱스)</li>
<li>Change Buffer (Secondary Index 변경 내용 버퍼링)</li>
</ul>
</li>
<li>Log buffer (Redo Log 변경 내용 버퍼링)</li>
</ul>
</li>
<li>On-Disk Structures<ul>
<li>DoubleWrite Buffer(플러시 이전에 더티 페이지들을 묶어 관리해 장애 복구에 사용)</li>
</ul>
</li>
</ol>
<img src = "https://velog.velcdn.com/images/cooper25_dev/post/8e3e5c3b-e2e5-4309-ad23-0005b547d735/image.png" width = "700px">

<br>

<h3 id="1-in-memory-structures">(1) In-memory Structures</h3>
<ul>
<li>Buffer Pool<ul>
<li>Adaptive Hash Index</li>
<li>Change Buffer</li>
</ul>
</li>
<li>Log buffer</li>
</ul>
<br>

<ol>
<li>Buffer Pool<ul>
<li>테이블 및 인덱스 데이터 캐싱을 목적으로 하는 메인 메모리 영역</li>
<li>장점 : 쓰기 작업 지연, 장애 복구 (with. Double Write Buffer)</li>
<li>구성 : <code>Data Page</code>, <code>Index Page</code>, <code>Undo Page</code>, <code>Lock Info</code>, <code>Change Buffer</code><ul>
<li>언두 로그?<ul>
<li>데이터의 변경 이력을 저장하는 공간 (MVCC)</li>
<li>장점 : 트랜잭션 보장, 격리 수준 보장</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>Adaptive Hash Index<ol>
<li>역할<ul>
<li>자주 사용되는 컬럼을 해시로 정의하여 B-Tree Index 대신 직접 Data 에 엑세스 가능</li>
</ul>
</li>
<li>장점<ul>
<li>Tx 기능 or 신뢰성 저하 없이 In-memory Database 와 같은 작업 성능을 낸다.</li>
</ul>
</li>
</ol>
</li>
<li>Change Buffer (구 : Insert Buffer)<ol>
<li>역할<ul>
<li><code>Secondary Index Page</code> 의 변경 내용을 캐싱하기 위한 임시 공간</li>
</ul>
</li>
<li>동작 과정<ol>
<li>변경할 Secondary Index Page 가 Buffer Pool 에 존재할 경우, 바로 메모리 내 변경 가능</li>
<li>변경할 Secondary Index Page 가 Buffer Pool 에 없는 경우, 변경 내역만 Change Buffer 에 저장, 향 후 disk 에 플러시<img src = "https://velog.velcdn.com/images/cooper25_dev/post/91de0b68-cef6-462d-90d9-04376a0eb472/image.png" width = "600px"></li>
</ol>
</li>
</ol>
</li>
<li>Log Buffer (구 : Redo Log Buffer)<ol>
<li>역할<ul>
<li><code>Redo Log</code> 파일에 기록할 Data 를 보관하는 메모리 영역<ul>
<li>Redo Log?<ul>
<li>완료되지 않는 Tx 에 의해 변경된 Data 를 적용하기 위해 사용</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>동작 과정<ol>
<li>Buffer Pool 의 데이터를 변경할 때 <strong>먼저 변경사항을 Log Buffer 에 기록</strong></li>
<li>설정 주기 혹은 <code>Tx commit</code> 시점에 디스크와 동기화<img src = "https://velog.velcdn.com/images/cooper25_dev/post/6d011279-69b7-49d6-a111-cd4919d48094/image.png" width = "600px">

</li>
</ol>
</li>
</ol>
</li>
</ol>
<blockquote>
<p><strong>undo log 와 redo log 차이</strong></p>
</blockquote>
<ol>
<li>undo log : 트랜잭션 보장과 격리 수준에 맞는 일관성있는 데이터 읽기를 위한 단일 Tx 에 관한 레코드 모임 (for. MVCC)</li>
<li>redo log : Crash Recovery 수행 시 완료되지 않은 트랜잭션에 의해 변경된 Data 적용하기 위함. (for. 데이터 동기화)</li>
</ol>
<br>

<h2 id="references">References</h2>
<ol>
<li>RealMySQL 4장 아키텍처</li>
<li><a href="https://blog.ex-em.com/1698">[DB 인사이드] MySQL Architecture - 6. InnoDB : In-Memory Structure</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[코드로 보는 MessageSourceAutoConfiguration]]></title>
            <link>https://velog.io/@cooper25_dev/%EC%BD%94%EB%93%9C%EB%A1%9C-%EB%B3%B4%EB%8A%94-MessageSourceAutoConfiguration</link>
            <guid>https://velog.io/@cooper25_dev/%EC%BD%94%EB%93%9C%EB%A1%9C-%EB%B3%B4%EB%8A%94-MessageSourceAutoConfiguration</guid>
            <pubDate>Thu, 12 Oct 2023 07:18:15 GMT</pubDate>
            <description><![CDATA[<ul>
<li>MessageSource 에 관련된 설정 여부를 검토하는 과정에서 검토한 코드</li>
<li><a href="https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfiguration.java">https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfiguration.java</a></li>
</ul>
<br>

<h3 id="summary">summary</h3>
<ol>
<li>SpringBoot 를 사용한다면 <code>application.properties</code> 를 선언해서 MessageSource 를 관리하자.<ol>
<li>MessageAutoConfiguration 은 MessageSource 빈을 선언하지 않으면 자동 설정된다.</li>
<li>application.properties 에서 <code>spring.messages</code> 로 시작하는 property 를 탐색해서 MessageSource 를 생성한다.</li>
</ol>
</li>
</ol>
<br>

<h1 id="1-messagesource-설정-및-선언-방법">1. MessageSource 설정 및 선언 방법</h1>
<h2 id="1-messagesource-선언-방법">[1] MessageSource 선언 방법</h2>
<h3 id="1-applicationproperties-선언하는-방법">(1) application.properties 선언하는 방법</h3>
<pre><code>spring.messages.basename=message/messages
spring.messages.encoding=UTF-8 # default
spring.messages.fallbackToSystemLocale=false</code></pre><h3 id="2-messagesource-bean-선언">(2) MessageSource Bean 선언</h3>
<pre><code class="language-java">@Configuration
public class MessageSourceConfig {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasenames(&quot;customMessage/messages&quot;);
        messageSource.setFallbackToSystemLocale(false);
        messageSource.setDefaultEncoding(&quot;UTF-8&quot;);

        return messageSource;
    }
}</code></pre>
<br>

<h2 id="2-message-선언-방법">[2] message 선언 방법</h2>
<ul>
<li>basename 하위에 <code>message_언어코드_국가코드.properties(.xml)</code> 선언하기</li>
</ul>
<img src= "https://velog.velcdn.com/images/cooper25_dev/post/39a86c2e-eedc-4519-a4b2-6096d1a22d4e/image.png" width = "300px">

<br>

<h1 id="2-messagesourceautoconfiguration">2. MessageSourceAutoConfiguration</h1>
<h2 id="1-meta-infspringspring-autoconfigure-metadataproperties">[1] META-INF/spring/spring-autoconfigure-metadata.properties</h2>
<ul>
<li><code>META-INF/spring/spring-autoconfigure-metadata.properties</code> 를 확인해보면 <code>MessageSourceAutoConfiguration</code> 이 존재하는 것을 확인</li>
</ul>
<img src = "https://velog.velcdn.com/images/cooper25_dev/post/b8e3a1ac-1da8-40d9-9f45-6e2b9463bf9e/image.png" width = "400px">

<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/daf22740-ded0-4477-98cc-ac677cc1e735/image.png" alt=""></p>
<br>

<h2 id="2-messagesourceautoconfiguration-활성-조건-확인">[2] MessageSourceAutoConfiguration 활성 조건 확인</h2>
<h3 id="1-설정-어노테이션-확인">(1) 설정 어노테이션 확인</h3>
<ul>
<li>위치 : org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration</li>
</ul>
<img src = "https://velog.velcdn.com/images/cooper25_dev/post/7d00f6fe-aa0e-4d86-bc5c-9c8813caced0/image.png" width = "500px">


<pre><code class="language-java">@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)</code></pre>
<ol>
<li>messageSource bean 이름이 존재하지 않을 경우</li>
<li>SearchStrategy<ul>
<li>BeanFactory 계층 구조에서 bean 에 대한 명명된 검색 전략.</li>
<li>CURRENT : Search only the current context.</li>
</ul>
</li>
</ol>
<h3 id="2-resourcebundlecondition">(2) ResourceBundleCondition</h3>
<p>[1] <code>getMatchOutcome</code></p>
<img src = "https://velog.velcdn.com/images/cooper25_dev/post/4d92ad14-b8bc-423f-8f3c-a139aeafe283/image.png" width = "500px">

<ol>
<li>basename 을 application.properties(application.yml) 에서 <code>spring.messages.basename</code> 속성에 대한 값을 가져온다. 없을 경우, “messages” 문자열 반환</li>
<li>cache 에 basename 으로 저장된 값이 있는지 확인한다. 만약 저장된 값이 없을 경우, <code>getMatchoutcomeForBasename</code> 을 호출하고 결과 값을 outcome 에 할당한다.</li>
<li>outcome 을 cache 변수에 할당한다.</li>
</ol>
<br>

<p>[2] <code>getMatchoutcomeForBasename</code></p>
<img src = "https://velog.velcdn.com/images/cooper25_dev/post/35c4801c-1b59-4591-b2ea-fbfc5e395f2a/image.png" width = "500px">

<ol>
<li>basename 리스트를 만들고 getResources 에 해당 basename 을 담아 호출한다.</li>
<li>getResource() 리턴 값인 Resource 의 값 존재유무를 확인한다.</li>
<li>존재하는 자원이 있다면, <code>match = true</code> 인 outcome을 반환한다.</li>
</ol>
<br>

<p>[3] <code>getResources</code></p>
<img src = "https://velog.velcdn.com/images/cooper25_dev/post/373aa6cd-a8ce-4d96-beb7-d44fee30f9ab/image.png" width = "500px">

<ol>
<li>classpath 내에 basename에 해당하는 properties 파일이 있는지 찾아서 반환한다.</li>
<li>classpath 내에 basename에 해당하는 properties 파일이 없다면 자동 설정이 안된다.
  (따라서 messages.properties 와 같은 기본 파일을 설정해두는 것이 좋을 듯 하다.</li>
</ol>
<br>

<h2 id="3-messagesource-생성-조건을-확인해보자">[3] MessageSource 생성 조건을 확인해보자</h2>
<ul>
<li>위치 : org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration</li>
</ul>
<img src = "https://velog.velcdn.com/images/cooper25_dev/post/953bea1d-2917-4245-b10e-1afd1f998a70/image.png" width = "400px">

<img src = "https://velog.velcdn.com/images/cooper25_dev/post/0b3f72cd-5a99-4967-8ba1-31462f20be0c/image.png" width = "700px">

<ol>
<li><code>MessageSourceProperties</code> 를 설정하고 <code>MessageSource</code> 을 빈으로 생성한다.</li>
</ol>
<br>

<h2 id="4-messagesource--autoconfiguration-vs-custom">[4] MessageSource : AutoConfiguration vs Custom</h2>
<h3 id="1-autoconfiguration--properties-기반-설정">(1) AutoConfiguration : properties 기반 설정</h3>
<pre><code>spring.messages.basename=message/messages
spring.messages.encoding=UTF-8
spring.messages.fallbackToSystemLocale=false</code></pre><ol>
<li><strong>spring.messages.basename</strong><ul>
<li>spring message 의 properties 기본 이름</li>
<li>e.g. message directory 의 message 로 시작하는 파일(properties or yml) 을 탐색한다는 의미</li>
</ul>
</li>
<li><strong>spring.messages.encoding</strong><ul>
<li>인코딩 방식</li>
</ul>
</li>
<li><strong>fallbackToSystemLocale</strong><ul>
<li>요청받은 locale 에 대한 메세지 파일이 없을 때, 시스템 locale을 사용할 것인지에 대한 옵션<ul>
<li><code>true</code> : 환경설정 언어에 해당하는 영어용 메세지 파일을 가져온다.</li>
<li><code>false</code> : 기본 메세지 파일에서 메세지를 가져온다.</li>
</ul>
</li>
</ul>
</li>
</ol>
<br>

<h3 id="2-custom---messagesource-빈-선언">(2) Custom  : MessageSource 빈 선언</h3>
<pre><code class="language-java">@Configuration
public class MessageSourceConfig {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasenames(&quot;customMessage/messages&quot;);
        messageSource.setFallbackToSystemLocale(false);
        messageSource.setDefaultEncoding(&quot;UTF-8&quot;);

        return messageSource;
    }
}</code></pre>
<h3 id="3-결론--custom-설정이-우선-순위가-높다">(3) 결론 : Custom 설정이 우선 순위가 높다.</h3>
<ul>
<li>MessageSourceAutoConfiguration 에서 messageSource 이름으로 시작하는 MessageSource  이 있으면 자동 설정이 활성화되지 않기 때문이다.</li>
</ul>
<pre><code class="language-java">// 여기
@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)
public class MessageSourceAutoConfiguration {</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[코드로 보는 스프링 시큐리티 basic authentication]]></title>
            <link>https://velog.io/@cooper25_dev/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BD%94%EB%93%9C%EB%A1%9C-%EB%B3%B4%EB%8A%94-basic-authentication-CORS</link>
            <guid>https://velog.io/@cooper25_dev/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BD%94%EB%93%9C%EB%A1%9C-%EB%B3%B4%EB%8A%94-basic-authentication-CORS</guid>
            <pubDate>Mon, 02 Oct 2023 13:03:33 GMT</pubDate>
            <description><![CDATA[<p>스프링 시큐리티 basic authentication 설정을 스프링 시큐리티에서 어떻게 설정해 나가는지 과정을 살펴보고자 한다.</p>
<h2 id="1-abstracthttpconfigurer">[1] AbstractHttpConfigurer</h2>
<h3 id="1-abstracthttpconfigurer-1">(1) AbstractHttpConfigurer</h3>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/98aa45dd-ab16-4b7f-8d74-ab352da9d033/image.png" alt=""></p>
<ol>
<li>disable() : 해당 AbstractHttpConfigurer 를 비활성화 시키는 기능을 제공하며 builder() 메서드를 통해 <code>HttpSecurityBuilder</code> 를 반환한다.<ul>
<li><code>HttpSecurityBuilder</code> : 시큐리티 설정을 관리하는 builder 인터페이스</li>
</ul>
</li>
</ol>
<br>

<h2 id="2-httpbasicconfigurer">[2] HttpBasicConfigurer</h2>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/6954a04a-fcb3-4b1c-98da-caf904add2da/image.png" alt=""></p>
<ol>
<li>AbstractHttpConfigurer abstract class 의 구현체이며, <code>Basic Authentication</code> 을 지원하는 설정을 담당하는 구현체이다.</li>
</ol>
<h3 id="1-httpbasicconfigurerinit">(1) HttpBasicConfigurer.init()</h3>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/9e7c0ed1-c34f-4eb2-9568-f1b1773c0136/image.png" alt=""></p>
<ol>
<li>SecurityConfigure interface 의 override 메서드이다.</li>
<li>BasicAuthentication 관련된 MediaTypeRequestMatcher, DefaultEntryPoint, DefaultLogoutHandler 기본 설정을 초기화한다.</li>
</ol>
<br>

<h3 id="2-httpbasicconfigurerconfigure">(2) HttpBasicConfigurer.configure()</h3>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/fa2d46c6-677f-49a7-ace2-8ec2e5e3f2d2/image.png" alt=""></p>
<ol>
<li>SecurityConfigure interface 의 override 메서드이다.</li>
<li>BasicAuthenticationFilter 설정 (e.g. AuthenticationManager, AuthenticationDetailsSource, RememberMeServices)</li>
</ol>
<br>


<h2 id="3-abstractconfiguredsecuritybuilder">[3] AbstractConfiguredSecurityBuilder</h2>
<h3 id="1-abstractconfiguredsecuritybuilderdobuild">(1) AbstractConfiguredSecurityBuilder.dobuild()</h3>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/4327ebfd-c676-4540-8e3b-175fcee9a524/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/c43a994c-f566-4b9b-9c6c-c08a2979e03f/image.png" alt=""></p>
<ol>
<li>init(), configure() : SecurityConfigurer 에 선언된 설정들을 초기화, 설정하는 메서드를 호출한다.<ul>
<li>해당 메서드는 SecurityFilterChain bean 을 생성하면 해당 설정들이 세팅된다.<ul>
<li>e.g. defaultSecurityFilterChain (SecurityFilterChainConfiguration) : 해당 설정은 사용자가 정의하면 해당 설정은 덮어쓰기 된다.(back-off)</li>
</ul>
</li>
</ul>
</li>
</ol>
<h2 id="4-basic-authentication">[4] Basic Authentication</h2>
<h3 id="1-www-authenticate">(1) WWW-Authenticate</h3>
<ol>
<li><code>WWW-Authenticate</code> header (response) : 사용자 이름과 비밀번호를 제공하라는 의미로 401 status code 와 함께 요청을 반려하는 헤더. 서버에는 각 다른 비밀번호가 있는 영역들이 있을 것이므로 헤더에 해당 영역을 설명한다.<ul>
<li>예시 : WWW-Authenticate : Basic realm=&quot;localhost&quot;</li>
</ul>
</li>
</ol>
<h3 id="2-요청-흐름">(2) 요청 흐름</h3>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/cb5b5be5-6e23-493c-9eec-200fa14ece41/image.png" alt=""></p>
<ol>
<li>서버는 클라이언트에게 <a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Status/401">401 (Unauthorized)</a>  status code 를 응답하며, 최소한 한 번의 시도에 포함된 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate">WWW-Authenticate (en-US)</a> 응답 헤더로 권한을 부여하는 방법에 대한 정보를 제공한다.</li>
<li>서버와 인증을 하기를 원하는 클라이언트는 <a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Authorization">Authorization</a> 요청 헤더 필드에 인증 정보를 포함함으로써 인증을 수행할 수 있다.</li>
<li>클라이언트는 대개 사용자에게 비밀번호 프롬프트를 표시할 것이고 정확한 Authorization 헤더를 포함하는 요청을 만들 것입니다.</li>
</ol>
<h2 id="5-basic-authentication-in-spring-security">[5] Basic Authentication in spring security</h2>
<h3 id="1-요청-흐름">(1) 요청 흐름</h3>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/653a7519-d1fa-490d-b17d-37018546e623/image.png" alt=""></p>
<ol>
<li>/private URL 에 미인증 요청(unauthenticated request) 하면, <code>AuthorizationFilter</code> 에서 <code>AccessDeniedException</code> 예외를 던진다.</li>
<li>미인증 이후 <code>ExceptionTraslationFilter</code> 는 인증을 시작한다.<ul>
<li>AuthenticationEntryPoint 의 구현체인 BasicAuthenticationEntryPoint 는 WWW-Authenticate header 를 반환한다.</li>
</ul>
</li>
<li><code>WWW-Authenticate</code> header 를 전달받은 client 는 username, password 를 포함하여 요청을 재시도한다.</li>
</ol>
<h3 id="2-인증-과정">(2) 인증 과정</h3>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/d465d459-7b13-45a6-b626-747351726d96/image.png" alt=""></p>
<ol>
<li><code>BasicAuthenticationFilter</code>는 <code>HttpServletRequest</code> 에서 username, password 를 추출해 UsernamePasswordAuthenticationToken
을 생성한다.</li>
<li>AuthenticationManager  에 토큰을 넘겨 인증을 한다.<ul>
<li>fail<ul>
<li>SecurityContextHolder 를 비운다.(clear out)</li>
<li>RememberMeServices.loginFail 을 호출한다. (만약 설정되지 않았을 경우, 미동작)</li>
<li>AuthenticationEntryPoint을 호출해 WWW-Authenticate 헤더를 다시 전달한다.</li>
</ul>
</li>
<li>success<ul>
<li>SecurityContextHoler 에 인증 객체를 보낸다.</li>
<li>RememberMeServices.loginSuccess 을 호출한다.</li>
<li>BasicAuthenticationFilter 의 FilterChain.doFilter(request,response) 을 호출의 이후 로직을 진행시킨다.</li>
</ul>
</li>
</ul>
</li>
</ol>
<h3 id="3-설정-방법">(3) 설정 방법</h3>
<pre><code class="language-java">@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        // ...
        .httpBasic(withDefaults());
    return http.build();
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[코드로 보는 QueryDSL transform() 에서 @Transactional 사용해야 이유]]></title>
            <link>https://velog.io/@cooper25_dev/%EC%BD%94%EB%93%9C%EB%A1%9C-%EB%B3%B4%EB%8A%94-QueryDSL-transform-%EC%97%90%EC%84%9C-Transactional-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@cooper25_dev/%EC%BD%94%EB%93%9C%EB%A1%9C-%EB%B3%B4%EB%8A%94-QueryDSL-transform-%EC%97%90%EC%84%9C-Transactional-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Wed, 13 Sep 2023 14:49:51 GMT</pubDate>
            <description><![CDATA[<h2 id="1-1n-dto-변환-transform-쿼리-문제--connection-leak">[1] 1:N DTO 변환 transform() 쿼리 문제 : Connection Leak</h2>
<p>QueryDSL 메서드를 통해서 1:N 연관관계를 조회한 데이터를 <code>transform()</code> 메서드를 통해 일괄적으로 DTO 로 변환하여 조회하는 로직을 작성했지만 <code>Connection Leak</code> 이 발생했던 문제가 있었다. 아래가 문제의 쿼리이다.</p>
<pre><code class="language-java">@RequiredArgsConstructor
public class StudentRepositoryImpl implements StudentRepositoryCustom {

    private final JPAQueryFactory jpaQueryFactory;

    //...

    @Override
    public List&lt;StudentLookupResponse&gt; findAllByStudentIds(List&lt;Long&gt; studentIds) {
        return jpaQueryFactory.select(student)
                .from(student)
                .leftJoin(award).on(student.id.eq(award.student.id))
                .where(student.id.in(studentIds))
                .transform(groupBy(student.id)
                        .list(Projections.constructor(StudentLookupResponse.class,
                                student.id,
                                student.name,
                                student.tagName,
                                GroupBy.list(
                                        Projections.constructor(AwardLookupResponse.class,
                                                award.id,
                                                award.name))
                                )
                        )
                );
    }
}</code></pre>
<br>

<h2 id="2-transform-메서드-동작-후-connectionleak-확인">[2] transform() 메서드 동작 후, ConnectionLeak 확인</h2>
<p>hikari pool 의 로깅 레벨을 설정하여 Connection Pool 을 확인해보았다. 실제 API 를 6회 요청을 하고 로그를 확인해보면 HikariPool 의 <code>active -&gt; idle</code> 로 전환되지 않았다.</p>
<pre><code class="language-yaml">logging:
  level:
    com.zaxxer.hikari: TRACE
    com.zaxxer.hikari.HikariConfig: DEBUG</code></pre>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/d4587497-b4d7-47f9-a02a-a459d9800522/image.png" alt=""></p>
<br>


<h2 id="3-transactional-을-선언하지-않으면-transform-사용시-entitymanager-는-닫히지-않는다">[3] @Transactional 을 선언하지 않으면 transform() 사용시 EntityManager 는 닫히지 않는다!</h2>
<blockquote>
<p>source : <a href="https://colin-d.medium.com/querydsl-%EC%97%90%EC%84%9C-db-connection-leak-%EC%9D%B4%EC%8A%88-40d426fd4337">Querydsl에서 transform 사용시에 DB connection leak 이슈</a></p>
</blockquote>
<h3 id="1-transform-메서드-내부-확인하기">(1) transform() 메서드 내부 확인하기</h3>
<ol>
<li>iterate(), fetch() 메서드 두가지를 비교해보자.<ol>
<li>공통점<ul>
<li>createQuery() 호출</li>
</ul>
</li>
<li>차이점<ul>
<li>iterate() -&gt; queryHandler.iterate() 반환</li>
<li>fetch -&gt; getResultList(query) 반환 </li>
</ul>
</li>
</ol>
</li>
</ol>
<pre><code class="language-java">package com.querydsl.jpa.impl;

public abstract class AbstractJPAQuery&lt;T, Q extends AbstractJPAQuery&lt;T, Q&gt;&gt; extends JPAQueryBase&lt;T, Q&gt; {

    @Override
    public CloseableIterator&lt;T&gt; iterate() {
        try {
            Query query = createQuery();
            return queryHandler.iterate(query, projection);
        } finally {
            reset();
        }
    }

    @Override
    @SuppressWarnings(&quot;unchecked&quot;)
    public List&lt;T&gt; fetch() {
        try {
            Query query = createQuery();
            return (List&lt;T&gt;) getResultList(query);
        } finally {
            reset();
        }
    }
}</code></pre>
<br>

<h3 id="2-sharedentitymanagercreator-의-invoke-메서드를-확인하자">(2) SharedEntityManagerCreator 의 invoke() 메서드를 확인하자.</h3>
<ol>
<li>현재 트랜잭션에 참여하고 있는 EntityMaager 를 조회한다. 존재하지 않을 경우 새로운 EntityManager 를 생성한다.</li>
<li>현재 생성된 EntityManager 의 메서드를 활용하기 위해서 <code>DeferredQueryInvocationHandler</code> Proxy 생성해서 반환한다.</li>
<li><code>DeferedQueryInvocationHandler</code> 은 <code>SharedEntityManager</code>에서 비트랜잭션 <code>createQuery()</code>가 호출될 때 해당 쿼리 객체를 처리한다.</li>
<li><code>DeferedQueryInvocationHandler</code> proxy의 <code>invoke()</code> 메서드를 실행하고 <code>queryTerminationMethod</code> 미리 정의해둔 method name일 경우에는 entityManager를 종료한다.</li>
</ol>
<pre><code class="language-java">public abstract class SharedEntityManagerCreator {

    private static class SharedEntityManagerInvocationHandler implements InvocationHandler, Serializable {

        @Override
        @Nullable
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // (1) 트랜잭션 참여하면 target 값을 반환하지 않는다.
            EntityManager target = EntityManagerFactoryUtils.doGetTransactionalEntityManager(
                    this.targetFactory, this.properties, this.synchronizedWithTransaction);

            // ...
            try {
                Object result = method.invoke(target, args);
                if (result instanceof Query) {
                    Query query = (Query) result;
                    if (isNewEm) {
                        Class&lt;?&gt;[] ifcs = cachedQueryInterfaces.computeIfAbsent(query.getClass(), key -&gt;
                                ClassUtils.getAllInterfacesForClass(key, this.proxyClassLoader));

                        // (2) DeferredQueryInvocationHandler 생성해서 다른 곳에서 호출
                        result = Proxy.newProxyInstance(this.proxyClassLoader, ifcs,
                                new DeferredQueryInvocationHandler(query, target)); 
                        isNewEm = false;
                    }
                    else {
                        EntityManagerFactoryUtils.applyTransactionTimeout(query, this.targetFactory);
                    }
                }
                return result;
            }
            catch (InvocationTargetException ex) {
                throw ex.getTargetException();
            }
            finally {
                if (isNewEm) {
                    EntityManagerFactoryUtils.closeEntityManager(target);
                }
            }
        }

    private static class DeferredQueryInvocationHandler implements InvocationHandler {

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // ...

            try {
                // ...
            }
            catch (InvocationTargetException ex) {
                throw ex.getTargetException();
            }
            finally {
                // (3) queryTerminatingMethods 포함되면 EntityManager 를 닫는다.
                if (queryTerminatingMethods.contains(method.getName())) { 
                    if (this.outputParameters != null &amp;&amp; this.target instanceof StoredProcedureQuery) {
                        StoredProcedureQuery storedProc = (StoredProcedureQuery) this.target;
                        for (Map.Entry&lt;Object, Object&gt; entry : this.outputParameters.entrySet()) {
                            try {
                                Object key = entry.getKey();
                                if (key instanceof Integer) {
                                    entry.setValue(storedProc.getOutputParameterValue((Integer) key));
                                }
                                else {
                                    entry.setValue(storedProc.getOutputParameterValue(key.toString()));
                                }
                            }
                            catch (IllegalArgumentException ex) {
                                entry.setValue(ex);
                            }
                        }
                    }
                    EntityManagerFactoryUtils.closeEntityManager(this.entityManager);
                    this.entityManager = null;
                }
            }
        }
}</code></pre>
<h3 id="3-queryterminationmethod-포함되야-entitymanager-닫힌다">(3) queryTerminationMethod 포함되야 EntityManager 닫힌다.</h3>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/62ffb9b3-6606-4302-8694-38897f352ce5/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/48655d6c-ac87-4d27-a423-ae4a1ea179b6/image.png" alt=""></p>
<p>DeferredQueryInvocationHandler 의 invoke() 메서드를 확인해보면 <code>queryTerminatingMethods</code> 에 정의된 메서드 이름에서 <code>iterate()</code> 가 포함되어 있지 않다. 즉, transform() 메서드의 내부 호출인 iterate() 메서드는 엔티티 매니저를 닫히지 않아 Connection 을 반환하지 않는 문제였다.</p>
<br>

<h2 id="4-transactional-은-작업이-완료되면-entitymanager-를-닫는다">[4] @Transactional 은 작업이 완료되면 EntityManager 를 닫는다.</h2>
<h3 id="1-트랜잭션-핵심-컴포넌트--transactioninterceptor">(1) 트랜잭션 핵심 컴포넌트 : TransactionInterceptor</h3>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/8c67e893-e010-4b9f-ba54-917fbde4721c/image.png" alt=""></p>
<p> 간단히 스프링에서 제공하는 @Transactional 을 선언 시의 동작 방식을 보자. 트랜잭션을 수행하는데 여러 컴포넌트들이 사용되지만 핵심 컴포넌트는 <code>TransactionInterceptor</code> 이다. <code>TransactionInterceptor</code> 은 부모 클래스인 <code>AbstractPlatformTransactionManager</code> 의 <code>invokeWithinTransaction()</code> 메서드를 호출하며 전체 트랜잭션이 동작한다.</p>
<br>

<h3 id="2-abstractplatformtransactionmanager-invokewithintransaction-내부에-핵심이-있다">(2) AbstractPlatformTransactionManager. invokeWithinTransaction() 내부에 핵심이 있다.</h3>
<p>AbstractPlatformTransactionManager 의 <code>invokeWithinTransaction()</code> 를 내부 로직의<code>cleanupAfterCompletion()</code>) 메서드를 확인해보자. 해당 메서드는 트랜잭션이 완료(commit or rollback) 시점에 ThreadLocal 의 트랜잭션 정보를 반환하면서 엔티티 매니저를 닫는다. EntityManager 를 닫으면서 사용한 Connection 을 ConnectionPool 에 반환하기 때문에 <code>@Transactional</code> 어노테이션을 선언하면 Connection 을 반환한다. 그러므로 <code>transform()</code> 메서드의 connection leak 을 방지하기 위해서는 꼭 @Transactional 을 선언해서 사용하도록 해야 한다.</p>
<pre><code class="language-java">public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager, Serializable {

    private void processCommit(DefaultTransactionStatus status) throws TransactionException {
        try {
            try {
                // ...                                
                else if (status.isNewTransaction()) {
                    // ...
                    doCommit(status); // 커밋하기 (구현체: JpaTransactionManager)
                }
                // ...                
            }
        }
        finally {
            // 트랜잭션 정보 제거
            cleanupAfterCompletion(status);
        }
    }

    private void cleanupAfterCompletion(DefaultTransactionStatus status) {
        status.setCompleted();
        if (status.isNewSynchronization()) {
            TransactionSynchronizationManager.clear();
        }
        if (status.isNewTransaction()) {
            doCleanupAfterCompletion(status.getTransaction()); // 여기 들어가면 EntityManager 다는 로직 있음.
        }
        if (status.getSuspendedResources() != null) {
            if (status.isDebug()) {
                logger.debug(&quot;Resuming suspended transaction after completion of inner transaction&quot;);
            }
            Object transaction = (status.hasTransaction() ? status.getTransaction() : null);
            resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources());
        }
    }

    @Override
    protected void doCleanupAfterCompletion(Object transaction) {
        // ...

        if (txObject.isNewEntityManagerHolder()) {
            EntityManager em = txObject.getEntityManagerHolder().getEntityManager();
            if (logger.isDebugEnabled()) {
                logger.debug(&quot;Closing JPA EntityManager [&quot; + em + &quot;] after transaction&quot;);
            }
            EntityManagerFactoryUtils.closeEntityManager(em); // EntityManager 닫기!
        }
        else {
            logger.debug(&quot;Not closing pre-bound JPA EntityManager after transaction&quot;);
        }
    }


}
</code></pre>
<h3 id="3-번외-querydsl-hibernate-에서-해결할-문제인가">(3) (번외) QueryDSL? Hibernate? 에서 해결할 문제인가?</h3>
<blockquote>
<p><a href="https://github.com/querydsl/querydsl/issues/3089">[Querydsl repo] Connection leak when using FetchableQueryBase#transform outside of a transaction  #3089</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/5137da86-705d-4fbb-8ab1-d9df6de25635/image.png" alt=""></p>
<p>실제 QueryDSL repository 에서도 관련 이슈에 대해 이야기하고 있지만 querydsl 가 아닌 것으로 판단하고 있다. 그 이유는 트랜잭션이 활성화되지 않을 때는 Connection 을 생성하는 주체는 Hibernate 이며 따라서 트랜잭션을 닫는 주체는 Hibernate 인 것으로 이야기하며 모든 메서드에 <code>@Transactional</code> 을 선언하는 것을 권장하고 있다.</p>
<br>

<h2 id="5-references">[5] References</h2>
<ul>
<li><a href="https://colin-d.medium.com/querydsl-%EC%97%90%EC%84%9C-db-connection-leak-%EC%9D%B4%EC%8A%88-40d426fd4337">Querydsl에서 transform 사용시에 DB connection leak 이슈</a></li>
<li><a href="https://github.com/spring-projects/spring-framework/blob/5.3.x/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java">[spring-framework-orm] SharedEntityManagerCreator</a></li>
<li><a href="https://github.com/querydsl/querydsl/issues/3089">[Querydsl] Connection leak when using FetchableQueryBase#transform outside of a transaction</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[해시 테이블(Hash Table)]]></title>
            <link>https://velog.io/@cooper25_dev/%ED%95%B4%EC%8B%9C-%ED%85%8C%EC%9D%B4%EB%B8%94Hash-Table</link>
            <guid>https://velog.io/@cooper25_dev/%ED%95%B4%EC%8B%9C-%ED%85%8C%EC%9D%B4%EB%B8%94Hash-Table</guid>
            <pubDate>Wed, 13 Sep 2023 04:22:43 GMT</pubDate>
            <description><![CDATA[<h2 id="1-해시테이블-hash-table-">[1] 해시테이블 (Hash Table) ??</h2>
<p> 해시 테이블은 Key, Value 형테로 데이터를 저장하는 자료구조이며, 평균 시간 복잡도가 O(1) 인 만큼 빠른 검색 속도를 제공한다. 해시 테이블이 빠른 속도를 제공하는 이유는 해시함수(hash function) 와 해시 테이블(hash table)의 버킷(bucket) 덕분이다. 해시 함수(hash function) 은 Key 값을 해시(Hash) 로 변환해주는 함수를 말하고, 버킷(bucket) 은 해시 테이블의 값을 저장하는 공간이다. </p>
<br>

<h2 id="2-해시테이블-동작원리">[2] 해시테이블 동작원리</h2>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/014191d7-1877-423d-9095-a8d1abad405a/image.png" alt=""></p>
<p>Key 값을 해시 함수(Hash Function) 을 거쳐 해시 값(Hash) 을 변환하여 해시테이블의 특정 버킷의 저장할 인덱스를 설정하고 <code>Key, Value, Hash</code> 와 같은 값들을 저장한다.
    - 해시 함수(Hash Function) : 임의의 데이터를 정수(Integer) 로 변환하는 함수</p>
<br>

<h2 id="3-해시-함수">[3] 해시 함수</h2>
<p>해시 함수는 크게 <code>Division Method</code>, <code>Digit Folding</code>, <code>Multiplication Method</code>, <code>Univeral Hashing</code> 기법이 존재한다.</p>
<ol>
<li><code>Division Method</code>: 나눗셈을 이용하는 방법으로 입력값을 테이블의 크기(bucket size)로 모듈러 연산을 통해 인덱스를 결정한다. ( 주소 = 입력값 % 버킷 사이즈) 테이블의 크기를 소수로 정하고 2의 제곱수와 먼 값을 사용해야 효과가 좋다고 알려져 있다.</li>
<li><code>Digit Folding</code>: 각 Key의 문자열을 ASCII 코드로 바꾸고 값을 합한 데이터를 테이블 내의 주소로 사용하는 방법이다.</li>
<li><code>Multiplication Method</code>: 숫자로 된 Key값 K와 0과 1사이의 실수 A, 보통 2의 제곱수인 m을 사용하여 다음과 같은 계산을 해준다. h(k)=(kAmod1) × m</li>
<li><code>Univeral Hashing</code>: 다수의 해시함수를 만들어 집합 H에 넣어두고, 무작위로 해시함수를 선택해 해시값을 만드는 기법이다.</li>
</ol>
<br>

<h2 id="4-hash-collision">[4] Hash collision</h2>
<p> 해시 함수의 결과가 같을 경우의 데이터를 관리하는 방법을 말한다. 해시 테이블 bucket 의 데이터를 관리하는 방법은 크게 <code>open address</code>, <code>seperate chaining</code> 을 통해 해결하고 있다.</p>
<h3 id="1-분리-연결법seperate-chaining">(1) 분리 연결법(seperate chaining)</h3>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/ac3ee8fa-3a4d-4a1a-a8f6-5b099c436c2c/image.png" alt=""></p>
<p><code>Seperating Chaining</code> 은 동일한 버킷의 데이터에 LinkedList 와 같은 자료구조를 추가해 다음 데이터 주소를 저장하는 것을 말한다. Seperating Chaining 방식은 해시 테이블의 확장이 필요없고 간단하게 구현하는 것이 가능하지만 동일한 버킷의 체이닝되는 데이터가 많아지면 탐색 속도가 떨어지는 단점이 있다.</p>
<br>

<h3 id="2-개방-주소법open-addressing">(2) 개방 주소법(Open Addressing)</h3>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/3f46c8b1-4d7d-4866-9e54-23108f96af7c/image.png" alt=""></p>
<p><code>Open Addressing</code> 은 비어있는 해시 테이블의 버킷을 활용하는 방법이다. Open Addressing 을 구현하기 위한 대표적인 방법은 <code>Linear Probing</code>, <code>Quadratic Probing</code>, <code>Double Hashing Probing</code> 이 존재한다.</p>
<ol>
<li><code>Linear Probing</code>: 현재의 버킷 index로부터 고정폭 만큼씩 이동하여 차례대로 검색해 비어 있는 버킷에 데이터를 저장한다.</li>
<li><code>Quadratic Probing</code>: 해시의 저장순서 폭을 제곱으로 저장하는 방식이다. 예를 들어 처음 충돌이 발생한 경우에는 1만큼 이동하고 그 다음 계속 충돌이 발생하면 2^2, 3^2 칸씩 옮기는 방식이다.</li>
<li><code>Double Hashing Probing</code>: 해시된 값을 한번 더 해싱하여 해시의 규칙성을 없애버리는 방식이다. 해시된 값을 한번 더 해싱하여 새로운 주소를 할당하기 때문에 다른 방법들보다 많은 연산을 하게 된다.</li>
</ol>
<br>

<h2 id="5-java-에서의-hashmap">[5] java 에서의 HashMap</h2>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/c8782bd3-23d1-485d-ba34-8664bedc1811/image.png" alt=""></p>
<p> 자바에서는 해시 충돌 해결 방법으로 <code>Seperate chaining</code> 방식을 선택하고 있으며, 데이터 접근, 삽입, 삭제 시간 모두 상수 시간에 접근하지만, 같은 해시 값이 늘어난다면 값이 해당 버킷에 대한 검색 시간이 선형 시간으로 변경될 수 있기 때문에 위에서 언급한 해시함수를 선언하는 것에 대해 신중할 필요가 있다. (<a href="https://velog.io/@lychee/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-%EC%95%84%EC%9D%B4%ED%85%9C-11.-equals%EB%A5%BC-%EC%9E%AC%EC%A0%95%EC%9D%98%ED%95%98%EB%A0%A4%EA%B1%B0%EB%93%A0-hashCode%EB%8F%84-%EC%9E%AC%EC%A0%95%EC%9D%98%ED%95%98%EB%9D%BC">[이펙티브 자바] 아이템 11. equals를 재정의하려거든 hashCode도 재정의하라</a>)</p>
<ul>
<li>image source : <a href="https://www.youtube.com/watch?v=ZBu_slSH5Sk">https://www.youtube.com/watch?v=ZBu_slSH5Sk</a></li>
</ul>
<br>

<h2 id="references">References</h2>
<ul>
<li><a href="https://www.youtube.com/watch?v=ZBu_slSH5Sk">[쉬운코드]맵(map)과 해시 테이블(hash table) 핵심만 모아보기! 맵과 해시 테이블(a.k.a 해시 맵)을 20분간 아주아주아주 알차게 설명합니다!!
</a></li>
<li><a href="https://mangkyu.tistory.com/102">[mangkyu][자료구조] 해시테이블(HashTable)이란?</a></li>
<li><a href="https://www.codingeek.com/data-structure/complete-guide-open-addressing-classification-eliminate-collisions/">A Complete Guide to Open Addressing &amp; its Classification to eliminate Collisions</a></li>
<li><a href="https://en.wikipedia.org/wiki/Hash_table">[Wikipedia] Hash Table</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[코드로 보는 OSIV (Open Session In View) ]]></title>
            <link>https://velog.io/@cooper25_dev/%EC%BD%94%EB%93%9C%EB%A1%9C-%EB%B3%B4%EB%8A%94-OSIV-Open-Session-In-View</link>
            <guid>https://velog.io/@cooper25_dev/%EC%BD%94%EB%93%9C%EB%A1%9C-%EB%B3%B4%EB%8A%94-OSIV-Open-Session-In-View</guid>
            <pubDate>Mon, 11 Sep 2023 02:39:38 GMT</pubDate>
            <description><![CDATA[<h1 id="osivopen-session-in-view">OSIV(Open-Session-In-View)</h1>
<h2 id="osivopen-session-in-view-란">OSIV(Open Session In View) 란??</h2>
<p><img src="https://velog.velcdn.com/images/cooper25_dev/post/ecb2158b-5fbb-41c3-a324-a9190977d3ce/image.png" alt=""></p>
<p> <code>OSIV(Open Session In View)</code>은 <strong>영속성 컨텍스트 관리 영역을 뷰(View)까지 확장하는 기능</strong>이다. Open Session In View라 불리는 이유는 JPA의 구현체인 Hibernate 에서 EntityManager의 구현체로 Session 을 사용한다. JPA에서는 OEIV(Open-EntityManager-In-View) 로 부르지만 통상적으로 Open-Session-In-View 로 부른다.</p>
<br>

<h2 id="spring-boot-의-프로퍼티-기반-osiv-설정-과정">Spring-Boot 의 프로퍼티 기반 OSIV 설정 과정</h2>
<p>Spring-Boot 는 프로퍼티를 기반으로 한 자동 설정을 지원한다. JpaBaseconfiguration 추상 클래스의 nested static class 인 <code>JpaWebConfiguration</code> 에에 OSIV 를 담당하는 인터셉터인 <code>OpenEntityanagerInViewInterceptor</code> 를 활성 여부를 관리한다. <code>spring.jpa.open-in-view</code> 프로퍼티를 통해 관리되며 별도의 설정이 없을 경우에는 <code>OpenEntityanagerInViewInterceptor</code> 는 빈으로 등록되고 인터셉터로 관리된다.</p>
<pre><code class="language-properties">spring.jpa.open-in-view: false # default value = true</code></pre>
<pre><code class="language-java">package org.springframework.boot.autoconfigure.orm.jpa;

// imports ...

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(JpaProperties.class)
public abstract class JpaBaseConfiguration implements BeanFactoryAware {

    private final DataSource dataSource;

    // 여기에 JpaProperties
    private final JpaProperties properties; 

    private final JtaTransactionManager jtaTransactionManager;

    private ConfigurableListableBeanFactory beanFactory;

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = Type.SERVLET)
    @ConditionalOnClass(WebMvcConfigurer.class)
    @ConditionalOnMissingBean({ OpenEntityManagerInViewInterceptor.class, OpenEntityManagerInViewFilter.class })
    @ConditionalOnMissingFilterBean(OpenEntityManagerInViewFilter.class)
    @ConditionalOnProperty(prefix = &quot;spring.jpa&quot;, name = &quot;open-in-view&quot;, havingValue = &quot;true&quot;, matchIfMissing = true)
    protected static class JpaWebConfiguration {

        private static final Log logger = LogFactory.getLog(JpaWebConfiguration.class);

        private final JpaProperties jpaProperties;

        protected JpaWebConfiguration(JpaProperties jpaProperties) {
            this.jpaProperties = jpaProperties;
        }

        // 실제 OSIV 를 담당하는 OpenEntityManagerInViewInterceptor 가 빈으로 선언되는 부분
        @Bean
        public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor() {
            if (this.jpaProperties.getOpenInView() == null) {
                logger.warn(&quot;spring.jpa.open-in-view is enabled by default. &quot;
                        + &quot;Therefore, database queries may be performed during view &quot;
                        + &quot;rendering. Explicitly configure spring.jpa.open-in-view to disable this warning&quot;);
            }
            return new OpenEntityManagerInViewInterceptor();
        }

        @Bean
        public WebMvcConfigurer openEntityManagerInViewInterceptorConfigurer(
                OpenEntityManagerInViewInterceptor interceptor) {
            return new WebMvcConfigurer() {

                @Override
                public void addInterceptors(InterceptorRegistry registry) {
                    registry.addWebRequestInterceptor(interceptor);
                }

            };
        }

    }</code></pre>
<br>

<h2 id="openentitymanagerinviewinterceptor-의-기본-동작을-확인해보자">OpenEntityManagerInViewInterceptor 의 기본 동작을 확인해보자.</h2>
<p>실제 코드를 확인해보면 요청이 들어올 경우에는 엔티티 매니저를 생성하고, 요청이 완료되는 경우에는 엔티티 매니저를 종료하는 것을 확인할 수 있다. 이외에도 엔티티 매니저가 관리되기 위한 <code>TransactionSynchronizationManager</code>, <code>AsyncRequestInterceptor</code> 와 같이 트랜잭션 동기화, 비동기를 위한 작업을 위한 설정이 있는 것을 확인해볼 수 있다.</p>
<pre><code class="language-java">package org.springframework.orm.jpa.support;

// imports ...

public class OpenEntityManagerInViewInterceptor extends EntityManagerFactoryAccessor implements AsyncWebRequestInterceptor {

    public static final String PARTICIPATE_SUFFIX = &quot;.PARTICIPATE&quot;;


    @Override
    public void preHandle(WebRequest request) throws DataAccessException {
        String key = getParticipateAttributeName();
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        if (asyncManager.hasConcurrentResult() &amp;&amp; applyEntityManagerBindingInterceptor(asyncManager, key)) {
            return;
        }

        EntityManagerFactory emf = obtainEntityManagerFactory();
        if (TransactionSynchronizationManager.hasResource(emf)) {
        // 트랜잭션 동기화와 관련된 로직
        }
        else {
            logger.debug(&quot;Opening JPA EntityManager in OpenEntityManagerInViewInterceptor&quot;);

            // EntityManager 가 생성된다.
            try {
                EntityManager em = createEntityManager();
                EntityManagerHolder emHolder = new EntityManagerHolder(em);
                TransactionSynchronizationManager.bindResource(emf, emHolder);

                AsyncRequestInterceptor interceptor = new AsyncRequestInterceptor(emf, emHolder);
                asyncManager.registerCallableInterceptor(key, interceptor);
                asyncManager.registerDeferredResultInterceptor(key, interceptor);
            }
            catch (PersistenceException ex) {
                throw new DataAccessResourceFailureException(&quot;Could not create JPA EntityManager&quot;, ex);
            }
        }
    }

    @Override
    public void afterCompletion(WebRequest request, @Nullable Exception ex) throws DataAccessException {
        if (!decrementParticipateCount(request)) {
            EntityManagerHolder emHolder = (EntityManagerHolder)
                    TransactionSynchronizationManager.unbindResource(obtainEntityManagerFactory());
            logger.debug(&quot;Closing JPA EntityManager in OpenEntityManagerInViewInterceptor&quot;);
            EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager()); // 2. 엔티티 매니저를 닫는다.
        }
    }
</code></pre>
<br>

<h2 id="만약-filter-에서-엔티티를-관리되어야-한다면">만약 Filter 에서 엔티티를 관리되어야 한다면?</h2>
<p> Spring Security 와 같이 FilterChain 기반에서 엔티티를 초기화해야 하는 경우가 있다. 일반적으로 <code>Open-Session-In-View</code> 는 안티 패턴이라 말한다. <u>요청이 시작, 종료되는 시점까지 DB Connection 을 잡고 있어 실시간 서비스와 같이 트래픽에 민감한 서비스의 경우에 성능 저하 요소가 될 수 있기 때문이다.</u> 하지만 사내의 시스템이 레거시 코드로 인해 필터 쪽에서 엔티티를 초기화해야 되서 불가피하게 사용해야 한다면 <code>OpenEntityManagerInViewFilter</code> 을 선언해서 사용해야 한다. <code>OpenEntityManagerInViewFilter</code> 는 스프링에서 지원해주기 때문에 해당 빈을 등록해서 사용하도록 하자. </p>
<p>(<em>만약 필터와 인터셉터의 차이를 잘 모르는 경우, 아래 블로그 글을 참고하도록 하자.</em>)</p>
<ul>
<li><a href="https://mangkyu.tistory.com/173">[mangkyu] [Spring] 필터(Filter) vs 인터셉터(Interceptor) 차이 및 용도 - (1)</a></li>
<li><a href="https://mangkyu.tistory.com/221">[mangkyu] [Spring] 필터(Filter)가 스프링 빈 등록과 주입이 가능한 이유(DelegatingFilterProxy의 등장) - (2)</a></li>
</ul>
<pre><code class="language-java">package org.springframework.orm.jpa.support;

// 여러가지 import

public class OpenEntityManagerInViewFilter extends OncePerRequestFilter {
// 비즈니스 로직
}</code></pre>
<pre><code class="language-java">// source : https://tecoble.techcourse.co.kr/post/2020-09-20-entity-lifecycle-2/

@Component
@Configuration
public class OpenEntityManagerConfig {
    @Bean
    public FilterRegistrationBean&lt;OpenEntityManagerInViewFilter&gt; openEntityManagerInViewFilter() {
        FilterRegistrationBean&lt;OpenEntityManagerInViewFilter&gt; filterFilterRegistrationBean = new FilterRegistrationBean&lt;&gt;();
        filterFilterRegistrationBean.setFilter(new OpenEntityManagerInViewFilter());
        filterFilterRegistrationBean.setOrder(Integer.MIN_VALUE); // 최우선 순위로 Filter 등록
        return filterFilterRegistrationBean;
    }
}</code></pre>
<br>

<h2 id="내가-생각하는-open-session-in-view">내가 생각하는 open-session-in-view</h2>
<p> 흔히 <code>open-session-in-view</code> 를 안티 패턴이라 부른다. 이전에 언급했듯이 실시간 트래픽이 중요한 서비스의 경우, 요청 시점에 EntityManager 가 생성되면서 DB Connection Pool 의 Connection 을 붙잡고 있어 Connection Pool 이 고갈될 수 있는 문제가 발생할 수 있다. </p>
<p> <code>open-session-in-view</code> 를 활성화 할 경우, DB 작업이 모두 완료되고 다른 외부 API 요청을 대기하는 시간까지 Connection 을 반환하지 않는다. <u>이와 같은 병목지점이 생기고 해당 로직이 많은 사용자가 사용한다면 ConnectionPool 의 Connection 을 모두 잡는다면 Connection 을 반환하기를 기다리는 문제가 발생할 수 있다.</u> 이는 사용자에게 나쁜 사용 경험을 제공해주기 때문에 특별한 이유가 없다면 사용하지 않는 것이 적합한 것 같다.</p>
<p><em>hikariCP 에서 제공하는 프로퍼티들의 기본 값 정도는 참고로 알고 있자.</em></p>
<pre><code class="language-properties"> # 주의 : DB 단위 - s, Spring Boot 단위 - ms
 # Spring Boot 설정을 Database 설정보다 적게 설정하는 것을 권장

 spring.datasource.hikari.minimumIdle=30000 #30초
 spring.datasource.hikari.connectionTimeout=30000 #30초
 spring.datasource.hikari.maximumPoolSize=10
 spring.datasource.hikari.idleTimeout=600000 # 10분
 spring.datasource.hikari.maxLifeTime=1800000 # 30분
 spring.datasource.hikari.autoCommit=true</code></pre>
<p> 비록 thymeleaf, jsp 와 같은 템플릿 엔진을 어드민 페이지로 사용하는 경우에 편의성을 가져다w주는 장점이 있지만 편리함으로 인해 템플릿 엔진에 의존적인 코드가 발생하고 vue.js 나 react 로 전환할 때의 공수가 늘어날 수 있다. 만약 기존 thymeleaf, jsp 와 같은 템플릿 엔진과 open-session-in-view 활성화해서 개발하고 있다면 최대한 엔티티 초기화를 템플릿 엔진에서 사용하지 않도록 하고, 엔티티 대신에 DTO 를 활용하는 방향으로 코드를 작성하는 방식으로 코드 작업을 하는 것이 낮은 의존성을 기반해서 유지보수하기 용이하지 않을까 싶다.</p>
<ul>
<li>신규 프로젝트에서 사용한다면 open-session-in-view 비활성화 하기</li>
<li>기존 프로젝트에서 사용한다면 open-session-in-view 를 비활성화하는 방향으로 작업하기 (e.g. DTO)</li>
</ul>
<br>

<h2 id="summary">summary</h2>
<ol>
<li>OSIV 는 영속성 컨텍스트의 라이프 사이클을 요청, 응답이 끝날 때까지 활성화하는 전략을 말한다.</li>
<li>OSIV 는 OpenEntityManagerInViewInterceptor, OpenEntityManagerInViewFilter 를 통해 영속화 범위가 확장된다.</li>
<li>OSIV 는 다른 로직의 처리가 길어지는만큼 Connection 을 붙잡고 있어 커넥션 고갈이 될 수 있는 문제로 인해 안티 패턴이다. 특별한 이유가 없다면 비활성화해서 사용하자. (비활성화 할경우, 영속성 컨텍스트는 @Transactional 내부에서만 관리된다.)</li>
</ol>
<br>

<h2 id="references">references</h2>
<ul>
<li><a href="https://www.yes24.com/Product/Goods/19040233">자바 ORM 표준 JPA 프로그래밍</a></li>
<li><a href="https://tecoble.techcourse.co.kr/post/2020-09-20-entity-lifecycle-2/">[tecoble] Entity Lifecycle을 고려해 코드를 작성하자 2편
</a></li>
<li><a href="https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.data.spring.datasource.hikari">[spring.io] Common Application Properties - Data Properties</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>